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
@@ -6,6 +6,16 @@ import { yamlParsePosition } from "./yamlParsePosition.js";
6
6
  function isPlainObject(value) {
7
7
  return typeof value === "object" && value !== null && !Array.isArray(value);
8
8
  }
9
+ function parseCheckout(value) {
10
+ if (typeof value === "string")
11
+ return value;
12
+ if (isPlainObject(value)) {
13
+ const branch = typeof value.branch === "string" ? value.branch : undefined;
14
+ const base = typeof value.base === "string" ? value.base : undefined;
15
+ return { ...(branch !== undefined ? { branch } : {}), ...(base !== undefined ? { base } : {}) };
16
+ }
17
+ return undefined;
18
+ }
9
19
  function parseGitIdentity(raw) {
10
20
  if (!isPlainObject(raw))
11
21
  return undefined;
@@ -30,7 +40,7 @@ export function coerceTaskFile(raw) {
30
40
  goal: record.goal,
31
41
  context: typeof record.context === "string" ? record.context : undefined,
32
42
  constraints: typeof record.constraints === "string" ? record.constraints : undefined,
33
- checkout: typeof record.checkout === "string" ? record.checkout : undefined,
43
+ checkout: parseCheckout(record.checkout),
34
44
  ...(typeof record.repository === "string" ? { repository: record.repository } : {}),
35
45
  ...(typeof record.ref === "string" ? { ref: record.ref } : {}),
36
46
  ...(typeof record.run_branch_template === "string"
@@ -73,6 +83,29 @@ export function parseTaskFile(raw, source = "task") {
73
83
  return { ...issue, taskId: task.id };
74
84
  }));
75
85
  }
86
+ if (isPlainObject(record?.checkout)) {
87
+ const { branch, base } = record.checkout;
88
+ if (branch !== undefined && typeof branch !== "string") {
89
+ return loadFailure([
90
+ {
91
+ code: "task.invalid_shape",
92
+ message: `Invalid ${source}: checkout.branch must be a string`,
93
+ category: "task",
94
+ taskId: task.id,
95
+ },
96
+ ]);
97
+ }
98
+ if (base !== undefined && typeof base !== "string") {
99
+ return loadFailure([
100
+ {
101
+ code: "task.invalid_shape",
102
+ message: `Invalid ${source}: checkout.base must be a string`,
103
+ category: "task",
104
+ taskId: task.id,
105
+ },
106
+ ]);
107
+ }
108
+ }
76
109
  return loadSuccess(task);
77
110
  }
