stageflow 0.36.0 → 0.40.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 (288) hide show
  1. package/README.md +61 -26
  2. package/dist/agent/claudeAdapter.js +21 -1
  3. package/dist/agent/claudeTools.d.ts +13 -0
  4. package/dist/agent/claudeTools.js +98 -0
  5. package/dist/agent/piAdapter.d.ts +9 -1
  6. package/dist/agent/piAdapter.js +59 -18
  7. package/dist/agent/piIsolatedMcp.js +14 -2
  8. package/dist/agent/piIsolatedMcpProbe.d.ts +5 -1
  9. package/dist/agent/piIsolatedMcpProbe.js +16 -30
  10. package/dist/agent/port.d.ts +5 -0
  11. package/dist/agent/sandboxPiTools.d.ts +99 -0
  12. package/dist/agent/sandboxPiTools.js +109 -0
  13. package/dist/agents/agentId.d.ts +8 -0
  14. package/dist/agents/agentId.js +27 -0
  15. package/dist/agents/approvalBroker.d.ts +16 -0
  16. package/dist/agents/approvalBroker.js +45 -0
  17. package/dist/agents/authorChat.d.ts +19 -0
  18. package/dist/agents/authorChat.js +63 -0
  19. package/dist/agents/chatTurn.d.ts +23 -0
  20. package/dist/agents/chatTurn.js +77 -0
  21. package/dist/agents/communications/agentmail.d.ts +19 -0
  22. package/dist/agents/communications/agentmail.js +46 -0
  23. package/dist/agents/communications/agentmailListener.d.ts +59 -0
  24. package/dist/agents/communications/agentmailListener.js +183 -0
  25. package/dist/agents/communications/allowlist.d.ts +10 -0
  26. package/dist/agents/communications/allowlist.js +36 -0
  27. package/dist/agents/communications/channelConnections.d.ts +3 -0
  28. package/dist/agents/communications/channelConnections.js +51 -0
  29. package/dist/agents/communications/delivery.d.ts +53 -0
  30. package/dist/agents/communications/delivery.js +142 -0
  31. package/dist/agents/communications/email.d.ts +26 -0
  32. package/dist/agents/communications/email.js +20 -0
  33. package/dist/agents/communications/identity.d.ts +13 -0
  34. package/dist/agents/communications/identity.js +18 -0
  35. package/dist/agents/communications/imessageListener.d.ts +41 -0
  36. package/dist/agents/communications/imessageListener.js +92 -0
  37. package/dist/agents/communications/inbound.d.ts +63 -0
  38. package/dist/agents/communications/inbound.js +143 -0
  39. package/dist/agents/communications/listener.d.ts +36 -0
  40. package/dist/agents/communications/listener.js +38 -0
  41. package/dist/agents/communications/photon.d.ts +19 -0
  42. package/dist/agents/communications/photon.js +38 -0
  43. package/dist/agents/communications/port.d.ts +27 -0
  44. package/dist/agents/communications/port.js +6 -0
  45. package/dist/agents/communications/slack.d.ts +28 -0
  46. package/dist/agents/communications/slack.js +65 -0
  47. package/dist/agents/communications/supervisor.d.ts +36 -0
  48. package/dist/agents/communications/supervisor.js +96 -0
  49. package/dist/agents/communications/telegram.d.ts +19 -0
  50. package/dist/agents/communications/telegram.js +52 -0
  51. package/dist/agents/definitionSummary.d.ts +13 -0
  52. package/dist/agents/definitionSummary.js +91 -0
  53. package/dist/agents/delegate.d.ts +3 -0
  54. package/dist/agents/delegate.js +8 -0
  55. package/dist/agents/errors.d.ts +10 -0
  56. package/dist/agents/errors.js +32 -0
  57. package/dist/agents/guardrails.d.ts +24 -0
  58. package/dist/agents/guardrails.js +39 -0
  59. package/dist/agents/historyFit.d.ts +10 -0
  60. package/dist/agents/historyFit.js +26 -0
  61. package/dist/agents/hostContext.d.ts +54 -0
  62. package/dist/agents/hostContext.js +156 -0
  63. package/dist/agents/ledgers.d.ts +29 -0
  64. package/dist/agents/ledgers.js +31 -0
  65. package/dist/agents/lineDiff.d.ts +12 -0
  66. package/dist/agents/lineDiff.js +46 -0
  67. package/dist/agents/ndjson.d.ts +9 -0
  68. package/dist/agents/ndjson.js +47 -0
  69. package/dist/agents/otlpExport.d.ts +9 -0
  70. package/dist/agents/otlpExport.js +58 -0
  71. package/dist/agents/otlpSink.d.ts +7 -0
  72. package/dist/agents/otlpSink.js +23 -0
  73. package/dist/agents/policy.d.ts +31 -0
  74. package/dist/agents/policy.js +84 -0
  75. package/dist/agents/projectRoot.d.ts +5 -0
  76. package/dist/agents/projectRoot.js +15 -0
  77. package/dist/agents/runtime/piRuntime.d.ts +52 -0
  78. package/dist/agents/runtime/piRuntime.js +474 -0
  79. package/dist/agents/runtime/types.d.ts +77 -0
  80. package/dist/agents/runtime/types.js +26 -0
  81. package/dist/agents/sandbox.d.ts +17 -0
  82. package/dist/agents/sandbox.js +10 -0
  83. package/dist/agents/scheduleSource.d.ts +32 -0
  84. package/dist/agents/scheduleSource.js +100 -0
  85. package/dist/agents/service.d.ts +298 -0
  86. package/dist/agents/service.js +1105 -0
  87. package/dist/agents/sessions.d.ts +38 -0
  88. package/dist/agents/sessions.js +321 -0
  89. package/dist/agents/store.d.ts +277 -0
  90. package/dist/agents/store.js +643 -0
  91. package/dist/agents/trace.d.ts +27 -0
  92. package/dist/agents/trace.js +29 -0
  93. package/dist/agents/turnLifecycle.d.ts +72 -0
  94. package/dist/agents/turnLifecycle.js +157 -0
  95. package/dist/agents/webChat/publicChat.d.ts +36 -0
  96. package/dist/agents/webChat/publicChat.js +290 -0
  97. package/dist/agents/webChat/rateLimit.d.ts +15 -0
  98. package/dist/agents/webChat/rateLimit.js +33 -0
  99. package/dist/agents/webChat/slug.d.ts +2 -0
  100. package/dist/agents/webChat/slug.js +9 -0
  101. package/dist/channels/providers/agentmail.js +14 -1
  102. package/dist/channels/providers/photonImessage.js +36 -0
  103. package/dist/channels/providers/slack.js +2 -1
  104. package/dist/channels/providers/telegram.js +2 -1
  105. package/dist/channels/types.d.ts +27 -0
  106. package/dist/cli/agentsCommand.d.ts +14 -0
  107. package/dist/cli/agentsCommand.js +291 -0
  108. package/dist/cli.js +9 -0
  109. package/dist/config/browseCatalog.js +4 -11
  110. package/dist/config/draftPackage.d.ts +1 -0
  111. package/dist/config/draftPackage.js +4 -0
  112. package/dist/config/hostConfig.js +9 -0
  113. package/dist/config/loadAgent.d.ts +266 -0
  114. package/dist/config/loadAgent.js +374 -0
  115. package/dist/config/loadPipeline.js +5 -2
  116. package/dist/config/loadTask.js +34 -1
  117. package/dist/config/mcpHostCatalog.d.ts +50 -0
  118. package/dist/config/mcpHostCatalog.js +405 -0
  119. package/dist/config/mcpOAuth.d.ts +37 -0
  120. package/dist/config/mcpOAuth.js +263 -0
  121. package/dist/config/mcpRedaction.d.ts +42 -0
  122. package/dist/config/mcpRedaction.js +314 -0
  123. package/dist/config/mcpToolCache.d.ts +20 -0
  124. package/dist/config/mcpToolCache.js +43 -0
  125. package/dist/config/mergePipelineIncludes.d.ts +2 -0
  126. package/dist/config/mergePipelineIncludes.js +25 -0
  127. package/dist/config/resolveStageMcpServers.d.ts +35 -3
  128. package/dist/config/resolveStageMcpServers.js +124 -9
  129. package/dist/config/validateCatalog.d.ts +1 -1
  130. package/dist/config/validateCatalog.js +22 -1
  131. package/dist/mcp/agentTools.d.ts +8 -0
  132. package/dist/mcp/agentTools.js +132 -0
  133. package/dist/mcp/catalogTools.js +17 -10
  134. package/dist/mcp/deps.d.ts +6 -0
  135. package/dist/mcp/pipelineAuthorSupport.d.ts +19 -0
  136. package/dist/mcp/pipelineAuthorSupport.js +10 -0
  137. package/dist/mcp/pipelineAuthorTools.d.ts +8 -0
  138. package/dist/mcp/pipelineAuthorTools.js +109 -0
  139. package/dist/mcp/server.js +5 -0
  140. package/dist/mcp/tools.js +4 -0
  141. package/dist/mcp/triggerTools.js +32 -0
  142. package/dist/memory/access.d.ts +36 -0
  143. package/dist/memory/access.js +116 -0
  144. package/dist/memory/agentIssues.d.ts +3 -0
  145. package/dist/memory/agentIssues.js +23 -0
  146. package/dist/memory/bashDeny.d.ts +6 -0
  147. package/dist/memory/bashDeny.js +35 -0
  148. package/dist/memory/host.d.ts +51 -0
  149. package/dist/memory/host.js +394 -0
  150. package/dist/memory/ids.d.ts +15 -0
  151. package/dist/memory/ids.js +31 -0
  152. package/dist/memory/worker.d.ts +1 -0
  153. package/dist/memory/worker.js +84 -0
  154. package/dist/models/catalog.d.ts +113 -0
  155. package/dist/models/catalog.js +238 -0
  156. package/dist/models/response.d.ts +29 -0
  157. package/dist/models/response.js +25 -0
  158. package/dist/operatorAgent/agentAuthorContext.d.ts +17 -0
  159. package/dist/operatorAgent/agentAuthorContext.js +32 -0
  160. package/dist/operatorAgent/fakeHost.js +2 -0
  161. package/dist/operatorAgent/host.js +6 -3
  162. package/dist/operatorAgent/index.d.ts +4 -0
  163. package/dist/operatorAgent/index.js +4 -0
  164. package/dist/operatorAgent/piModel.js +2 -0
  165. package/dist/operatorAgent/profiles/agentAuthor.d.ts +11 -0
  166. package/dist/operatorAgent/profiles/agentAuthor.js +192 -0
  167. package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -0
  168. package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +29 -0
  169. package/dist/operatorAgent/profiles/workshopAuthor.js +2 -2
  170. package/dist/operatorAgent/profiles/workshopAuthorTools.js +3 -3
  171. package/dist/operatorAgent/proposals.d.ts +1 -0
  172. package/dist/operatorAgent/proposals.js +23 -2
  173. package/dist/package-meta.d.ts +1 -1
  174. package/dist/package-meta.js +1 -1
  175. package/dist/preflight/pipelinePreflight.js +1 -0
  176. package/dist/runstore/createStore.d.ts +2 -0
  177. package/dist/runstore/createStore.js +8 -0
  178. package/dist/runstore/overviewSummary.d.ts +56 -0
  179. package/dist/runstore/overviewSummary.js +134 -0
  180. package/dist/runstore/port.d.ts +3 -0
  181. package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -0
  182. package/dist/runstore/sqlite/SqliteRunStore.js +18 -0
  183. package/dist/runstore/sqlite/migrations/013-agents.d.ts +12 -0
  184. package/dist/runstore/sqlite/migrations/013-agents.js +156 -0
  185. package/dist/runstore/sqlite/migrations/014-agent-deliveries.d.ts +11 -0
  186. package/dist/runstore/sqlite/migrations/014-agent-deliveries.js +32 -0
  187. package/dist/runstore/sqlite/migrations/index.js +4 -0
  188. package/dist/runtime/gitCheckoutCapability.d.ts +1 -0
  189. package/dist/runtime/gitCheckoutCapability.js +1 -1
  190. package/dist/runtime/gitWorktreeCheckout.d.ts +22 -0
  191. package/dist/runtime/gitWorktreeCheckout.js +120 -0
  192. package/dist/runtime/sandboxContainer.d.ts +62 -0
  193. package/dist/runtime/sandboxContainer.js +151 -0
  194. package/dist/runtime/sandboxDesk.d.ts +28 -0
  195. package/dist/runtime/sandboxDesk.js +242 -0
  196. package/dist/runtime/sandboxFiles.d.ts +20 -0
  197. package/dist/runtime/sandboxFiles.js +150 -0
  198. package/dist/runtime/stageAttemptBootstrap.js +15 -1
  199. package/dist/runtime/stageEnvironment.d.ts +2 -0
  200. package/dist/runtime/stageEnvironment.js +8 -1
  201. package/dist/runtime/stageProcessLauncher.js +23 -0
  202. package/dist/runtime/stageRoots.d.ts +9 -0
  203. package/dist/runtime/stageRoots.js +14 -0
  204. package/dist/runtime/stageRunner.d.ts +5 -0
  205. package/dist/runtime/stageRunner.js +38 -4
  206. package/dist/runtime/stageSandbox.d.ts +16 -0
  207. package/dist/runtime/stageSandbox.js +54 -0
  208. package/dist/runtime/stageSecrets.d.ts +6 -1
  209. package/dist/runtime/stageSecrets.js +34 -1
  210. package/dist/runtime/stageWorker.js +138 -105
  211. package/dist/runtime/workspaceBinding.d.ts +2 -1
  212. package/dist/runtime/workspaceBinding.js +20 -4
  213. package/dist/server/agentAuthorRoutes.d.ts +5 -0
  214. package/dist/server/agentAuthorRoutes.js +64 -0
  215. package/dist/server/agentRoutes.d.ts +5 -0
  216. package/dist/server/agentRoutes.js +323 -0
  217. package/dist/server/bootstrap.d.ts +8 -0
  218. package/dist/server/bootstrap.js +36 -10
  219. package/dist/server/controlToken.js +8 -0
  220. package/dist/server/http.d.ts +8 -1
  221. package/dist/server/http.js +172 -20
  222. package/dist/server/mcpHost.js +1 -0
  223. package/dist/server/memoryRoutes.d.ts +5 -0
  224. package/dist/server/memoryRoutes.js +146 -0
  225. package/dist/server/projectMcpRoutes.d.ts +1 -0
  226. package/dist/server/projectMcpRoutes.js +241 -3
  227. package/dist/toolkit/guard.d.ts +27 -0
  228. package/dist/toolkit/guard.js +98 -0
  229. package/dist/toolkit/index.d.ts +48 -0
  230. package/dist/toolkit/index.js +82 -0
  231. package/dist/toolkit/meta.d.ts +36 -0
  232. package/dist/toolkit/meta.js +117 -0
  233. package/dist/toolkit/profiles.d.ts +13 -0
  234. package/dist/toolkit/profiles.js +25 -0
  235. package/dist/toolkit/record.d.ts +15 -0
  236. package/dist/toolkit/record.js +17 -0
  237. package/dist/tools/memoryTools.d.ts +95 -0
  238. package/dist/tools/memoryTools.js +179 -0
  239. package/dist/types/pipeline.d.ts +3 -0
  240. package/dist/types/stage.d.ts +2 -0
  241. package/dist/types/task.d.ts +11 -1
  242. package/dist/ui/assets/AssistantRuntimeProvider-CsiPFhCT.js +9 -0
  243. package/dist/ui/assets/MarkdownText-D7tYaIFO.js +29 -0
  244. package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +6 -0
  245. package/dist/ui/assets/Tooltip-DrpoRRNi.js +1 -0
  246. package/dist/ui/assets/chat-BbLukq9W.js +5 -0
  247. package/dist/ui/assets/chat-zIZU_bP1.css +2 -0
  248. package/dist/ui/assets/jsx-runtime-iVoFAvQ_.js +1 -0
  249. package/dist/ui/assets/{index-Clv4sJpU.css → main-BFo79krC.css} +1 -1
  250. package/dist/ui/assets/main-CijYMeM0.js +259 -0
  251. package/dist/ui/chat.html +25 -0
  252. package/dist/ui/index.html +6 -2
  253. package/dist/workshop/modelSettings.d.ts +2 -2
  254. package/dist/workshop/modelSettings.js +5 -11
  255. package/examples/README.md +1 -0
  256. package/examples/channels-smoke/README.md +12 -1
  257. package/examples/github-release/README.md +1 -1
  258. package/examples/memory/README.md +101 -0
  259. package/examples/memory/desk-outage.task.yaml +15 -0
  260. package/examples/memory/desk-refund.task.yaml +15 -0
  261. package/examples/memory/desk-revisit-outage.task.yaml +11 -0
  262. package/examples/memory/desk-revisit.pipeline.yaml +123 -0
  263. package/examples/memory/desk.pipeline.yaml +192 -0
  264. package/examples/memory/file-card.pipeline.yaml +39 -0
  265. package/examples/memory/file-ledger.task.yaml +22 -0
  266. package/examples/memory/file-release.task.yaml +24 -0
  267. package/examples/memory/file-rich.pipeline.yaml +40 -0
  268. package/examples/memory/file-ticket.task.yaml +7 -0
  269. package/examples/memory/file-timeline.task.yaml +28 -0
  270. package/examples/memory/find-card.pipeline.yaml +46 -0
  271. package/examples/memory/find-ticket.task.yaml +7 -0
  272. package/examples/memory/missing.pipeline.yaml +16 -0
  273. package/examples/memory/probe-bash.task.yaml +3 -0
  274. package/examples/memory/recall.pipeline.yaml +24 -0
  275. package/examples/memory/recall.task.yaml +5 -0
  276. package/examples/memory/remember-ada.task.yaml +6 -0
  277. package/examples/memory/remember-grace.task.yaml +6 -0
  278. package/examples/memory/remember.pipeline.yaml +58 -0
  279. package/examples/memory/sandbox-memory-bash.pipeline.yaml +28 -0
  280. package/examples/memory/stageflow.yaml +12 -0
  281. package/examples/memory/update-card.pipeline.yaml +43 -0
  282. package/examples/memory/update-ledger.task.yaml +23 -0
  283. package/examples/memory/update-rich.pipeline.yaml +44 -0
  284. package/examples/memory/update-ticket.task.yaml +7 -0
  285. package/examples/ship-feature/README.md +15 -0
  286. package/examples/stage-mcp/README.md +1 -1
  287. package/package.json +2 -1
  288. package/dist/ui/assets/index-BOhqnPNg.js +0 -260
