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
@@ -1,5 +1,9 @@
1
1
  import { probeProjectMcpServer } from "../agent/piIsolatedMcpProbe.js";
2
- import { listProjectMcpCatalog } from "../config/resolveStageMcpServers.js";
2
+ import { assertServerName, deleteMcpSecret, isMcpHostStateError, mcpEnvAllowNames, listMcpSecretNames, parsePastedMcpJson, readHostMcpServers, removeHostMcpServer, requiredVars, saveHostMcpServer, setMcpSecret, } from "../config/mcpHostCatalog.js";
3
+ import { maskEntry } from "../config/mcpRedaction.js";
4
+ import { listCachedMcpTools } from "../config/mcpToolCache.js";
5
+ import { completeMcpOAuth, MCP_OAUTH_HTTP_STATUS, mcpOAuthStatus, McpOAuthError, removeMcpOAuth, startMcpOAuth, } from "../config/mcpOAuth.js";
6
+ import { listProjectMcpCatalog, loadMcpCatalog, StageMcpError, } from "../config/resolveStageMcpServers.js";
3
7
  function requestAbortSignal(req) {
4
8
  const controller = new AbortController();
5
9
  const abort = () => {
@@ -14,8 +18,59 @@ function requestAbortSignal(req) {
14
18
  req.once("close", abort);
15
19
  return controller.signal;
16
20
  }
21
+ /** decodeURIComponent that reports malformed percent-encoding instead of throwing. */
22
+ function decodeSegment(raw) {
23
+ try {
24
+ return decodeURIComponent(raw ?? "");
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
17
30
  export async function handleProjectMcpRoutes(req, res, ctx) {
18
- const { projectRoot, json } = ctx;
31
+ try {
32
+ return await handleRoutes(req, res, ctx);
33
+ }
34
+ catch (err) {
35
+ // A corrupt host-state file is never read as empty and never overwritten.
36
+ if (isMcpHostStateError(err)) {
37
+ ctx.json(res, 409, { error: err.message, code: err.code });
38
+ return true;
39
+ }
40
+ throw err;
41
+ }
42
+ }
43
+ async function handleRoutes(req, res, ctx) {
44
+ const { projectRoot, json, readJsonBody } = ctx;
45
+ const hostFailure = (err) => {
46
+ const code = err?.code;
47
+ if (code === "invalid_server" || code === "invalid_secret" || code === "reserved_name") {
48
+ json(res, 400, { error: err.message, code });
49
+ return true;
50
+ }
51
+ return false;
52
+ };
53
+ const bodyObject = async () => {
54
+ try {
55
+ const body = await readJsonBody(req);
56
+ if (body !== null && typeof body === "object" && !Array.isArray(body)) {
57
+ return body;
58
+ }
59
+ }
60
+ catch {
61
+ /* fall through */
62
+ }
63
+ json(res, 400, { error: "Body must be a JSON object", code: "invalid_body" });
64
+ return undefined;
65
+ };
66
+ /** Decoded `:name` segment, or a 400 has been sent and undefined is returned. */
67
+ const nameParam = (raw) => {
68
+ const name = decodeSegment(raw);
69
+ if (name === undefined) {
70
+ json(res, 400, { error: "Malformed percent-encoding in the name", code: "invalid_name" });
71
+ }
72
+ return name;
73
+ };
19
74
  const method = req.method ?? "GET";
20
75
  const rawUrl = req.url ?? "/";
21
76
  const pathname = new URL(rawUrl, "http://localhost").pathname;
@@ -23,9 +78,192 @@ export async function handleProjectMcpRoutes(req, res, ctx) {
23
78
  json(res, 200, await listProjectMcpCatalog(projectRoot));
24
79
  return true;
25
80
  }
81
+ if (method === "POST" && pathname === "/api/project-mcp/import") {
82
+ const body = await bodyObject();
83
+ if (!body)
84
+ return true;
85
+ try {
86
+ const entries = parsePastedMcpJson(body.json ?? body);
87
+ const added = [];
88
+ const secretsStored = [];
89
+ const errors = [];
90
+ for (const [name, entry] of Object.entries(entries)) {
91
+ try {
92
+ const saved = saveHostMcpServer(name, entry);
93
+ added.push(name);
94
+ secretsStored.push(...saved.secretsStored);
95
+ }
96
+ catch (err) {
97
+ if (err.code === undefined)
98
+ throw err;
99
+ errors.push({ name, error: err.message });
100
+ }
101
+ }
102
+ json(res, 200, { added, secretsStored, errors });
103
+ }
104
+ catch (err) {
105
+ if (!hostFailure(err))
106
+ throw err;
107
+ }
108
+ return true;
109
+ }
110
+ if (pathname === "/api/mcp-secrets" && method === "GET") {
111
+ const servers = (await loadMcpCatalog(projectRoot, { scope: { kind: "validate" } }).catch((err) => {
112
+ if (err instanceof StageMcpError && err.code === "host_state_corrupt")
113
+ throw err;
114
+ return undefined;
115
+ }))?.servers ?? {};
116
+ const usedBy = new Map();
117
+ for (const [server, entry] of Object.entries(servers)) {
118
+ for (const v of requiredVars(entry))
119
+ usedBy.set(v, [...(usedBy.get(v) ?? []), server]);
120
+ }
121
+ const stored = new Set(listMcpSecretNames());
122
+ const allowed = new Set(mcpEnvAllowNames());
123
+ json(res, 200, {
124
+ secrets: [...stored].map((name) => ({ name, usedBy: usedBy.get(name) ?? [] })),
125
+ // Placeholders servers reference; `set` = a stored console secret, or an allow-listed
126
+ // (STAGEFLOW_MCP_ENV_ALLOW) Host env var that is present. Other Host env is never consulted.
127
+ needed: [...usedBy].map(([name, by]) => ({
128
+ name,
129
+ usedBy: by,
130
+ set: stored.has(name) || (allowed.has(name) && (process.env[name] ?? "") !== ""),
131
+ })),
132
+ });
133
+ return true;
134
+ }
135
+ const secretMatch = pathname.match(/^\/api\/mcp-secrets\/([^/]+)$/);
136
+ if (secretMatch && (method === "PUT" || method === "DELETE")) {
137
+ const name = nameParam(secretMatch[1]);
138
+ if (name === undefined)
139
+ return true;
140
+ if (method === "DELETE") {
141
+ if (!deleteMcpSecret(name))
142
+ json(res, 404, { error: "Secret not found", code: "not_found" });
143
+ else
144
+ json(res, 200, { name });
145
+ return true;
146
+ }
147
+ const body = await bodyObject();
148
+ if (!body)
149
+ return true;
150
+ if (typeof body.value !== "string") {
151
+ json(res, 400, { error: "value must be a string", code: "invalid_secret" });
152
+ return true;
153
+ }
154
+ try {
155
+ setMcpSecret(name, body.value);
156
+ json(res, 200, { name });
157
+ }
158
+ catch (err) {
159
+ if (!hostFailure(err))
160
+ throw err;
161
+ }
162
+ return true;
163
+ }
164
+ const toolsMatch = pathname.match(/^\/api\/project-mcp\/([^/]+)\/tools$/);
165
+ if (toolsMatch && method === "GET") {
166
+ const name = nameParam(toolsMatch[1]);
167
+ if (name === undefined)
168
+ return true;
169
+ const cached = listCachedMcpTools(name);
170
+ json(res, 200, cached ? { name, tools: cached.tools, cachedAt: cached.cachedAt } : { name, tools: null });
171
+ return true;
172
+ }
173
+ const oauthMatch = pathname.match(/^\/api\/project-mcp\/([^/]+)\/oauth(\/start|\/complete)?$/);
174
+ if (oauthMatch) {
175
+ const name = nameParam(oauthMatch[1]);
176
+ if (name === undefined)
177
+ return true;
178
+ const action = oauthMatch[2];
179
+ try {
180
+ assertServerName(name);
181
+ if (method === "GET" && !action) {
182
+ json(res, 200, { name, status: await mcpOAuthStatus(name, projectRoot) });
183
+ return true;
184
+ }
185
+ if (method === "DELETE" && !action) {
186
+ await removeMcpOAuth(name, projectRoot);
187
+ json(res, 200, { name, status: "not_authenticated" });
188
+ return true;
189
+ }
190
+ if (method === "POST" && action === "/start") {
191
+ json(res, 200, { name, ...(await startMcpOAuth(projectRoot, name)) });
192
+ return true;
193
+ }
194
+ if (method === "POST" && action === "/complete") {
195
+ const body = await bodyObject();
196
+ if (!body)
197
+ return true;
198
+ if (typeof body.input !== "string" || body.input.trim() === "") {
199
+ json(res, 400, { error: "Paste the full address from the browser", code: "invalid_body" });
200
+ return true;
201
+ }
202
+ json(res, 200, { name, status: await completeMcpOAuth(name, body.input, projectRoot) });
203
+ return true;
204
+ }
205
+ }
206
+ catch (err) {
207
+ if (err instanceof McpOAuthError) {
208
+ json(res, MCP_OAUTH_HTTP_STATUS[err.code] ?? 500, { error: err.message, code: err.code });
209
+ return true;
210
+ }
211
+ if (hostFailure(err))
212
+ return true;
213
+ if (err instanceof StageMcpError) {
214
+ json(res, 400, { error: err.message, code: err.code });
215
+ return true;
216
+ }
217
+ throw err;
218
+ }
219
+ }
220
+ const serverMatch = pathname.match(/^\/api\/project-mcp\/([^/]+)$/);
221
+ if (serverMatch && method === "GET") {
222
+ const name = nameParam(serverMatch[1]);
223
+ if (name === undefined)
224
+ return true;
225
+ const config = readHostMcpServers()[name];
226
+ if (config)
227
+ json(res, 200, { name, origin: "console", config: maskEntry(config) });
228
+ else
229
+ json(res, 404, { error: "Only servers saved from the console can be read back", code: "not_found" });
230
+ return true;
231
+ }
232
+ if (serverMatch && method === "PUT") {
233
+ const name = nameParam(serverMatch[1]);
234
+ if (name === undefined)
235
+ return true;
236
+ const body = await bodyObject();
237
+ if (!body)
238
+ return true;
239
+ try {
240
+ json(res, 200, saveHostMcpServer(name, body.config));
241
+ }
242
+ catch (err) {
243
+ if (!hostFailure(err))
244
+ throw err;
245
+ }
246
+ return true;
247
+ }
248
+ if (serverMatch && method === "DELETE") {
249
+ const name = nameParam(serverMatch[1]);
250
+ if (name === undefined)
251
+ return true;
252
+ if (removeHostMcpServer(name)) {
253
+ json(res, 200, { name });
254
+ return true;
255
+ }
256
+ const inProject = (await listProjectMcpCatalog(projectRoot)).servers.some((e) => e.name === name);
257
+ json(res, inProject ? 409 : 404, inProject
258
+ ? { error: "Defined in the project .mcp.json; edit that file to remove it", code: "project_defined" }
259
+ : { error: "Server not found", code: "not_found" });
260
+ return true;
261
+ }
26
262
  const probeMatch = pathname.match(/^\/api\/project-mcp\/([^/]+)\/probe$/);
27
263
  if (method === "POST" && probeMatch) {
28
- const name = decodeURIComponent(probeMatch[1] ?? "");
264
+ const name = nameParam(probeMatch[1]);
265
+ if (name === undefined)
266
+ return true;
29
267
  const result = await probeProjectMcpServer({
30
268
  projectRoot,
31
269
  name,
@@ -0,0 +1,27 @@
1
+ import type { AgentDefinition } from "../config/loadAgent.js";
2
+ import type { AgentService } from "../agents/service.js";
3
+ /** The running agent on whose behalf a toolkit tool is called. Absent for operators (MCP, CLI, console). */
4
+ export type ToolCaller = {
5
+ agentId: string;
6
+ definition: AgentDefinition;
7
+ };
8
+ /**
9
+ * Everything `candidate` would be able to do that `caller` cannot: tool grants
10
+ * (builtin, MCP, Stageflow, sub-agents), skills and the browser. An agent may
11
+ * only hand out what it holds itself.
12
+ */
13
+ export declare function grantsBeyond(caller: AgentDefinition, candidate: AgentDefinition): string[];
14
+ /**
15
+ * Tools the caller must ask a person about that the candidate could run without asking.
16
+ * Handing out a tool is not enough to check: the copy must not drop the approval too.
17
+ */
18
+ export declare function approvalsWeakened(caller: AgentDefinition, candidate: AgentDefinition): string[];
19
+ /**
20
+ * Checks that only apply when an agent (not an operator) calls an agent-management tool:
21
+ * no editing itself, no granting what it does not hold, no deciding its own approvals.
22
+ * Returns a refusal message, or null when the call may proceed.
23
+ */
24
+ export declare function checkAgentCaller(tool: string, args: Record<string, unknown>, caller: ToolCaller, service: AgentService | undefined, projectRoot: string): Promise<{
25
+ ok: false;
26
+ message: string;
27
+ } | null>;
@@ -0,0 +1,98 @@
1
+ import { grantedToolIds, parseAgentDefinition } from "../config/loadAgent.js";
2
+ /**
3
+ * Everything `candidate` would be able to do that `caller` cannot: tool grants
4
+ * (builtin, MCP, Stageflow, sub-agents), skills and the browser. An agent may
5
+ * only hand out what it holds itself.
6
+ */
7
+ export function grantsBeyond(caller, candidate) {
8
+ const held = grantedToolIds(caller);
9
+ const beyond = [];
10
+ for (const id of grantedToolIds(candidate)) {
11
+ if (held.has(id))
12
+ continue;
13
+ const [server, tool] = [id.slice(0, id.indexOf(".")), id.slice(id.indexOf(".") + 1)];
14
+ // A wildcard on the caller's MCP server covers every tool of that server.
15
+ if (id.includes(".") && caller.tools.mcp[server]?.includes("*") && candidate.tools.mcp[server]?.includes(tool))
16
+ continue;
17
+ beyond.push(id);
18
+ }
19
+ for (const server of Object.keys(candidate.tools.mcp)) {
20
+ if (candidate.tools.mcp[server]?.includes("*") && !caller.tools.mcp[server]?.includes("*"))
21
+ beyond.push(`${server}.*`);
22
+ }
23
+ for (const skill of candidate.tools.skills)
24
+ if (!caller.tools.skills.includes(skill))
25
+ beyond.push(`skill ${skill}`);
26
+ if (candidate.tools.browser && !caller.tools.browser)
27
+ beyond.push("browser");
28
+ return [...new Set(beyond)].sort();
29
+ }
30
+ /**
31
+ * Tools the caller must ask a person about that the candidate could run without asking.
32
+ * Handing out a tool is not enough to check: the copy must not drop the approval too.
33
+ */
34
+ export function approvalsWeakened(caller, candidate) {
35
+ const held = grantedToolIds(candidate);
36
+ const holds = (id) => {
37
+ if (held.has(id))
38
+ return true;
39
+ const dot = id.indexOf(".");
40
+ return dot > 0 && candidate.tools.mcp[id.slice(0, dot)]?.includes("*") === true;
41
+ };
42
+ return caller.approvals.ask_first.filter((id) => holds(id) && !candidate.approvals.ask_first.includes(id)).sort();
43
+ }
44
+ const refusal = (message) => ({ ok: false, message });
45
+ /**
46
+ * Checks that only apply when an agent (not an operator) calls an agent-management tool:
47
+ * no editing itself, no granting what it does not hold, no deciding its own approvals.
48
+ * Returns a refusal message, or null when the call may proceed.
49
+ */
50
+ export async function checkAgentCaller(tool, args, caller, service, projectRoot) {
51
+ if (!service)
52
+ return null;
53
+ const self = caller.agentId;
54
+ const checkDefinition = (candidate, what) => {
55
+ const beyond = grantsBeyond(caller.definition, candidate);
56
+ if (beyond.length > 0)
57
+ return refusal(`${what} would grant tools this agent does not hold: ${beyond.join(", ")}.`);
58
+ const weakened = approvalsWeakened(caller.definition, candidate);
59
+ return weakened.length > 0 ? refusal(`${what} would let tools run without asking first that this agent must ask about: ${weakened.join(", ")}.`) : null;
60
+ };
61
+ switch (tool) {
62
+ case "create_agent": {
63
+ const parsed = parseAgentDefinition(args.definition);
64
+ return parsed.ok ? checkDefinition(parsed.value, "That agent") : null;
65
+ }
66
+ case "save_agent": {
67
+ if (args.id === self)
68
+ return refusal("An agent cannot edit its own definition.");
69
+ const parsed = parseAgentDefinition(args.definition);
70
+ return parsed.ok ? checkDefinition(parsed.value, "That change") : null;
71
+ }
72
+ case "restore_agent_revision": {
73
+ if (args.id === self)
74
+ return refusal("An agent cannot edit its own definition.");
75
+ if (typeof args.id !== "string" || typeof args.revision !== "number")
76
+ return null;
77
+ const definition = await service.definitionAt(projectRoot, args.id, args.revision).catch(() => null);
78
+ return definition ? checkDefinition(definition, "That revision") : null;
79
+ }
80
+ case "archive_agent":
81
+ return args.id === self ? refusal("An agent cannot archive itself.") : null;
82
+ case "enable_agent": {
83
+ if (typeof args.id !== "string")
84
+ return null;
85
+ const detail = await service.get(projectRoot, args.id).catch(() => null);
86
+ return detail?.definition ? checkDefinition(detail.definition, "Enabling that agent") : null;
87
+ }
88
+ case "approve_agent_action":
89
+ case "deny_agent_action": {
90
+ if (typeof args.sessionId !== "string")
91
+ return null;
92
+ const session = await service.getSession(projectRoot, args.sessionId).catch(() => null);
93
+ return session?.session.agentId === self ? refusal("An agent cannot decide approvals on its own sessions.") : null;
94
+ }
95
+ default:
96
+ return null;
97
+ }
98
+ }
@@ -0,0 +1,48 @@
1
+ import type { McpToolDeps } from "../mcp/deps.js";
2
+ import { type ToolCaller } from "./guard.js";
3
+ import { type ToolMeta } from "./meta.js";
4
+ export * from "./meta.js";
5
+ export { grantsBeyond, type ToolCaller } from "./guard.js";
6
+ export type ToolkitResult = {
7
+ ok: boolean;
8
+ text: string;
9
+ };
10
+ export type ToolkitTool = ToolMeta & {
11
+ name: string;
12
+ /** The id the policy gate and `approvals.ask_first` use. */
13
+ grantId: string;
14
+ description: string;
15
+ /** JSON Schema of the arguments. */
16
+ parameters: Record<string, unknown>;
17
+ run(args: Record<string, unknown>, ctx: {
18
+ signal: AbortSignal;
19
+ caller?: ToolCaller;
20
+ }): Promise<ToolkitResult>;
21
+ };
22
+ /** Drop mentions of HTTP routes: a model cannot call them, and the text only confuses it. */
23
+ export declare function plainDescription(text: string): string;
24
+ /**
25
+ * Every Stageflow tool, derived from the MCP registrations. A tool with no
26
+ * metadata (or metadata with no tool) is a programming error, caught here
27
+ * and by the coverage test.
28
+ */
29
+ export declare function buildToolkit(deps: McpToolDeps): ToolkitTool[];
30
+ export declare function toolkitNames(toolkit: readonly ToolkitTool[]): string[];
31
+ export type ToolkitCatalog = {
32
+ groups: Array<{
33
+ id: string;
34
+ title: string;
35
+ summary: string;
36
+ /** True when the whole group is for operators only; the console lists it as locked. */
37
+ operatorOnly: boolean;
38
+ tools: Array<{
39
+ name: string;
40
+ grantId: string;
41
+ risk: ToolMeta["risk"];
42
+ description: string;
43
+ operatorOnly: boolean;
44
+ }>;
45
+ }>;
46
+ };
47
+ /** The groups and tools the console offers for granting. Handlers are never run, so no Host services are needed. */
48
+ export declare function describeToolkit(): ToolkitCatalog;
@@ -0,0 +1,82 @@
1
+ import { z } from "zod";
2
+ import { defaultAgentProjectRoot } from "../agents/projectRoot.js";
3
+ import { checkAgentCaller } from "./guard.js";
4
+ import { isOperatorOnlyTool, stageflowGrantId, TOOL_GROUPS, TOOL_META } from "./meta.js";
5
+ import { recordMcpTools } from "./record.js";
6
+ export * from "./meta.js";
7
+ export { grantsBeyond } from "./guard.js";
8
+ function toResult(raw) {
9
+ const result = (raw ?? {});
10
+ const text = (result.content ?? []).map((part) => (part.type === "text" ? (part.text ?? "") : `[${part.type}]`)).join("\n");
11
+ return { ok: result.isError !== true, text };
12
+ }
13
+ /** Drop mentions of HTTP routes: a model cannot call them, and the text only confuses it. */
14
+ export function plainDescription(text) {
15
+ const withoutParens = text.replace(/\s*\([^()]*\/api\/[^()]*\)/g, "");
16
+ const sentences = withoutParens.split(/(?<=\.)\s+/).filter((sentence) => !sentence.includes("/api/"));
17
+ return sentences.join(" ").replace(/\s+([.,:])/g, "$1").trim();
18
+ }
19
+ function jsonSchema(schema) {
20
+ if (!schema)
21
+ return { type: "object", properties: {} };
22
+ const { $schema: _ignored, ...rest } = z.toJSONSchema(schema, { io: "input", unrepresentable: "any" });
23
+ return rest;
24
+ }
25
+ /**
26
+ * Every Stageflow tool, derived from the MCP registrations. A tool with no
27
+ * metadata (or metadata with no tool) is a programming error, caught here
28
+ * and by the coverage test.
29
+ */
30
+ export function buildToolkit(deps) {
31
+ const recorded = recordMcpTools(deps);
32
+ const missing = recorded.filter((tool) => !(tool.name in TOOL_META)).map((tool) => tool.name);
33
+ if (missing.length > 0)
34
+ throw new Error(`Toolkit metadata missing for: ${missing.join(", ")}`);
35
+ const root = () => defaultAgentProjectRoot(deps.cwd);
36
+ return recorded.map((tool) => {
37
+ const meta = TOOL_META[tool.name];
38
+ return {
39
+ ...meta,
40
+ name: tool.name,
41
+ grantId: stageflowGrantId(tool.name),
42
+ description: plainDescription(tool.description),
43
+ parameters: jsonSchema(tool.inputSchema),
44
+ async run(args, ctx) {
45
+ const parsed = tool.inputSchema ? tool.inputSchema.safeParse(args) : { success: true, data: args };
46
+ if (!parsed.success)
47
+ return { ok: false, text: JSON.stringify({ error: "Invalid arguments", issues: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`) }) };
48
+ if (ctx.caller) {
49
+ const refused = await checkAgentCaller(tool.name, parsed.data, ctx.caller, deps.agentService, root());
50
+ if (refused)
51
+ return { ok: false, text: JSON.stringify({ error: refused.message }) };
52
+ }
53
+ const mcpCtx = { mcpReq: { signal: ctx.signal, _meta: undefined, notify: async () => undefined } };
54
+ const raw = tool.inputSchema ? await tool.handler(parsed.data, mcpCtx) : await tool.handler(mcpCtx);
55
+ return toResult(raw);
56
+ },
57
+ };
58
+ });
59
+ }
60
+ export function toolkitNames(toolkit) {
61
+ return toolkit.map((tool) => tool.name);
62
+ }
63
+ let catalogCache;
64
+ /** The groups and tools the console offers for granting. Handlers are never run, so no Host services are needed. */
65
+ export function describeToolkit() {
66
+ if (catalogCache)
67
+ return catalogCache;
68
+ const described = recordMcpTools({ cwd: process.cwd(), store: {}, manager: {} });
69
+ const byName = new Map(described.map((tool) => [tool.name, tool.description]));
70
+ catalogCache = {
71
+ groups: TOOL_GROUPS.map((group) => ({
72
+ id: group.id,
73
+ title: group.title,
74
+ summary: group.summary,
75
+ operatorOnly: group.operatorOnly === true,
76
+ tools: Object.entries(TOOL_META)
77
+ .filter(([, meta]) => meta.group === group.id)
78
+ .map(([name, meta]) => ({ name, grantId: stageflowGrantId(name), risk: meta.risk, description: plainDescription(byName.get(name) ?? ""), operatorOnly: isOperatorOnlyTool(name) })),
79
+ })),
80
+ };
81
+ return catalogCache;
82
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Pure description of Stageflow's own tools: which group each belongs to and
3
+ * how risky it is. No handlers live here, so config validation and the console
4
+ * can import it without pulling in the runtime.
5
+ */
6
+ export type ToolRisk = "read" | "write" | "destructive" | "external";
7
+ export type ToolGroupId = "discover" | "runs.read" | "runs.control" | "runs.cleanup" | "gates.read" | "gates.decide" | "triggers" | "agents.read" | "agents.manage" | "agents.cleanup" | "agents.converse" | "pipelines.author";
8
+ export type ToolGroup = {
9
+ id: ToolGroupId;
10
+ title: string;
11
+ summary: string;
12
+ /** Never offered to agents: these decide things a person must decide (approvals, gates). */
13
+ operatorOnly?: true;
14
+ };
15
+ export declare const TOOL_GROUPS: readonly ToolGroup[];
16
+ export type ToolMeta = {
17
+ group: ToolGroupId;
18
+ risk: ToolRisk;
19
+ /** Not offered to agents even though its group is. */
20
+ operatorOnly?: true;
21
+ };
22
+ export declare const TOOL_META: Readonly<Record<string, ToolMeta>>;
23
+ export type StageflowGrants = {
24
+ groups: readonly string[];
25
+ tools: readonly string[];
26
+ };
27
+ /** The grant id the policy gate and `approvals.ask_first` use for a Stageflow tool. */
28
+ export declare const STAGEFLOW_GRANT_PREFIX = "stageflow.";
29
+ export declare const stageflowGrantId: (tool: string) => string;
30
+ export declare function isToolGroupId(value: string): value is ToolGroupId;
31
+ export declare function isOperatorOnlyGroup(group: string): boolean;
32
+ export declare function isOperatorOnlyTool(name: string): boolean;
33
+ /** Tools of a group an agent may hold; operator-only tools are left out. */
34
+ export declare function toolsInGroup(group: string): string[];
35
+ /** Every Stageflow tool name a definition grants, whole groups and single tools together. */
36
+ export declare function grantedStageflowTools(grants: StageflowGrants): string[];