78
111
  export function loadTaskFromYamlOutcome(yamlText, source = "task yaml") {
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Host-level MCP state kept under $STAGEFLOW_HOME so it survives image rebuilds:
3
+ * - `mcp/.mcp.json` — servers added from the console, merged over the project `.mcp.json`
4
+ * - `secrets/mcp-secrets.json` — write-only values that fill `${VAR}` in those configs
5
+ */
6
+ export type HostMcpError = Error & {
7
+ code: "invalid_server" | "invalid_secret" | "reserved_name";
8
+ };
9
+ /**
10
+ * A host-state file exists but cannot be trusted (unparseable, wrong shape, unreadable). It is
11
+ * never read as empty and never overwritten: the owner has to fix or remove the file.
12
+ */
13
+ export declare class McpHostStateError extends Error {
14
+ readonly code: "host_state_corrupt";
15
+ readonly file: string;
16
+ constructor(file: string, detail: string);
17
+ }
18
+ export declare function isMcpHostStateError(err: unknown): err is McpHostStateError;
19
+ export declare function hostMcpCatalogPath(home?: string): string;
20
+ export declare function mcpSecretsPath(home?: string): string;
21
+ /** Servers the console saved. Missing file = empty; a corrupt file throws McpHostStateError. */
22
+ export declare function readHostMcpServers(home?: string): Record<string, Record<string, unknown>>;
23
+ /** Host env names an owner explicitly lets agents and probes use to fill `${NAME}`. */
24
+ export declare const MCP_ENV_ALLOW = "STAGEFLOW_MCP_ENV_ALLOW";
25
+ export declare function mcpEnvAllowNames(env?: NodeJS.ProcessEnv): string[];
26
+ export declare function assertServerName(name: string): void;
27
+ export declare function validateServerEntry(name: string, rawEntry: unknown): Record<string, unknown>;
28
+ /** Placeholders (`${NAME}`) without a `:-default`, in the order they appear. */
29
+ export declare function requiredVars(entry: Record<string, unknown>): string[];
30
+ export type SaveServerResult = {
31
+ name: string;
32
+ /** Names of secrets created from literal tokens in the pasted config. */
33
+ secretsStored: string[];
34
+ };
35
+ /**
36
+ * Save one server. Literal tokens in env, headers, url (query/userinfo) and args are moved into the
37
+ * secrets store and replaced with `${NAME}`, so the catalog file never holds a secret.
38
+ *
39
+ * Both files are read first (a corrupt one aborts before anything is written). The secrets file is
40
+ * written before the catalog and put back if the catalog write fails.
41
+ */
42
+ export declare function saveHostMcpServer(name: string, entry: unknown, home?: string): SaveServerResult;
43
+ export declare function removeHostMcpServer(name: string, home?: string): boolean;
44
+ /** Pull `{ name: entry }` out of pasted JSON: a full `.mcp.json`, a bare map, or `{ mcpServers }`. */
45
+ export declare function parsePastedMcpJson(raw: unknown): Record<string, unknown>;
46
+ export declare function readMcpSecrets(home?: string): Record<string, string>;
47
+ export declare function setMcpSecret(name: string, value: string, home?: string): void;
48
+ export declare function deleteMcpSecret(name: string, home?: string): boolean;
49
+ /** Secret names only; values never leave the store through the API. */
50
+ export declare function listMcpSecretNames(home?: string): string[];
@@ -0,0 +1,405 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import path from "node:path";
4
+ import { STAGEFLOW_MCP_SERVER_NAME } from "../agent/claudeTools.js";
5
+ import { globalStageflowHome } from "../project/globalHome.js";
6
+ import { isForeverDeniedSecret, isProviderOrCloudName } from "../runtime/stageEnvironment.js";
7
+ import { assertSafeMcpUrl, extractSecrets, McpUrlPolicyError, } from "./mcpRedaction.js";
8
+ function hostError(code, message) {
9
+ return Object.assign(new Error(message), { code });
10
+ }
11
+ /**
12
+ * A host-state file exists but cannot be trusted (unparseable, wrong shape, unreadable). It is
13
+ * never read as empty and never overwritten: the owner has to fix or remove the file.
14
+ */
15
+ export class McpHostStateError extends Error {
16
+ code = "host_state_corrupt";
17
+ file;
18
+ constructor(file, detail) {
19
+ super(`MCP host state file "${file}" ${detail}. Fix or remove it; nothing was changed.`);
20
+ this.name = "McpHostStateError";
21
+ this.file = file;
22
+ }
23
+ }
24
+ export function isMcpHostStateError(err) {
25
+ return err instanceof McpHostStateError;
26
+ }
27
+ function isPlainObject(value) {
28
+ return typeof value === "object" && value !== null && !Array.isArray(value);
29
+ }
30
+ export function hostMcpCatalogPath(home = globalStageflowHome()) {
31
+ return path.join(home, "mcp", ".mcp.json");
32
+ }
33
+ export function mcpSecretsPath(home = globalStageflowHome()) {
34
+ return path.join(home, "secrets", "mcp-secrets.json");
35
+ }
36
+ /** Write via a uniquely named tmp file created with the final mode, then rename into place. */
37
+ function atomicWrite(file, body, mode) {
38
+ mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
39
+ const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
40
+ try {
41
+ writeFileSync(tmp, body, { mode, flag: "wx" });
42
+ renameSync(tmp, file);
43
+ }
44
+ catch (err) {
45
+ try {
46
+ unlinkSync(tmp);
47
+ }
48
+ catch {
49
+ /* tmp never created */
50
+ }
51
+ throw err;
52
+ }
53
+ }
54
+ function readJsonState(file) {
55
+ if (!existsSync(file))
56
+ return undefined;
57
+ let raw;
58
+ try {
59
+ raw = readFileSync(file, "utf8");
60
+ }
61
+ catch (err) {
62
+ throw new McpHostStateError(file, `could not be read (${err.code ?? "error"})`);
63
+ }
64
+ try {
65
+ return JSON.parse(raw);
66
+ }
67
+ catch {
68
+ throw new McpHostStateError(file, "is not valid JSON");
69
+ }
70
+ }
71
+ /** Servers the console saved. Missing file = empty; a corrupt file throws McpHostStateError. */
72
+ export function readHostMcpServers(home = globalStageflowHome()) {
73
+ const file = hostMcpCatalogPath(home);
74
+ const parsed = readJsonState(file);
75
+ if (parsed === undefined)
76
+ return {};
77
+ if (!isPlainObject(parsed) || !isPlainObject(parsed.mcpServers)) {
78
+ throw new McpHostStateError(file, "does not have an mcpServers object");
79
+ }
80
+ const out = {};
81
+ for (const [name, entry] of Object.entries(parsed.mcpServers)) {
82
+ if (name === STAGEFLOW_MCP_SERVER_NAME)
83
+ continue;
84
+ if (!isPlainObject(entry)) {
85
+ throw new McpHostStateError(file, `has a non-object entry for "${name}"`);
86
+ }
87
+ out[name] = entry;
88
+ }
89
+ return out;
90
+ }
91
+ function writeHostMcpServers(servers, home) {
92
+ atomicWrite(hostMcpCatalogPath(home), `${JSON.stringify({ mcpServers: servers }, null, 2)}\n`, 0o600);
93
+ }
94
+ /** Host env names an owner explicitly lets agents and probes use to fill `${NAME}`. */
95
+ export const MCP_ENV_ALLOW = "STAGEFLOW_MCP_ENV_ALLOW";
96
+ export function mcpEnvAllowNames(env = process.env) {
97
+ const raw = env[MCP_ENV_ALLOW];
98
+ if (raw === undefined || raw.trim() === "")
99
+ return [];
100
+ return raw
101
+ .split(",")
102
+ .map((n) => n.trim())
103
+ .filter((n) => n.length > 0 && !isForeverDeniedSecret(n));
104
+ }
105
+ const SERVER_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
106
+ const SECRET_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
107
+ const MAX_SECRET_BYTES = 16 * 1024;
108
+ const PLACEHOLDER = /\$\{([^}]*)\}/g;
109
+ export function assertServerName(name) {
110
+ if (name === STAGEFLOW_MCP_SERVER_NAME) {
111
+ throw hostError("reserved_name", `"${name}" is reserved`);
112
+ }
113
+ if (!SERVER_NAME.test(name)) {
114
+ throw hostError("invalid_server", "Server name must be 1-64 letters, digits, '.', '_' or '-'");
115
+ }
116
+ }
117
+ /**
118
+ * Other tools write remote servers as `serverUrl` / `httpUrl` and mark the transport with `type`.
119
+ * Map those onto our shape: `url`, plus `httpTransport: "sse"` only when SSE is asked for
120
+ * (streamable HTTP is the default and falls back to SSE by itself).
121
+ */
122
+ function normaliseSnippet(entry) {
123
+ const out = { ...entry };
124
+ for (const alias of ["serverUrl", "httpUrl"]) {
125
+ if (out.url === undefined && typeof out[alias] === "string")
126
+ out.url = out[alias];
127
+ delete out[alias];
128
+ }
129
+ const kind = out.type ?? out.transport;
130
+ delete out.type;
131
+ delete out.transport;
132
+ if (kind !== undefined) {
133
+ if (kind === "sse")
134
+ out.httpTransport = "sse";
135
+ else if (kind === "http" || kind === "streamable-http" || kind === "streamableHttp") {
136
+ if (out.httpTransport === undefined)
137
+ out.httpTransport = "streamable-http";
138
+ }
139
+ else if (kind !== "stdio") {
140
+ throw hostError("invalid_server", `Unsupported server type "${String(kind)}" (use stdio, http or sse)`);
141
+ }
142
+ }
143
+ return out;
144
+ }
145
+ export function validateServerEntry(name, rawEntry) {
146
+ assertServerName(name);
147
+ if (!isPlainObject(rawEntry)) {
148
+ throw hostError("invalid_server", `Server "${name}" must be an object`);
149
+ }
150
+ const entry = normaliseSnippet(rawEntry);
151
+ const hasCommand = Object.hasOwn(entry, "command");
152
+ const hasUrl = Object.hasOwn(entry, "url");
153
+ if (hasCommand === hasUrl) {
154
+ throw hostError("invalid_server", `Server "${name}" needs exactly one of "command" (stdio) or "url" (http)`);
155
+ }
156
+ const text = (key) => {
157
+ const v = entry[key];
158
+ if (v === undefined)
159
+ return undefined;
160
+ if (typeof v !== "string" || v.trim().length === 0) {
161
+ throw hostError("invalid_server", `"${key}" must be a non-empty string`);
162
+ }
163
+ return v;
164
+ };
165
+ const out = {};
166
+ if (hasCommand)
167
+ out.command = text("command");
168
+ if (hasUrl) {
169
+ const url = text("url");
170
+ if (!/^(?:https?:\/\/|\$\{)/i.test(url)) {
171
+ throw hostError("invalid_server", `"url" must start with http:// or https://`);
172
+ }
173
+ try {
174
+ assertSafeMcpUrl(url);
175
+ }
176
+ catch (err) {
177
+ if (err instanceof McpUrlPolicyError)
178
+ throw hostError("invalid_server", err.message);
179
+ throw err;
180
+ }
181
+ out.url = url;
182
+ const transport = entry.httpTransport;
183
+ if (transport !== undefined) {
184
+ if (transport !== "sse" && transport !== "streamable-http") {
185
+ throw hostError("invalid_server", `"httpTransport" must be "sse" or "streamable-http"`);
186
+ }
187
+ // Streamable HTTP is the default; only SSE needs to be spelled out.
188
+ if (transport === "sse")
189
+ out.httpTransport = "sse";
190
+ }
191
+ }
192
+ else if (entry.httpTransport !== undefined) {
193
+ throw hostError("invalid_server", `"httpTransport" only applies to url servers`);
194
+ }
195
+ if (entry.args !== undefined) {
196
+ if (!Array.isArray(entry.args) || entry.args.some((a) => typeof a !== "string")) {
197
+ throw hostError("invalid_server", `"args" must be an array of strings`);
198
+ }
199
+ out.args = entry.args;
200
+ }
201
+ for (const key of ["env", "headers"]) {
202
+ const bag = entry[key];
203
+ if (bag === undefined)
204
+ continue;
205
+ if (!isPlainObject(bag) || Object.values(bag).some((v) => typeof v !== "string")) {
206
+ throw hostError("invalid_server", `"${key}" must map names to strings`);
207
+ }
208
+ if (key === "env" && hasUrl) {
209
+ throw hostError("invalid_server", `"env" only applies to stdio servers`);
210
+ }
211
+ if (key === "headers" && hasCommand) {
212
+ throw hostError("invalid_server", `"headers" only applies to http servers`);
213
+ }
214
+ out[key] = { ...bag };
215
+ }
216
+ const cwd = text("cwd");
217
+ if (cwd !== undefined)
218
+ out.cwd = cwd;
219
+ assertPlaceholderNamesAllowed(name, out);
220
+ return out;
221
+ }
222
+ /** Every `${NAME}` / `${NAME:-x}` name in an entry, in order of appearance. */
223
+ function placeholderNames(entry) {
224
+ const found = new Set();
225
+ const scan = (value) => {
226
+ if (typeof value !== "string")
227
+ return;
228
+ for (const m of value.matchAll(PLACEHOLDER)) {
229
+ const inner = (m[1] ?? "").split(":-")[0] ?? "";
230
+ if (SECRET_NAME.test(inner))
231
+ found.add(inner);
232
+ }
233
+ };
234
+ scan(entry.command);
235
+ scan(entry.url);
236
+ scan(entry.cwd);
237
+ if (Array.isArray(entry.args))
238
+ entry.args.forEach(scan);
239
+ for (const key of ["env", "headers"]) {
240
+ const bag = entry[key];
241
+ if (isPlainObject(bag))
242
+ Object.values(bag).forEach(scan);
243
+ }
244
+ return [...found];
245
+ }
246
+ /**
247
+ * A server entry may not ask for the Host's own credentials: control tokens (forever denied) or
248
+ * provider/cloud credentials, unless the owner put the name in STAGEFLOW_MCP_ENV_ALLOW.
249
+ */
250
+ function assertPlaceholderNamesAllowed(server, entry) {
251
+ const allowed = new Set(mcpEnvAllowNames());
252
+ for (const name of placeholderNames(entry)) {
253
+ if (isForeverDeniedSecret(name)) {
254
+ throw hostError("invalid_server", `Server "${server}" uses \${${name}}, which can never be provided to an MCP server`);
255
+ }
256
+ if (isProviderOrCloudName(name) && !allowed.has(name)) {
257
+ throw hostError("invalid_server", `Server "${server}" uses \${${name}}, a provider or cloud credential name. Use a different variable name, or list it in ${MCP_ENV_ALLOW} to allow it.`);
258
+ }
259
+ }
260
+ }
261
+ function secretNameFor(key, server, value, existing) {
262
+ const serverPart = server.toUpperCase().replace(/[^A-Z0-9]/g, "_");
263
+ // `Authorization` says nothing about which service the token is for.
264
+ const base = key.toLowerCase() === "authorization"
265
+ ? `${serverPart}_TOKEN`
266
+ : key.toUpperCase().replace(/[^A-Z0-9_]/g, "_").replace(/^(\d)/, "_$1") || "SECRET";
267
+ const prefixed = `${server.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_${base}`;
268
+ for (const candidate of [base, prefixed]) {
269
+ if (isForeverDeniedSecret(candidate) || isProviderOrCloudName(candidate))
270
+ continue;
271
+ if (existing[candidate] === undefined || existing[candidate] === value)
272
+ return candidate;
273
+ }
274
+ return `${prefixed}_2`;
275
+ }
276
+ /** Placeholders (`${NAME}`) without a `:-default`, in the order they appear. */
277
+ export function requiredVars(entry) {
278
+ const found = new Set();
279
+ const scan = (value) => {
280
+ if (typeof value !== "string")
281
+ return;
282
+ for (const m of value.matchAll(PLACEHOLDER)) {
283
+ const inner = m[1] ?? "";
284
+ if (SECRET_NAME.test(inner))
285
+ found.add(inner);
286
+ }
287
+ };
288
+ scan(entry.command);
289
+ scan(entry.url);
290
+ scan(entry.cwd);
291
+ if (Array.isArray(entry.args))
292
+ entry.args.forEach(scan);
293
+ for (const key of ["env", "headers"]) {
294
+ const bag = entry[key];
295
+ if (isPlainObject(bag))
296
+ Object.values(bag).forEach(scan);
297
+ }
298
+ return [...found];
299
+ }
300
+ /**
301
+ * Save one server. Literal tokens in env, headers, url (query/userinfo) and args are moved into the
302
+ * secrets store and replaced with `${NAME}`, so the catalog file never holds a secret.
303
+ *
304
+ * Both files are read first (a corrupt one aborts before anything is written). The secrets file is
305
+ * written before the catalog and put back if the catalog write fails.
306
+ */
307
+ export function saveHostMcpServer(name, entry, home = globalStageflowHome()) {
308
+ const validated = validateServerEntry(name, entry);
309
+ const secrets = readMcpSecrets(home);
310
+ const servers = readHostMcpServers(home);
311
+ const { entry: stored, secrets: pending } = extractSecrets(validated, (key, value, taken) => secretNameFor(key, name, value, taken), secrets);
312
+ for (const secretName of Object.keys(pending))
313
+ assertUsableSecret(secretName, pending[secretName]);
314
+ servers[name] = stored;
315
+ const names = Object.keys(pending);
316
+ if (names.length === 0) {
317
+ writeHostMcpServers(servers, home);
318
+ return { name, secretsStored: [] };
319
+ }
320
+ const secretsFile = mcpSecretsPath(home);
321
+ const previous = existsSync(secretsFile) ? readFileSync(secretsFile, "utf8") : undefined;
322
+ writeMcpSecretsFile({ ...secrets, ...pending }, home);
323
+ try {
324
+ writeHostMcpServers(servers, home);
325
+ }
326
+ catch (err) {
327
+ try {
328
+ if (previous === undefined)
329
+ rmSync(secretsFile, { force: true });
330
+ else
331
+ atomicWrite(secretsFile, previous, 0o600);
332
+ }
333
+ catch {
334
+ /* best effort: the original error is the one to report */
335
+ }
336
+ throw err;
337
+ }
338
+ return { name, secretsStored: names };
339
+ }
340
+ export function removeHostMcpServer(name, home = globalStageflowHome()) {
341
+ const servers = readHostMcpServers(home);
342
+ if (!Object.hasOwn(servers, name))
343
+ return false;
344
+ delete servers[name];
345
+ writeHostMcpServers(servers, home);
346
+ return true;
347
+ }
348
+ /** Pull `{ name: entry }` out of pasted JSON: a full `.mcp.json`, a bare map, or `{ mcpServers }`. */
349
+ export function parsePastedMcpJson(raw) {
350
+ if (!isPlainObject(raw)) {
351
+ throw hostError("invalid_server", "Pasted JSON must be an object");
352
+ }
353
+ const inner = isPlainObject(raw.mcpServers) ? raw.mcpServers : raw;
354
+ if (Object.hasOwn(inner, "command") || Object.hasOwn(inner, "url")) {
355
+ throw hostError("invalid_server", 'Wrap a single server as { "mcpServers": { "name": { … } } } so it has a name');
356
+ }
357
+ if (Object.keys(inner).length === 0) {
358
+ throw hostError("invalid_server", "No servers found in the pasted JSON");
359
+ }
360
+ return inner;
361
+ }
362
+ export function readMcpSecrets(home = globalStageflowHome()) {
363
+ const file = mcpSecretsPath(home);
364
+ const parsed = readJsonState(file);
365
+ if (parsed === undefined)
366
+ return {};
367
+ if (!isPlainObject(parsed)) {
368
+ throw new McpHostStateError(file, "is not a JSON object of secret names to values");
369
+ }
370
+ const out = {};
371
+ for (const [name, value] of Object.entries(parsed)) {
372
+ if (typeof value === "string" && SECRET_NAME.test(name))
373
+ out[name] = value;
374
+ }
375
+ return out;
376
+ }
377
+ function writeMcpSecretsFile(secrets, home) {
378
+ atomicWrite(mcpSecretsPath(home), `${JSON.stringify(secrets, null, 2)}\n`, 0o600);
379
+ }
380
+ function assertUsableSecret(name, value) {
381
+ if (!SECRET_NAME.test(name) || isForeverDeniedSecret(name)) {
382
+ throw hostError("invalid_secret", `"${name}" is not a usable secret name`);
383
+ }
384
+ if (value.length === 0 || Buffer.byteLength(value) > MAX_SECRET_BYTES) {
385
+ throw hostError("invalid_secret", "Secret value must be 1 byte to 16 KB");
386
+ }
387
+ }
388
+ export function setMcpSecret(name, value, home = globalStageflowHome()) {
389
+ assertUsableSecret(name, value);
390
+ const secrets = readMcpSecrets(home);
391
+ secrets[name] = value;
392
+ writeMcpSecretsFile(secrets, home);
393
+ }
394
+ export function deleteMcpSecret(name, home = globalStageflowHome()) {
395
+ const secrets = readMcpSecrets(home);
396
+ if (!Object.hasOwn(secrets, name))
397
+ return false;
398
+ delete secrets[name];
399
+ writeMcpSecretsFile(secrets, home);
400
+ return true;
401
+ }
402
+ /** Secret names only; values never leave the store through the API. */
403
+ export function listMcpSecretNames(home = globalStageflowHome()) {
404
+ return Object.keys(readMcpSecrets(home)).sort();
405
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * OAuth sign-in for remote MCP servers, run through pi-mcp-adapter's own flow.
3
+ *
4
+ * Tokens live in the adapter's encrypted-file store under the Host's Pi agent dir
5
+ * (`$STAGEFLOW_HOME/agent/mcp-oauth-encrypted`) and are keyed by a Host-generated key, so it works
6
+ * in a container with no OS keyring. The adapter refuses a symlinked store, and stage attempts run
7
+ * with a sealed per-attempt agent dir, so signed-in servers are available to agents and Check, not
8
+ * to pipeline stages.
9
+ */
10
+ export declare const OAUTH_FILE_KEY_ENV = "PI_MCP_ADAPTER_OAUTH_FILE_KEY";
11
+ export type McpOAuthStatus = "authenticated" | "expired" | "not_authenticated";
12
+ export type McpOAuthErrorCode = "not_found" | "not_http" | "store_unavailable" | "flow_failed" | "invalid_input" | "blocked_url" | "host_state_corrupt";
13
+ /** HTTP status for each OAuth error code; the routes use this one table. */
14
+ export declare const MCP_OAUTH_HTTP_STATUS: Record<McpOAuthErrorCode, number>;
15
+ export declare class McpOAuthError extends Error {
16
+ readonly code: McpOAuthErrorCode;
17
+ constructor(message: string, code: McpOAuthErrorCode);
18
+ }
19
+ export declare function oauthKeyPath(home?: string): string;
20
+ /**
21
+ * Make the Host's file key available to the adapter, creating it on first use. The key is written
22
+ * to a private tmp file and linked into place, so a concurrent reader never sees a half-written key.
23
+ */
24
+ export declare function ensureOAuthFileKey(home?: string): void;
25
+ /** Everything the adapter needs before it can read or write OAuth tokens in this process. */
26
+ export declare function prepareMcpOAuth(): void;
27
+ export declare function resetMcpOAuthFlowCacheForTests(): void;
28
+ /** Begin sign-in; returns the link to open in a browser. */
29
+ export declare function startMcpOAuth(projectRoot: string, name: string): Promise<{
30
+ authorizationUrl: string;
31
+ }>;
32
+ /** The pasted redirect address, or a bare authorization code of sane length. */
33
+ export declare function assertOAuthInput(input: string): void;
34
+ /** Finish sign-in from the redirect URL the browser landed on (or a bare code). */
35
+ export declare function completeMcpOAuth(name: string, input: string, projectRoot?: string): Promise<McpOAuthStatus>;
36
+ export declare function mcpOAuthStatus(name: string, projectRoot?: string): Promise<McpOAuthStatus>;
37
+ export declare function removeMcpOAuth(name: string, projectRoot?: string): Promise<void>;