@@ -15,6 +15,7 @@ export declare function mergePipelineStages(rootPath: string): Promise<LoadOutco
15
15
  schemas?: PayloadSchemaMap;
16
16
  requires?: ToolRequirement[];
17
17
  channels?: StageChannel[];
18
+ memories?: string[];
18
19
  warnings: LoadIssue[];
19
20
  }>>;
20
21
  /** Synthetic `declaringPath` for stage entries with no backing file. */
@@ -27,5 +28,6 @@ export declare function mergeInlinePipelineStages(raw: InlinePipelineDefinition)
27
28
  schemas?: PayloadSchemaMap;
28
29
  requires?: ToolRequirement[];
29
30
  channels?: StageChannel[];
31
+ memories?: string[];
30
32
  warnings: LoadIssue[];
31
33
  }>>;
@@ -7,6 +7,7 @@ import { readYamlObject } from "./readYamlObject.js";
7
7
  import { yamlParsePosition } from "./yamlParsePosition.js";
8
8
  import { parseToolRequires } from "./toolRequires.js";
9
9
  import { parseChannels } from "../channels/stageChannels.js";
10
+ import { parseMemoryIdList } from "../memory/ids.js";
10
11
  import { classifyYamlDocument, collectDocumentKeys, mixedDialectIssue, } from "./yamlDialect.js";
11
12
  function isPlainObject(value) {
12
13
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -194,6 +195,10 @@ export async function mergePipelineStages(rootPath) {
194
195
  if (!channelsOutcome.ok)
195
196
  return channelsOutcome;
196
197
  const channels = channelsOutcome.value;
198
+ const memoriesOutcome = parsePipelineMemories(raw.memories, `pipeline ${absRoot}`, pipelineId);
199
+ if (!memoriesOutcome.ok)
200
+ return memoriesOutcome;
201
+ const memories = memoriesOutcome.value;
197
202
  const idLocations = new Map();
198
203
  const entries = [];
199
204
  const warnings = [];
@@ -217,6 +222,7 @@ export async function mergePipelineStages(rootPath) {
217
222
  ...(schemas !== undefined ? { schemas } : {}),
218
223
  ...(requires !== undefined ? { requires } : {}),
219
224
  ...(channels !== undefined ? { channels } : {}),
225
+ ...(memories !== undefined ? { memories } : {}),
220
226
  warnings,
221
227
  });
222
228
  }
@@ -283,6 +289,10 @@ export async function mergeInlinePipelineStages(raw) {
283
289
  if (!channelsOutcome.ok)
284
290
  return channelsOutcome;
285
291
  const channels = channelsOutcome.value;
292
+ const memoriesOutcome = parsePipelineMemories(raw.memories, `inline pipeline ${pipelineId}`, pipelineId);
293
+ if (!memoriesOutcome.ok)
294
+ return memoriesOutcome;
295
+ const memories = memoriesOutcome.value;
286
296
  const entries = [];
287
297
  for (let index = 0; index < raw.stages.length; index++) {
288
298
  const entry = raw.stages[index];
@@ -306,9 +316,24 @@ export async function mergeInlinePipelineStages(raw) {
306
316
  ...(schemas !== undefined ? { schemas } : {}),
307
317
  ...(requires !== undefined ? { requires } : {}),
308
318
  ...(channels !== undefined ? { channels } : {}),
319
+ ...(memories !== undefined ? { memories } : {}),
309
320
  warnings: [],
310
321
  });
311
322
  }
323
+ function parsePipelineMemories(raw, label, pipelineId) {
324
+ const parsed = parseMemoryIdList(raw);
325
+ if (!parsed.ok) {
326
+ return loadFailure([
327
+ {
328
+ code: "pipeline.invalid_memories",
329
+ message: `Invalid ${label}: ${parsed.message}`,
330
+ category: "pipeline",
331
+ pipelineId,
332
+ },
333
+ ]);
334
+ }
335
+ return loadSuccess(parsed.value);
336
+ }
312
337
  function parsePipelineChannels(raw, label, pipelineId) {
313
338
  if (raw === undefined)
314
339
  return loadSuccess(undefined);
@@ -1,4 +1,4 @@
1
- export type StageMcpErrorCode = "missing_catalog" | "unknown_server" | "reserved_name" | "unresolved_var" | "invalid_config" | "untrusted_config_origin" | "connect_failed";
1
+ export type StageMcpErrorCode = "missing_catalog" | "unknown_server" | "reserved_name" | "unresolved_var" | "invalid_config" | "untrusted_config_origin" | "connect_failed" | "host_state_corrupt";
2
2
  export declare class StageMcpError extends Error {
3
3
  readonly code: StageMcpErrorCode;
4
4
  constructor(message: string, code: StageMcpErrorCode);
@@ -9,18 +9,45 @@ export declare const MCP_CATALOG_FILENAME = ".mcp.json";
9
9
  export declare const STAGEFLOW_STAGE_ARTIFACTS_DIR_ENV = "STAGEFLOW_STAGE_ARTIFACTS_DIR";
10
10
  export declare function stampStagePromptArtifactsDir(prompt: string, artifactsDir: string): string;
11
11
  export type McpCatalogServers = Record<string, Record<string, unknown>>;
12
+ export type McpServerOrigin = "project" | "console";
12
13
  export type InspectedMcpCatalog = {
13
14
  path: string;
14
15
  servers: McpCatalogServers;
16
+ /** Where each server came from; `console` = saved from the UI under $STAGEFLOW_HOME. */
17
+ origins: Record<string, McpServerOrigin>;
18
+ };
19
+ /**
20
+ * Who is asking. Without a scope only the project `.mcp.json` is visible; with one, servers saved
21
+ * from the console are merged in (console entry wins on a name clash) and secrets are filled by
22
+ * the rules of that scope:
23
+ * - stage: console secrets whose names the stage declared in `secrets:`, plus the stage env
24
+ * - agent: console secrets plus Host env names listed in STAGEFLOW_MCP_ENV_ALLOW
25
+ * - probe: same as agent, so a probe and an agent run resolve one config identically
26
+ * - validate: placeholders are checked but no secret value is ever read into the output
27
+ * Console secrets always win over Host env for the same name.
28
+ */
29
+ export type McpResolveScope = {
30
+ kind: "stage";
31
+ secrets: readonly string[];
32
+ } | {
33
+ kind: "agent";
34
+ } | {
35
+ kind: "probe";
36
+ } | {
37
+ kind: "validate";
38
+ };
39
+ export type McpCatalogLoadOptions = {
40
+ scope?: McpResolveScope;
15
41
  };
16
42
  export declare function mcpCatalogPath(projectRoot: string): string;
17
43
  export declare function parseMcpCatalog(raw: string, catalogPath?: string): McpCatalogServers;
18
- export declare function mcpCatalogExists(projectRoot: string): Promise<boolean>;
19
- export declare function loadMcpCatalog(projectRoot: string): Promise<InspectedMcpCatalog>;
44
+ export declare function mcpCatalogExists(projectRoot: string, options?: McpCatalogLoadOptions): Promise<boolean>;
45
+ export declare function loadMcpCatalog(projectRoot: string, options?: McpCatalogLoadOptions): Promise<InspectedMcpCatalog>;
20
46
  export type ProjectMcpCatalogTransport = "stdio" | "http";
21
47
  export type ProjectMcpCatalogEntry = {
22
48
  name: string;
23
49
  transport: ProjectMcpCatalogTransport;
50
+ origin: McpServerOrigin;
24
51
  };
25
52
  export type ProjectMcpCatalogListStatus = "ok" | "missing_catalog" | "invalid_config";
26
53
  export type ProjectMcpCatalogList = {
@@ -32,7 +59,10 @@ export declare function assertMcpAllowlistKnown(servers: McpCatalogServers, allo
32
59
  export declare function resolveStageMcpServers(options: {
33
60
  projectRoot: string;
34
61
  allowlist?: readonly string[];
62
+ /** Stage env for the project-only and stage scopes; agent/probe/validate do not read it as a secret source. */
35
63
  env: NodeJS.ProcessEnv;
64
+ /** Host env that STAGEFLOW_MCP_ENV_ALLOW may draw from (agent/probe scopes). Defaults to process.env. */
65
+ hostEnv?: NodeJS.ProcessEnv;
36
66
  stageId?: string;
37
67
  bindingKind?: "repository" | "checkout" | "none";
38
68
  trustWorkspaceConfig?: string[];
@@ -40,4 +70,6 @@ export declare function resolveStageMcpServers(options: {
40
70
  trustProjectRoot?: string;
41
71
  /** When true, .mcp.json is treated as workspace-sourced. */
42
72
  workspaceSourced?: boolean;
73
+ /** Merge console-saved servers and fill `${VAR}` by the scope's rules. Omit for project-only. */
74
+ scope?: McpResolveScope;
43
75
  }): Promise<ResolvedMcpServers>;
@@ -1,6 +1,8 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { STAGEFLOW_MCP_SERVER_NAME } from "../agent/claudeTools.js";
4
+ import { isMcpHostStateError, mcpEnvAllowNames, readHostMcpServers, readMcpSecrets, } from "./mcpHostCatalog.js";
5
+ import { assertSafeMcpUrl, McpUrlPolicyError } from "./mcpRedaction.js";
4
6
  export class StageMcpError extends Error {
5
7
  code;
6
8
  constructor(message, code) {
@@ -51,7 +53,31 @@ export function parseMcpCatalog(raw, catalogPath = MCP_CATALOG_FILENAME) {
51
53
  }
52
54
  return servers;
53
55
  }
54
- export async function mcpCatalogExists(projectRoot) {
56
+ /** Console servers; a corrupt host file surfaces as a StageMcpError instead of reading as empty. */
57
+ function hostServersOrThrow() {
58
+ try {
59
+ return readHostMcpServers();
60
+ }
61
+ catch (err) {
62
+ if (isMcpHostStateError(err))
63
+ throw new StageMcpError(err.message, "host_state_corrupt");
64
+ throw err;
65
+ }
66
+ }
67
+ function hostSecretsOrThrow() {
68
+ try {
69
+ return readMcpSecrets();
70
+ }
71
+ catch (err) {
72
+ if (isMcpHostStateError(err))
73
+ throw new StageMcpError(err.message, "host_state_corrupt");
74
+ throw err;
75
+ }
76
+ }
77
+ export async function mcpCatalogExists(projectRoot, options = {}) {
78
+ if (options.scope !== undefined && Object.keys(hostServersOrThrow()).length > 0) {
79
+ return true;
80
+ }
55
81
  try {
56
82
  await readFile(mcpCatalogPath(projectRoot), "utf8");
57
83
  return true;
@@ -63,20 +89,31 @@ export async function mcpCatalogExists(projectRoot) {
63
89
  throw err;
64
90
  }
65
91
  }
66
- export async function loadMcpCatalog(projectRoot) {
92
+ export async function loadMcpCatalog(projectRoot, options = {}) {
67
93
  const catalogPath = mcpCatalogPath(projectRoot);
94
+ const hostServers = options.scope !== undefined ? hostServersOrThrow() : {};
68
95
  let raw;
69
96
  try {
70
97
  raw = await readFile(catalogPath, "utf8");
71
98
  }
72
99
  catch (err) {
73
100
  const code = err.code;
74
- if (code === "ENOENT") {
101
+ if (code !== "ENOENT") {
102
+ throw new StageMcpError(`MCP catalog "${MCP_CATALOG_FILENAME}" could not be read`, "invalid_config");
103
+ }
104
+ if (Object.keys(hostServers).length === 0) {
75
105
  throw new StageMcpError(`MCP catalog "${MCP_CATALOG_FILENAME}" is missing`, "missing_catalog");
76
106
  }
77
- throw new StageMcpError(`MCP catalog "${MCP_CATALOG_FILENAME}" could not be read`, "invalid_config");
78
107
  }
79
- return { path: catalogPath, servers: parseMcpCatalog(raw, catalogPath) };
108
+ const servers = raw !== undefined ? parseMcpCatalog(raw, catalogPath) : {};
109
+ const origins = {};
110
+ for (const name of Object.keys(servers))
111
+ origins[name] = "project";
112
+ for (const [name, entry] of Object.entries(hostServers)) {
113
+ servers[name] = entry;
114
+ origins[name] = "console";
115
+ }
116
+ return { path: catalogPath, servers, origins };
80
117
  }
81
118
  function coarseMcpTransport(entry) {
82
119
  if (Object.hasOwn(entry, "command")) {
@@ -89,14 +126,20 @@ function coarseMcpTransport(entry) {
89
126
  }
90
127
  export async function listProjectMcpCatalog(projectRoot) {
91
128
  try {
92
- const catalog = await loadMcpCatalog(projectRoot);
129
+ const catalog = await loadMcpCatalog(projectRoot, {
130
+ scope: { kind: "validate" },
131
+ });
93
132
  const servers = [];
94
133
  for (const [name, entry] of Object.entries(catalog.servers)) {
95
134
  const transport = coarseMcpTransport(entry);
96
135
  if (transport === undefined) {
97
136
  return { status: "invalid_config", servers: [] };
98
137
  }
99
- servers.push({ name, transport });
138
+ servers.push({
139
+ name,
140
+ transport,
141
+ origin: catalog.origins[name] ?? "project",
142
+ });
100
143
  }
101
144
  return { status: "ok", servers };
102
145
  }
@@ -207,6 +250,69 @@ function interpolateServer(entry, env, serverName, projectRoot, stageId) {
207
250
  }
208
251
  return stampSpawnRoot(resolved, projectRoot, serverName);
209
252
  }
253
+ function bagOfStrings(value) {
254
+ return isPlainObject(value) && Object.values(value).every((v) => typeof v === "string");
255
+ }
256
+ /** Shape check for any entry about to be used (project or console). */
257
+ function assertEntryShape(name, entry) {
258
+ const bad = (what) => {
259
+ throw new StageMcpError(`MCP catalog server "${name}" ${what}`, "invalid_config");
260
+ };
261
+ const hasCommand = Object.hasOwn(entry, "command");
262
+ const hasUrl = Object.hasOwn(entry, "url");
263
+ if (hasCommand === hasUrl)
264
+ bad('needs exactly one of "command" or "url"');
265
+ if (hasCommand && (typeof entry.command !== "string" || entry.command.length === 0)) {
266
+ bad('"command" must be a non-empty string');
267
+ }
268
+ if (hasUrl && (typeof entry.url !== "string" || entry.url.length === 0)) {
269
+ bad('"url" must be a non-empty string');
270
+ }
271
+ if (entry.args !== undefined &&
272
+ (!Array.isArray(entry.args) || entry.args.some((a) => typeof a !== "string"))) {
273
+ bad('"args" must be an array of strings');
274
+ }
275
+ for (const key of ["env", "headers"]) {
276
+ if (entry[key] !== undefined && !bagOfStrings(entry[key]))
277
+ bad(`"${key}" must map names to strings`);
278
+ }
279
+ if (entry.cwd !== undefined && typeof entry.cwd !== "string")
280
+ bad('"cwd" must be a string');
281
+ }
282
+ function assertResolvedUrlSafe(name, resolved) {
283
+ if (typeof resolved.url !== "string")
284
+ return;
285
+ try {
286
+ assertSafeMcpUrl(resolved.url);
287
+ }
288
+ catch (err) {
289
+ if (err instanceof McpUrlPolicyError) {
290
+ throw new StageMcpError(`MCP catalog server "${name}": ${err.message}`, "invalid_config");
291
+ }
292
+ throw err;
293
+ }
294
+ }
295
+ const VALIDATE_PLACEHOLDER = "<redacted>";
296
+ /** The values `${NAME}` may be filled from, by scope. Console secrets win over Host env. */
297
+ function envForScope(scope, stageEnv, hostEnv) {
298
+ const secrets = hostSecretsOrThrow();
299
+ if (scope.kind === "stage") {
300
+ const declared = new Set(scope.secrets);
301
+ const own = Object.fromEntries(Object.entries(secrets).filter(([n]) => declared.has(n)));
302
+ return { ...stageEnv, ...own };
303
+ }
304
+ const allowed = {};
305
+ for (const n of mcpEnvAllowNames(hostEnv)) {
306
+ if (hostEnv[n] !== undefined)
307
+ allowed[n] = hostEnv[n];
308
+ }
309
+ if (scope.kind === "validate") {
310
+ const names = new Set([...Object.keys(secrets), ...Object.keys(allowed), ...Object.keys(stageEnv)]);
311
+ return Object.fromEntries([...names].map((n) => [n, VALIDATE_PLACEHOLDER]));
312
+ }
313
+ // agent and probe: one rule, so the same config resolves the same way in both.
314
+ return { ...allowed, ...secrets };
315
+ }
210
316
  export async function resolveStageMcpServers(options) {
211
317
  const allowlist = options.allowlist ?? [];
212
318
  if (allowlist.length === 0) {
@@ -224,11 +330,20 @@ export async function resolveStageMcpServers(options) {
224
330
  throw new StageMcpError(decision.message ?? "untrusted_config_origin", "untrusted_config_origin");
225
331
  }
226
332
  }
227
- const catalog = await loadMcpCatalog(options.projectRoot);
333
+ // Workspace-sourced catalogs never see console servers or secrets.
334
+ const scope = options.workspaceSourced === true ? undefined : options.scope;
335
+ const catalog = await loadMcpCatalog(options.projectRoot, scope ? { scope } : {});
228
336
  assertMcpAllowlistKnown(catalog.servers, allowlist);
337
+ const env = scope
338
+ ? envForScope(scope, options.env, options.hostEnv ?? process.env)
339
+ : options.env;
229
340
  const resolved = {};
230
341
  for (const name of allowlist) {
231
- resolved[name] = interpolateServer(catalog.servers[name], options.env, name, options.projectRoot, options.stageId);
342
+ const entry = catalog.servers[name];
343
+ assertEntryShape(name, entry);
344
+ resolved[name] = interpolateServer(entry, env, name, options.projectRoot, options.stageId);
345
+ if (scope?.kind !== "validate")
346
+ assertResolvedUrlSafe(name, resolved[name]);
232
347
  }
233
348
  return resolved;
234
349
  }
@@ -4,7 +4,7 @@ import type { LoadIssue } from "./loadOutcome.js";
4
4
  import type { AnyChannelProvider } from "../channels/types.js";
5
5
  export type ValidationSeverity = "error" | "warning";
6
6
  export type ValidationCategory = "pipeline" | "stage" | "catalog" | "task" | "trigger";
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
+ 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" | "pipeline.invalid_memories" | "pipeline.memory_unavailable" | "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";
8
8
  export type ValidationFinding = {
9
9
  severity: ValidationSeverity;
10
10
  code: ValidationFindingCode | string;
@@ -17,6 +17,7 @@ import { findMissingMcpCommands } from "../preflight/mcpCommands.js";
17
17
  import { ChannelError } from "../channels/errors.js";
18
18
  import { ChannelRegistry } from "../channels/registry.js";
19
19
  import { validateStageChannels } from "../channels/stageChannels.js";
20
+ import { inspectMemorySync, memoryHome } from "../memory/host.js";
20
21
  export function relPath(cwd, absPath) {
21
22
  return path.relative(cwd, absPath).replace(/\\/g, "/");
22
23
  }
@@ -276,6 +277,25 @@ function findingsForStageChannels(cwd, pipelinePath, loaded, projectRoot, provid
276
277
  }
277
278
  return users.flatMap((stage) => validateStageChannels(registry, stage).map((issue) => findingStageError(cwd, pipelinePath, issue.message, "stage.channel_unavailable", issue.stageId)));
278
279
  }
280
+ const MEMORY_FINDING = {
281
+ MEMORY_NOT_FOUND: (id) => `Memory "${id}" does not exist. Create it before attaching it.`,
282
+ MEMORY_MISMATCH: (id) => `Memory "${id}" does not match the id in its file.`,
283
+ MEMORY_UNREADABLE: (id) => `Memory "${id}" could not be read.`,
284
+ MEMORY_INVALID_ID: (id) => `Memory "${id}" is not a valid id.`,
285
+ };
286
+ function findingsForMemories(cwd, pipelinePath, loaded) {
287
+ const ids = loaded.pipeline.memories;
288
+ if (!ids?.length)
289
+ return [];
290
+ const home = memoryHome();
291
+ return ids.flatMap((id) => {
292
+ const inspected = inspectMemorySync(home, id);
293
+ if (inspected.ok)
294
+ return [];
295
+ const message = (MEMORY_FINDING[inspected.code] ?? MEMORY_FINDING.MEMORY_UNREADABLE)(id);
296
+ return [findingPipelineError(cwd, pipelinePath, message, "pipeline.memory_unavailable", loaded.pipeline.id)];
297
+ });
298
+ }
279
299
  function findingsForStageSecrets(cwd, pipelinePath, loaded) {
280
300
  const registry = loadSecretRegistry(process.env);
281
301
  const findings = [];
@@ -386,7 +406,7 @@ async function findingsForStageMcpCatalog(cwd, loaded) {
386
406
  }
387
407
  let catalog;
388
408
  try {
389
- catalog = await loadMcpCatalog(projectRoot);
409
+ catalog = await loadMcpCatalog(projectRoot, { scope: { kind: "validate" } });
390
410
  }
391
411
  catch (err) {
392
412
  if (!(err instanceof StageMcpError))
@@ -459,6 +479,7 @@ async function runPipelineValidation(nameOrPath, options) {
459
479
  findings.push(...(await findingsForStageMcpCatalog(cwd, outcome.value)));
460
480
  findings.push(...findingsForStageSecrets(cwd, pipelinePath, outcome.value));
461
481
  findings.push(...findingsForStageChannels(cwd, pipelinePath, outcome.value, projectRoot, options.channelProviders));
482
+ findings.push(...findingsForMemories(cwd, pipelinePath, outcome.value));
462
483
  findings.push(...findingsForStageflowHomeAbsolutePaths(cwd, outcome.value));
463
484
  if (validateStages && outcome.value.stageSources) {
464
485
  for (const source of Object.values(outcome.value.stageSources)) {
@@ -0,0 +1,8 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
+ import type { McpToolDeps } from "./deps.js";
3
+ /**
4
+ * Agent management for operators driving Stageflow over MCP. A running agent reaches these
5
+ * only through the Stageflow tool groups it is granted (`agents.*`); the guard in
6
+ * `toolkit/guard.ts` stops it acting on its own sessions.
7
+ */
8
+ export declare function registerAgentTools(server: McpServer, deps: McpToolDeps): void;
@@ -0,0 +1,132 @@
1
+ import { z } from "zod";
2
+ import { defaultAgentProjectRoot } from "../agents/projectRoot.js";
3
+ import { AGENT_ID_PATTERN } from "../config/loadAgent.js";
4
+ import { AgentError, isAgentError } from "../agents/errors.js";
5
+ import { SESSION_STATUSES } from "../agents/store.js";
6
+ import { textResult } from "./toolResults.js";
7
+ const agentId = z.string().regex(AGENT_ID_PATTERN);
8
+ const MCP_ACTOR = { source: "api", author: "mcp" };
9
+ function required(value, what) {
10
+ if (value === undefined)
11
+ throw new Error(`MCP agent tools require ${what} from host bootstrap`);
12
+ return value;
13
+ }
14
+ /** Run a service call and turn domain failures into tool errors with their code and field issues. */
15
+ async function call(work) {
16
+ try {
17
+ return textResult(await work());
18
+ }
19
+ catch (error) {
20
+ if (isAgentError(error)) {
21
+ return textResult({ code: error.code, message: error.message, ...(error.issues.length ? { issues: error.issues } : {}) }, true);
22
+ }
23
+ throw error;
24
+ }
25
+ }
26
+ /**
27
+ * Agent management for operators driving Stageflow over MCP. A running agent reaches these
28
+ * only through the Stageflow tool groups it is granted (`agents.*`); the guard in
29
+ * `toolkit/guard.ts` stops it acting on its own sessions.
30
+ */
31
+ export function registerAgentTools(server, deps) {
32
+ const service = () => required(deps.agentService, "agentService");
33
+ const chat = () => required(deps.agentChat, "agentChat");
34
+ const root = () => defaultAgentProjectRoot(deps.cwd);
35
+ /** A session must belong to the agent named in the call, or it is not found (as in the HTTP routes). */
36
+ const sessionFor = async (id, sessionId) => {
37
+ const detail = await service().getSession(root(), sessionId);
38
+ if (detail.session.agentId !== id)
39
+ throw new AgentError("agent.session_not_found");
40
+ return detail;
41
+ };
42
+ server.registerTool("list_agents", { description: "List project agents (agents/*.agent.yaml) with status and session stats.", inputSchema: z.object({}) }, () => call(async () => ({ agents: await service().list(root()) })));
43
+ server.registerTool("get_agent", {
44
+ description: "Get one agent: summary, full definition, canonical YAML, and warnings.",
45
+ inputSchema: z.object({ id: agentId }),
46
+ }, ({ id }) => call(() => service().get(root(), id)));
47
+ server.registerTool("validate_agent", {
48
+ description: "Validate an agent definition without saving. Returns field issues, warnings, and canonical YAML.",
49
+ inputSchema: z.object({ definition: z.record(z.string(), z.unknown()) }),
50
+ }, ({ definition }) => call(() => service().validate(root(), definition)));
51
+ server.registerTool("create_agent", {
52
+ description: "Create an agent. It always starts disabled; enable it with enable_agent.",
53
+ inputSchema: z.object({ definition: z.record(z.string(), z.unknown()) }),
54
+ }, ({ definition }) => call(() => service().create(root(), definition, MCP_ACTOR)));
55
+ server.registerTool("save_agent", {
56
+ description: "Save a full agent definition as a new revision. baseRevision must be the current revision (optimistic lock).",
57
+ inputSchema: z.object({
58
+ id: agentId,
59
+ baseRevision: z.number().int().nonnegative(),
60
+ definition: z.record(z.string(), z.unknown()),
61
+ summary: z.string().max(200).optional(),
62
+ }),
63
+ }, ({ id, baseRevision, definition, summary }) => call(() => service().save(root(), id, { baseRevision, definition, summary: summary ?? null }, MCP_ACTOR)));
64
+ server.registerTool("enable_agent", { description: "Enable an agent for new sessions and turns.", inputSchema: z.object({ id: agentId }) }, ({ id }) => call(() => service().setEnabled(root(), id, true, MCP_ACTOR)));
65
+ server.registerTool("disable_agent", {
66
+ description: "Disable an agent. New sessions and turns are refused; a running turn finishes.",
67
+ inputSchema: z.object({ id: agentId }),
68
+ }, ({ id }) => call(() => service().setEnabled(root(), id, false, MCP_ACTOR)));
69
+ server.registerTool("list_agent_revisions", { description: "List an agent's revisions, newest first.", inputSchema: z.object({ id: agentId }) }, ({ id }) => call(async () => ({ revisions: await service().listRevisions(root(), id) })));
70
+ server.registerTool("restore_agent_revision", {
71
+ description: "Save an earlier revision's definition as the newest revision.",
72
+ inputSchema: z.object({
73
+ id: agentId,
74
+ revision: z.number().int().positive(),
75
+ baseRevision: z.number().int().nonnegative(),
76
+ }),
77
+ }, ({ id, revision, baseRevision }) => call(() => service().restore(root(), id, { revision, baseRevision }, MCP_ACTOR)));
78
+ server.registerTool("agent_chat", {
79
+ description: "Send one message to an agent and wait for its reply. Omit sessionId to start a new session.",
80
+ inputSchema: z.object({ id: agentId, message: z.string().min(1), sessionId: z.string().optional() }),
81
+ }, ({ id, message, sessionId }) => call(() => chat().send({
82
+ projectRoot: root(),
83
+ agentId: id,
84
+ text: message,
85
+ source: "mcp",
86
+ ...(sessionId ? { sessionId } : {}),
87
+ })));
88
+ server.registerTool("get_agent_session", {
89
+ description: "Fetch a session of an agent: transcript, turns, approvals.",
90
+ inputSchema: z.object({ id: agentId, sessionId: z.string() }),
91
+ }, ({ id, sessionId }) => call(() => sessionFor(id, sessionId)));
92
+ server.registerTool("list_agent_sessions", {
93
+ description: "List an agent's sessions, newest first, with turn counts, tokens and cost.",
94
+ inputSchema: z.object({
95
+ id: agentId,
96
+ status: z.enum(SESSION_STATUSES).optional(),
97
+ search: z.string().optional(),
98
+ limit: z.number().int().min(1).max(100).optional(),
99
+ cursor: z.string().optional(),
100
+ }),
101
+ }, ({ id, status, search, limit, cursor }) => call(() => service().listSessions(root(), id, {
102
+ limit: limit ?? 25,
103
+ ...(status ? { status } : {}),
104
+ ...(search ? { search } : {}),
105
+ ...(cursor ? { cursor } : {}),
106
+ })));
107
+ server.registerTool("stop_agent_session", {
108
+ description: "Stop the turn currently running in a session. The session stays open.",
109
+ inputSchema: z.object({ id: agentId, sessionId: z.string() }),
110
+ }, ({ id, sessionId }) => call(async () => {
111
+ await sessionFor(id, sessionId);
112
+ return { stopped: chat().stop(sessionId) };
113
+ }));
114
+ server.registerTool("archive_agent", {
115
+ description: "Archive an agent: its definition leaves the project and new sessions are refused. Past sessions are kept.",
116
+ inputSchema: z.object({ id: agentId }),
117
+ }, ({ id }) => call(async () => { await service().archive(root(), id); return { archived: true }; }));
118
+ server.registerTool("approve_agent_action", {
119
+ description: "Approve the pending ask-first action on a session of an agent, as an operator.",
120
+ inputSchema: z.object({ id: agentId, sessionId: z.string() }),
121
+ }, ({ id, sessionId }) => call(async () => {
122
+ await sessionFor(id, sessionId);
123
+ return service().decideApproval(root(), sessionId, "approve", { kind: "operator" });
124
+ }));
125
+ server.registerTool("deny_agent_action", {
126
+ description: "Deny the pending ask-first action on a session of an agent. The agent is told it was refused.",
127
+ inputSchema: z.object({ id: agentId, sessionId: z.string() }),
128
+ }, ({ id, sessionId }) => call(async () => {
129
+ await sessionFor(id, sessionId);
130
+ return service().decideApproval(root(), sessionId, "deny", { kind: "operator" });
131
+ }));
132
+ }
@@ -1,6 +1,7 @@
1
+ import { buildModelsResponse } from "../models/response.js";
1
2
  import { z } from "zod";
2
3
  import { catalogPathErrorBody, CatalogPathError, resolveCatalogRelativePath, resolveCatalogStartInput, selectCatalogRootForStart, } from "../config/catalogRelativePath.js";
3
- import { listModelsMultiProject, listPipelinesMultiProject, listTasksMultiProject, } from "../config/multiProjectCatalog.js";
4
+ import { listPipelinesMultiProject, listTasksMultiProject, } from "../config/multiProjectCatalog.js";
4
5
  import { resolveCatalogRoots } from "../config/resolveCatalogRoots.js";
5
6
  import { describePipeline } from "../config/describePipeline.js";
6
7
  import { loadPipeline } from "../config/loadPipeline.js";
@@ -41,7 +42,17 @@ const taskFileSchema = z.object({
41
42
  goal: z.string(),
42
43
  context: z.string().optional(),
43
44
  constraints: z.string().optional(),
44
- checkout: z.string().optional(),
45
+ checkout: z
46
+ .union([
47
+ z.string(),
48
+ z
49
+ .object({
50
+ branch: z.string().optional(),
51
+ base: z.string().optional(),
52
+ })
53
+ .strict(),
54
+ ])
55
+ .optional(),
45
56
  repository: z.string().optional(),
46
57
  ref: z.string().optional(),
47
58
  run_branch_template: z.string().optional(),
@@ -233,14 +244,14 @@ export function registerCatalogTools(server, deps) {
233
244
  });
234
245
  });
235
246
  server.registerTool("list_models", {
236
- description: "List catalog model ids across every project this host knows about (same multi-root source as GET /api/models). Optional project_root filter narrows.",
247
+ description: "List model ids from the providers connected on this host, plus ids pipelines use (same source as GET /api/models). Returns models, providers (grouped), the default model and unlisted ids. Optional project_root filter narrows the pipeline-used ids.",
237
248
  inputSchema: z.object({
238
249
  project_root: z.string().optional(),
239
250
  }),
240
251
  }, async ({ project_root }) => {
241
- const result = await listModelsMultiProject({
252
+ const { response, used: result } = await buildModelsResponse({
242
253
  store,
243
- bootCwd: cwd,
254
+ cwd,
244
255
  projectRootFilter: project_root,
245
256
  });
246
257
  if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
@@ -251,11 +262,7 @@ export function registerCatalogTools(server, deps) {
251
262
  root_errors: result.root_errors,
252
263
  }, true);
253
264
  }
254
- return textResult({
255
- models: result.models,
256
- entries: result.items,
257
- root_errors: result.root_errors,
258
- });
265
+ return textResult(response);
259
266
  });
260
267
  server.registerTool("list_runs", {
261
268
  description: "List known pipeline runs. Optional filters: status, since (ISO created_at lower bound), pipeline (id or path), caller_id.",
@@ -1,5 +1,8 @@
1
1
  import type { BootProviderResult } from "../agent/bootProviderConfig.js";
2
2
  import type { ProviderAuthContext } from "../agent/providerAuth.js";
3
+ import type { AgentChatController } from "../agents/chatTurn.js";
4
+ import type { ConversationBackend } from "../agents/sessions.js";
5
+ import type { AgentService } from "../agents/service.js";
3
6
  import type { RunStore } from "../runstore/port.js";
4
7
  import type { RunChangeBus } from "../runtime/runChangeBus.js";
5
8
  import type { RunManager } from "../runtime/runManager.js";
@@ -11,6 +14,9 @@ export type McpToolDeps = {
11
14
  providerAuthContext?: ProviderAuthContext;
12
15
  projectRoot?: string;
13
16
  providerBoot?: BootProviderResult;
17
+ agentService?: AgentService;
18
+ agentChat?: AgentChatController;
19
+ agentConversationBackend?: ConversationBackend;
14
20
  };
15
21
  export type McpHttpDeps = McpToolDeps & {
16
22
  runChangeBus: RunChangeBus;
@@ -0,0 +1,19 @@
1
+ import { loadDraftPackage, validateDraftPackage, type DraftPackage } from "../config/draftPackage.js";
2
+ export { loadDraftPackage, validateDraftPackage, type DraftPackage };
3
+ export type PublishFacadeResult = {
4
+ ok: true;
5
+ pipelinePath: string;
6
+ stagePaths: string[];
7
+ taskPath?: string;
8
+ } | {
9
+ ok: false;
10
+ error: string;
11
+ status: number;
12
+ };
13
+ /** Write a package the way the Workshop's save does, flattening its result shape. */
14
+ export declare function publishDraftPackageFacade(projectRoot: string, input: {
15
+ directory: string;
16
+ draft: DraftPackage;
17
+ pipelineFilename: string;
18
+ allowInvalid?: boolean;
19
+ }, mode: "create" | "overwrite"): Promise<PublishFacadeResult>;