stageflow 0.35.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/README.md +18 -8
  2. package/dist/agent/claudeAdapter.js +12 -0
  3. package/dist/agent/piAdapter.d.ts +9 -1
  4. package/dist/agent/piAdapter.js +30 -2
  5. package/dist/agent/piIsolatedMcp.js +14 -2
  6. package/dist/agent/piIsolatedMcpProbe.d.ts +5 -1
  7. package/dist/agent/piIsolatedMcpProbe.js +16 -30
  8. package/dist/agent/port.d.ts +7 -0
  9. package/dist/agents/agentId.d.ts +8 -0
  10. package/dist/agents/agentId.js +27 -0
  11. package/dist/agents/approvalBroker.d.ts +16 -0
  12. package/dist/agents/approvalBroker.js +45 -0
  13. package/dist/agents/authorChat.d.ts +19 -0
  14. package/dist/agents/authorChat.js +63 -0
  15. package/dist/agents/chatTurn.d.ts +23 -0
  16. package/dist/agents/chatTurn.js +77 -0
  17. package/dist/agents/communications/agentmail.d.ts +19 -0
  18. package/dist/agents/communications/agentmail.js +46 -0
  19. package/dist/agents/communications/agentmailListener.d.ts +59 -0
  20. package/dist/agents/communications/agentmailListener.js +183 -0
  21. package/dist/agents/communications/allowlist.d.ts +10 -0
  22. package/dist/agents/communications/allowlist.js +36 -0
  23. package/dist/agents/communications/channelConnections.d.ts +3 -0
  24. package/dist/agents/communications/channelConnections.js +51 -0
  25. package/dist/agents/communications/delivery.d.ts +53 -0
  26. package/dist/agents/communications/delivery.js +142 -0
  27. package/dist/agents/communications/email.d.ts +26 -0
  28. package/dist/agents/communications/email.js +20 -0
  29. package/dist/agents/communications/identity.d.ts +13 -0
  30. package/dist/agents/communications/identity.js +18 -0
  31. package/dist/agents/communications/imessageListener.d.ts +41 -0
  32. package/dist/agents/communications/imessageListener.js +92 -0
  33. package/dist/agents/communications/inbound.d.ts +63 -0
  34. package/dist/agents/communications/inbound.js +143 -0
  35. package/dist/agents/communications/listener.d.ts +36 -0
  36. package/dist/agents/communications/listener.js +38 -0
  37. package/dist/agents/communications/photon.d.ts +19 -0
  38. package/dist/agents/communications/photon.js +38 -0
  39. package/dist/agents/communications/port.d.ts +27 -0
  40. package/dist/agents/communications/port.js +6 -0
  41. package/dist/agents/communications/slack.d.ts +28 -0
  42. package/dist/agents/communications/slack.js +65 -0
  43. package/dist/agents/communications/supervisor.d.ts +36 -0
  44. package/dist/agents/communications/supervisor.js +96 -0
  45. package/dist/agents/communications/telegram.d.ts +19 -0
  46. package/dist/agents/communications/telegram.js +52 -0
  47. package/dist/agents/definitionSummary.d.ts +13 -0
  48. package/dist/agents/definitionSummary.js +91 -0
  49. package/dist/agents/delegate.d.ts +3 -0
  50. package/dist/agents/delegate.js +8 -0
  51. package/dist/agents/errors.d.ts +10 -0
  52. package/dist/agents/errors.js +32 -0
  53. package/dist/agents/guardrails.d.ts +24 -0
  54. package/dist/agents/guardrails.js +39 -0
  55. package/dist/agents/historyFit.d.ts +10 -0
  56. package/dist/agents/historyFit.js +26 -0
  57. package/dist/agents/hostContext.d.ts +54 -0
  58. package/dist/agents/hostContext.js +156 -0
  59. package/dist/agents/ledgers.d.ts +29 -0
  60. package/dist/agents/ledgers.js +31 -0
  61. package/dist/agents/lineDiff.d.ts +12 -0
  62. package/dist/agents/lineDiff.js +46 -0
  63. package/dist/agents/ndjson.d.ts +9 -0
  64. package/dist/agents/ndjson.js +47 -0
  65. package/dist/agents/otlpExport.d.ts +9 -0
  66. package/dist/agents/otlpExport.js +58 -0
  67. package/dist/agents/otlpSink.d.ts +7 -0
  68. package/dist/agents/otlpSink.js +23 -0
  69. package/dist/agents/policy.d.ts +31 -0
  70. package/dist/agents/policy.js +84 -0
  71. package/dist/agents/projectRoot.d.ts +5 -0
  72. package/dist/agents/projectRoot.js +15 -0
  73. package/dist/agents/runtime/piRuntime.d.ts +52 -0
  74. package/dist/agents/runtime/piRuntime.js +474 -0
  75. package/dist/agents/runtime/types.d.ts +77 -0
  76. package/dist/agents/runtime/types.js +26 -0
  77. package/dist/agents/sandbox.d.ts +17 -0
  78. package/dist/agents/sandbox.js +10 -0
  79. package/dist/agents/scheduleSource.d.ts +32 -0
  80. package/dist/agents/scheduleSource.js +100 -0
  81. package/dist/agents/service.d.ts +298 -0
  82. package/dist/agents/service.js +1105 -0
  83. package/dist/agents/sessions.d.ts +38 -0
  84. package/dist/agents/sessions.js +321 -0
  85. package/dist/agents/store.d.ts +277 -0
  86. package/dist/agents/store.js +643 -0
  87. package/dist/agents/trace.d.ts +27 -0
  88. package/dist/agents/trace.js +29 -0
  89. package/dist/agents/turnLifecycle.d.ts +72 -0
  90. package/dist/agents/turnLifecycle.js +157 -0
  91. package/dist/agents/webChat/publicChat.d.ts +36 -0
  92. package/dist/agents/webChat/publicChat.js +290 -0
  93. package/dist/agents/webChat/rateLimit.d.ts +15 -0
  94. package/dist/agents/webChat/rateLimit.js +33 -0
  95. package/dist/agents/webChat/slug.d.ts +2 -0
  96. package/dist/agents/webChat/slug.js +9 -0
  97. package/dist/channels/config.d.ts +11 -0
  98. package/dist/channels/config.js +95 -0
  99. package/dist/channels/configWriter.d.ts +40 -0
  100. package/dist/channels/configWriter.js +148 -0
  101. package/dist/channels/connectionEdits.d.ts +34 -0
  102. package/dist/channels/connectionEdits.js +53 -0
  103. package/dist/channels/errorInfo.d.ts +18 -0
  104. package/dist/channels/errorInfo.js +71 -0
  105. package/dist/channels/errors.d.ts +12 -0
  106. package/dist/channels/errors.js +13 -0
  107. package/dist/channels/health.d.ts +41 -0
  108. package/dist/channels/health.js +99 -0
  109. package/dist/channels/host.d.ts +55 -0
  110. package/dist/channels/host.js +185 -0
  111. package/dist/channels/http.d.ts +31 -0
  112. package/dist/channels/http.js +79 -0
  113. package/dist/channels/index.d.ts +10 -0
  114. package/dist/channels/index.js +9 -0
  115. package/dist/channels/ledger.d.ts +37 -0
  116. package/dist/channels/ledger.js +95 -0
  117. package/dist/channels/providers/agentmail.d.ts +26 -0
  118. package/dist/channels/providers/agentmail.js +94 -0
  119. package/dist/channels/providers/kit.d.ts +40 -0
  120. package/dist/channels/providers/kit.js +78 -0
  121. package/dist/channels/providers/photonImessage.d.ts +45 -0
  122. package/dist/channels/providers/photonImessage.js +188 -0
  123. package/dist/channels/providers/photonWhatsapp.d.ts +49 -0
  124. package/dist/channels/providers/photonWhatsapp.js +146 -0
  125. package/dist/channels/providers/slack.d.ts +25 -0
  126. package/dist/channels/providers/slack.js +102 -0
  127. package/dist/channels/providers/telegram.d.ts +21 -0
  128. package/dist/channels/providers/telegram.js +101 -0
  129. package/dist/channels/providers.d.ts +5 -0
  130. package/dist/channels/providers.js +22 -0
  131. package/dist/channels/registry.d.ts +54 -0
  132. package/dist/channels/registry.js +186 -0
  133. package/dist/channels/secretStore.d.ts +21 -0
  134. package/dist/channels/secretStore.js +75 -0
  135. package/dist/channels/secrets.d.ts +15 -0
  136. package/dist/channels/secrets.js +50 -0
  137. package/dist/channels/service.d.ts +54 -0
  138. package/dist/channels/service.js +307 -0
  139. package/dist/channels/stageChannels.d.ts +45 -0
  140. package/dist/channels/stageChannels.js +110 -0
  141. package/dist/channels/types.d.ts +169 -0
  142. package/dist/channels/types.js +1 -0
  143. package/dist/cli/agentsCommand.d.ts +14 -0
  144. package/dist/cli/agentsCommand.js +291 -0
  145. package/dist/cli/doctorCommand.js +3 -1
  146. package/dist/cli.js +9 -0
  147. package/dist/config/browseCatalog.js +4 -11
  148. package/dist/config/draftPackage.d.ts +2 -0
  149. package/dist/config/draftPackage.js +8 -0
  150. package/dist/config/hostConfig.js +8 -0
  151. package/dist/config/loadAgent.d.ts +266 -0
  152. package/dist/config/loadAgent.js +374 -0
  153. package/dist/config/loadPipeline.js +31 -4
  154. package/dist/config/loadStage.js +15 -0
  155. package/dist/config/mcpHostCatalog.d.ts +50 -0
  156. package/dist/config/mcpHostCatalog.js +405 -0
  157. package/dist/config/mcpOAuth.d.ts +37 -0
  158. package/dist/config/mcpOAuth.js +263 -0
  159. package/dist/config/mcpRedaction.d.ts +42 -0
  160. package/dist/config/mcpRedaction.js +314 -0
  161. package/dist/config/mcpToolCache.d.ts +20 -0
  162. package/dist/config/mcpToolCache.js +43 -0
  163. package/dist/config/mergePipelineIncludes.d.ts +5 -0
  164. package/dist/config/mergePipelineIncludes.js +52 -0
  165. package/dist/config/pipelineStageKeys.js +1 -0
  166. package/dist/config/resolveStageMcpServers.d.ts +35 -3
  167. package/dist/config/resolveStageMcpServers.js +124 -9
  168. package/dist/config/validateCatalog.d.ts +4 -1
  169. package/dist/config/validateCatalog.js +45 -1
  170. package/dist/errors/codes.d.ts +1 -1
  171. package/dist/errors/codes.js +1 -0
  172. package/dist/mcp/agentTools.d.ts +8 -0
  173. package/dist/mcp/agentTools.js +132 -0
  174. package/dist/mcp/catalogTools.js +7 -9
  175. package/dist/mcp/deps.d.ts +6 -0
  176. package/dist/mcp/pipelineAuthorSupport.d.ts +19 -0
  177. package/dist/mcp/pipelineAuthorSupport.js +10 -0
  178. package/dist/mcp/pipelineAuthorTools.d.ts +8 -0
  179. package/dist/mcp/pipelineAuthorTools.js +109 -0
  180. package/dist/mcp/server.js +5 -0
  181. package/dist/mcp/tools.js +4 -0
  182. package/dist/mcp/triggerTools.js +32 -0
  183. package/dist/memory/access.d.ts +36 -0
  184. package/dist/memory/access.js +116 -0
  185. package/dist/memory/agentIssues.d.ts +3 -0
  186. package/dist/memory/agentIssues.js +23 -0
  187. package/dist/memory/bashDeny.d.ts +6 -0
  188. package/dist/memory/bashDeny.js +35 -0
  189. package/dist/memory/host.d.ts +51 -0
  190. package/dist/memory/host.js +394 -0
  191. package/dist/memory/ids.d.ts +15 -0
  192. package/dist/memory/ids.js +31 -0
  193. package/dist/memory/worker.d.ts +1 -0
  194. package/dist/memory/worker.js +84 -0
  195. package/dist/models/catalog.d.ts +113 -0
  196. package/dist/models/catalog.js +238 -0
  197. package/dist/models/response.d.ts +29 -0
  198. package/dist/models/response.js +25 -0
  199. package/dist/operatorAgent/agentAuthorContext.d.ts +17 -0
  200. package/dist/operatorAgent/agentAuthorContext.js +32 -0
  201. package/dist/operatorAgent/fakeHost.js +2 -0
  202. package/dist/operatorAgent/host.js +6 -3
  203. package/dist/operatorAgent/index.d.ts +4 -0
  204. package/dist/operatorAgent/index.js +4 -0
  205. package/dist/operatorAgent/piModel.js +2 -0
  206. package/dist/operatorAgent/profiles/agentAuthor.d.ts +11 -0
  207. package/dist/operatorAgent/profiles/agentAuthor.js +192 -0
  208. package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -0
  209. package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +29 -0
  210. package/dist/operatorAgent/profiles/workshopAuthor.js +2 -2
  211. package/dist/operatorAgent/profiles/workshopAuthorTools.js +3 -3
  212. package/dist/operatorAgent/proposals.d.ts +1 -0
  213. package/dist/operatorAgent/proposals.js +23 -2
  214. package/dist/package-meta.d.ts +1 -1
  215. package/dist/package-meta.js +1 -1
  216. package/dist/preflight/channelChecks.d.ts +17 -0
  217. package/dist/preflight/channelChecks.js +106 -0
  218. package/dist/preflight/pipelinePreflight.d.ts +9 -2
  219. package/dist/preflight/pipelinePreflight.js +10 -1
  220. package/dist/runstore/createStore.d.ts +2 -0
  221. package/dist/runstore/createStore.js +8 -0
  222. package/dist/runstore/overviewSummary.d.ts +56 -0
  223. package/dist/runstore/overviewSummary.js +134 -0
  224. package/dist/runstore/port.d.ts +3 -0
  225. package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -0
  226. package/dist/runstore/sqlite/SqliteRunStore.js +18 -0
  227. package/dist/runstore/sqlite/migrations/013-agents.d.ts +12 -0
  228. package/dist/runstore/sqlite/migrations/013-agents.js +156 -0
  229. package/dist/runstore/sqlite/migrations/014-agent-deliveries.d.ts +11 -0
  230. package/dist/runstore/sqlite/migrations/014-agent-deliveries.js +32 -0
  231. package/dist/runstore/sqlite/migrations/index.js +4 -0
  232. package/dist/runtime/pipelineRunner.js +3 -1
  233. package/dist/runtime/repositoryMaterialize.d.ts +1 -1
  234. package/dist/runtime/runManager.js +4 -1
  235. package/dist/runtime/stageAttemptBootstrap.js +31 -1
  236. package/dist/runtime/stageEnvironment.d.ts +2 -0
  237. package/dist/runtime/stageEnvironment.js +6 -1
  238. package/dist/runtime/stageProcessLauncher.js +54 -1
  239. package/dist/runtime/stageSecrets.d.ts +6 -1
  240. package/dist/runtime/stageSecrets.js +34 -1
  241. package/dist/server/agentAuthorRoutes.d.ts +5 -0
  242. package/dist/server/agentAuthorRoutes.js +64 -0
  243. package/dist/server/agentRoutes.d.ts +5 -0
  244. package/dist/server/agentRoutes.js +323 -0
  245. package/dist/server/bootstrap.d.ts +11 -0
  246. package/dist/server/bootstrap.js +40 -10
  247. package/dist/server/channelRoutes.d.ts +9 -0
  248. package/dist/server/channelRoutes.js +88 -0
  249. package/dist/server/controlToken.js +8 -0
  250. package/dist/server/http.d.ts +8 -1
  251. package/dist/server/http.js +183 -20
  252. package/dist/server/mcpHost.js +1 -0
  253. package/dist/server/memoryRoutes.d.ts +5 -0
  254. package/dist/server/memoryRoutes.js +146 -0
  255. package/dist/server/projectMcpRoutes.d.ts +1 -0
  256. package/dist/server/projectMcpRoutes.js +241 -3
  257. package/dist/toolkit/guard.d.ts +27 -0
  258. package/dist/toolkit/guard.js +98 -0
  259. package/dist/toolkit/index.d.ts +48 -0
  260. package/dist/toolkit/index.js +82 -0
  261. package/dist/toolkit/meta.d.ts +36 -0
  262. package/dist/toolkit/meta.js +117 -0
  263. package/dist/toolkit/profiles.d.ts +13 -0
  264. package/dist/toolkit/profiles.js +25 -0
  265. package/dist/toolkit/record.d.ts +15 -0
  266. package/dist/toolkit/record.js +17 -0
  267. package/dist/tools/memoryTools.d.ts +95 -0
  268. package/dist/tools/memoryTools.js +179 -0
  269. package/dist/tools/sendMessage.d.ts +21 -0
  270. package/dist/tools/sendMessage.js +25 -0
  271. package/dist/types/pipeline.d.ts +7 -1
  272. package/dist/types/stage.d.ts +15 -0
  273. package/dist/ui/assets/AssistantRuntimeProvider-CsiPFhCT.js +9 -0
  274. package/dist/ui/assets/MarkdownText-D7tYaIFO.js +29 -0
  275. package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +6 -0
  276. package/dist/ui/assets/Tooltip-DrpoRRNi.js +1 -0
  277. package/dist/ui/assets/chat-BbLukq9W.js +5 -0
  278. package/dist/ui/assets/chat-zIZU_bP1.css +2 -0
  279. package/dist/ui/assets/jsx-runtime-iVoFAvQ_.js +1 -0
  280. package/dist/ui/assets/{index-CIPbbBUi.css → main-BFo79krC.css} +1 -1
  281. package/dist/ui/assets/main-CijYMeM0.js +259 -0
  282. package/dist/ui/chat.html +25 -0
  283. package/dist/ui/index.html +6 -2
  284. package/dist/workshop/modelSettings.d.ts +2 -2
  285. package/dist/workshop/modelSettings.js +5 -11
  286. package/examples/README.md +1 -0
  287. package/examples/channels-smoke/README.md +23 -0
  288. package/examples/channels-smoke/channels-smoke.pipeline.yaml +11 -0
  289. package/examples/channels-smoke/channels-smoke.task.yaml +3 -0
  290. package/examples/channels-smoke/send-email.yaml +32 -0
  291. package/examples/channels-smoke/send-text.yaml +31 -0
  292. package/examples/github-release/README.md +1 -1
  293. package/examples/memory/README.md +99 -0
  294. package/examples/memory/desk-outage.task.yaml +15 -0
  295. package/examples/memory/desk-refund.task.yaml +15 -0
  296. package/examples/memory/desk-revisit-outage.task.yaml +11 -0
  297. package/examples/memory/desk-revisit.pipeline.yaml +123 -0
  298. package/examples/memory/desk.pipeline.yaml +192 -0
  299. package/examples/memory/file-card.pipeline.yaml +39 -0
  300. package/examples/memory/file-ledger.task.yaml +22 -0
  301. package/examples/memory/file-release.task.yaml +24 -0
  302. package/examples/memory/file-rich.pipeline.yaml +40 -0
  303. package/examples/memory/file-ticket.task.yaml +7 -0
  304. package/examples/memory/file-timeline.task.yaml +28 -0
  305. package/examples/memory/find-card.pipeline.yaml +46 -0
  306. package/examples/memory/find-ticket.task.yaml +7 -0
  307. package/examples/memory/missing.pipeline.yaml +16 -0
  308. package/examples/memory/recall.pipeline.yaml +24 -0
  309. package/examples/memory/recall.task.yaml +5 -0
  310. package/examples/memory/remember-ada.task.yaml +6 -0
  311. package/examples/memory/remember-grace.task.yaml +6 -0
  312. package/examples/memory/remember.pipeline.yaml +58 -0
  313. package/examples/memory/stageflow.yaml +12 -0
  314. package/examples/memory/update-card.pipeline.yaml +43 -0
  315. package/examples/memory/update-ledger.task.yaml +23 -0
  316. package/examples/memory/update-rich.pipeline.yaml +44 -0
  317. package/examples/memory/update-ticket.task.yaml +7 -0
  318. package/examples/stage-mcp/README.md +1 -1
  319. package/package.json +9 -3
  320. package/dist/ui/assets/index-BoFPkKFg.js +0 -256
@@ -0,0 +1,54 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { ChannelService } from "../channels/service.js";
3
+ import { AgentInboundRouter } from "./communications/inbound.js";
4
+ import { ReplyDelivery } from "./communications/delivery.js";
5
+ import { ChannelSupervisor } from "./communications/supervisor.js";
6
+ import { AgentChatController } from "./chatTurn.js";
7
+ import { type OtlpSink } from "./otlpSink.js";
8
+ import type { ContactInfo } from "../channels/types.js";
9
+ import type { McpToolDeps } from "../mcp/deps.js";
10
+ import { AgentService, type AgentEnvironment } from "./service.js";
11
+ import type { ConversationBackend } from "./sessions.js";
12
+ import { PublicWebChat } from "./webChat/publicChat.js";
13
+ export type AgentHostContext = {
14
+ service: AgentService;
15
+ inbound: AgentInboundRouter;
16
+ publicChat: PublicWebChat;
17
+ chat: AgentChatController;
18
+ backend: ConversationBackend;
19
+ /** Streams held open to AgentMail and iMessage; absent unless the Host listens (not in tests). */
20
+ channelListeners?: ChannelSupervisor;
21
+ /** Replies to people on channels: retries, resend, mark delivered. */
22
+ delivery: ReplyDelivery;
23
+ /** How people reach a connection (the number to text), cached for a minute. */
24
+ channelContact(connectionId: string): Promise<{
25
+ provider: string;
26
+ contact: ContactInfo;
27
+ } | undefined>;
28
+ /** Stop what the Host started for agents (listeners). */
29
+ stop(): Promise<void>;
30
+ };
31
+ /** Read-only lookups the editor warnings use: `.mcp.json`, skills, and channel connections. */
32
+ export declare function createAgentEnvironment(options: {
33
+ agentDir: string;
34
+ cwd: string;
35
+ channels?: Pick<ChannelService, "list">;
36
+ }): AgentEnvironment;
37
+ /**
38
+ * Agent runtime for one Host. Agent state shares the Host's SQLite connection
39
+ * (migration 013). Turns left running by a previous process are failed here,
40
+ * since nothing can resume them.
41
+ */
42
+ export declare function createAgentHostContext(options: {
43
+ cwd: string;
44
+ agentDir: string;
45
+ connection: Database.Database;
46
+ channels?: Pick<ChannelService, "list">;
47
+ backend?: ConversationBackend;
48
+ environment?: AgentEnvironment;
49
+ otlpSink?: OtlpSink | undefined;
50
+ /** Host services the Stageflow tools need; agent services are added here. Without it agents get no Stageflow tools. */
51
+ /** Hold streams open to AgentMail and iMessage for agents linked to them. Only a running Host does this. */
52
+ listenOnChannels?: boolean;
53
+ toolkitDeps?: Pick<McpToolDeps, "manager" | "store" | "agentDir" | "providerAuthContext" | "projectRoot" | "providerBoot">;
54
+ }): AgentHostContext;
@@ -0,0 +1,156 @@
1
+ import { listSkills } from "../config/listSkills.js";
2
+ import { listProjectMcpCatalog } from "../config/resolveStageMcpServers.js";
3
+ import { channelInboundConnections } from "./communications/channelConnections.js";
4
+ import { channelHostFor } from "../channels/host.js";
5
+ import { channelIdentity } from "./communications/identity.js";
6
+ import { AgentInboundRouter } from "./communications/inbound.js";
7
+ import { ReplyDelivery } from "./communications/delivery.js";
8
+ import { ChannelSupervisor } from "./communications/supervisor.js";
9
+ import { AgentChatController } from "./chatTurn.js";
10
+ import { otlpSinkFromEnv } from "./otlpSink.js";
11
+ import { buildToolkit } from "../toolkit/index.js";
12
+ import { createPiAgentRuntime } from "./runtime/piRuntime.js";
13
+ import { AgentService } from "./service.js";
14
+ import { getModelCatalog } from "../models/catalog.js";
15
+ import { defaultAgentProjectRoot } from "./projectRoot.js";
16
+ import { PublicWebChat } from "./webChat/publicChat.js";
17
+ import { AgentStore } from "./store.js";
18
+ /** Read-only lookups the editor warnings use: `.mcp.json`, skills, and channel connections. */
19
+ export function createAgentEnvironment(options) {
20
+ return {
21
+ async mcpServerNames(projectRoot) {
22
+ const catalog = await listProjectMcpCatalog(projectRoot);
23
+ return catalog.status === "ok" ? catalog.servers.map((server) => server.name) : [];
24
+ },
25
+ async skillNames(projectRoot) {
26
+ const catalog = await listSkills({ cwd: projectRoot, agentDir: options.agentDir });
27
+ return catalog.skills.map((skill) => skill.name);
28
+ },
29
+ async modelInfo(id) {
30
+ const snapshot = await getModelCatalog(options.cwd).snapshot().catch(() => undefined);
31
+ if (!snapshot || snapshot.providers.length === 0)
32
+ return undefined;
33
+ const model = snapshot.providers.flatMap((provider) => provider.models).find((entry) => entry.id === id);
34
+ return { known: model !== undefined, nativeTools: model?.nativeTools ?? false };
35
+ },
36
+ async connectionIdentity(connectionId) {
37
+ try {
38
+ const connection = channelHostFor(defaultAgentProjectRoot(options.cwd)).registry.resolve(connectionId, false);
39
+ return channelIdentity({ id: connection.id, provider: connection.provider, config: connection.config, credential: connection.credential ? { secrets: connection.credential.secrets } : undefined });
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ },
45
+ async connectionIds() {
46
+ if (!options.channels)
47
+ return undefined;
48
+ return options.channels.list().connections.map((connection) => connection.id);
49
+ },
50
+ };
51
+ }
52
+ /**
53
+ * Agent runtime for one Host. Agent state shares the Host's SQLite connection
54
+ * (migration 013). Turns left running by a previous process are failed here,
55
+ * since nothing can resume them.
56
+ */
57
+ export function createAgentHostContext(options) {
58
+ const service = new AgentService({
59
+ store: new AgentStore(options.connection),
60
+ environment: options.environment ??
61
+ createAgentEnvironment({
62
+ agentDir: options.agentDir,
63
+ cwd: options.cwd,
64
+ ...(options.channels ? { channels: options.channels } : {}),
65
+ }),
66
+ });
67
+ service.recoverInterruptedTurns();
68
+ let toolkit;
69
+ let chatRef;
70
+ const backend = options.backend ??
71
+ createPiAgentRuntime({
72
+ cwd: options.cwd,
73
+ agentDir: options.agentDir,
74
+ ...(options.toolkitDeps
75
+ ? {
76
+ toolkit: () => (toolkit ??= buildToolkit({
77
+ ...options.toolkitDeps,
78
+ cwd: options.cwd,
79
+ agentService: service,
80
+ ...(chatRef ? { agentChat: chatRef } : {}),
81
+ })),
82
+ }
83
+ : {}),
84
+ });
85
+ const chat = new AgentChatController({
86
+ service,
87
+ backend,
88
+ otlpSink: "otlpSink" in options ? options.otlpSink : otlpSinkFromEnv(),
89
+ logError: (message) => console.error(message),
90
+ });
91
+ chatRef = chat;
92
+ // Archived agents will not run again: let the runtime drop what it holds for their sessions.
93
+ service.onArchived((_agentId, sessionIds) => {
94
+ for (const sessionId of sessionIds)
95
+ void chat.release(sessionId);
96
+ });
97
+ // The send ledger lives under the project root, so use the agents' writable root, not a read-only install dir.
98
+ const channelConnectionsForAgents = channelInboundConnections(defaultAgentProjectRoot(options.cwd));
99
+ const delivery = new ReplyDelivery({
100
+ service,
101
+ reply: (connectionId, to, text, key) => channelConnectionsForAgents.reply(connectionId, to, text, key),
102
+ log: (message) => console.error(message),
103
+ });
104
+ const inbound = new AgentInboundRouter({
105
+ service,
106
+ chat,
107
+ connections: channelConnectionsForAgents,
108
+ projectRoot: defaultAgentProjectRoot(options.cwd),
109
+ logError: (message) => console.error(message),
110
+ delivery,
111
+ });
112
+ const publicChat = new PublicWebChat({
113
+ service,
114
+ chat,
115
+ projectRoot: defaultAgentProjectRoot(options.cwd),
116
+ allowedOrigins: (process.env.STAGEFLOW_PUBLIC_CHAT_ORIGINS ?? "")
117
+ .split(",")
118
+ .map((origin) => origin.trim())
119
+ .filter((origin) => origin.length > 0),
120
+ logError: (message) => console.error(message),
121
+ });
122
+ const connections = channelConnectionsForAgents;
123
+ const channelListeners = options.listenOnChannels
124
+ ? new ChannelSupervisor({
125
+ service,
126
+ projectRoot: defaultAgentProjectRoot(options.cwd),
127
+ resolve: (id) => connections.resolve?.(id),
128
+ deliver: (message) => inbound.deliver(message),
129
+ log: (message) => console.error(message),
130
+ })
131
+ : undefined;
132
+ const contactCache = new Map();
133
+ const channelContact = async (connectionId) => {
134
+ const cached = contactCache.get(connectionId);
135
+ if (cached && Date.now() - cached.at < 60_000)
136
+ return cached.value;
137
+ const provider = connections.resolve?.(connectionId)?.provider;
138
+ const contact = provider ? await connections.contact?.(connectionId) : undefined;
139
+ const value = provider && contact ? { provider, contact } : undefined;
140
+ // A failed lookup is retried on the next ask rather than remembered.
141
+ if (value?.contact.ok !== false)
142
+ contactCache.set(connectionId, { at: Date.now(), value });
143
+ return value;
144
+ };
145
+ channelListeners?.start();
146
+ if (options.listenOnChannels)
147
+ delivery.start();
148
+ if (options.listenOnChannels)
149
+ void inbound.resumeInterrupted().catch((error) => console.error(`agent resume failed: ${String(error)}`));
150
+ return { service, chat, backend, inbound, publicChat, channelContact, ...(channelListeners ? { channelListeners } : {}), delivery, stop: async () => {
151
+ delivery.stop();
152
+ await channelListeners?.stop();
153
+ // The Host owns every runtime session's lifetime: nothing outlives it.
154
+ await backend.dispose?.().catch((error) => console.error(`agent runtime dispose failed: ${String(error)}`));
155
+ } };
156
+ }
@@ -0,0 +1,29 @@
1
+ import { type AgentDelivery, type AgentStore } from "./store.js";
2
+ export type DeliveryInput = {
3
+ turnId: string;
4
+ sessionId: string;
5
+ connectionId: string;
6
+ provider: string;
7
+ replyTo: string;
8
+ text: string;
9
+ };
10
+ export type DeliveryClaim = "first" | "retry" | "manual";
11
+ /** What reply delivery needs of storage, and nothing else. */
12
+ export type DeliveryLedger = {
13
+ create(input: DeliveryInput): AgentDelivery;
14
+ get(turnId: string): AgentDelivery | undefined;
15
+ patch(turnId: string, patch: Parameters<AgentStore["updateDelivery"]>[1]): AgentDelivery | undefined;
16
+ due(): AgentDelivery[];
17
+ /**
18
+ * Take the right to send, atomically: moves the delivery to `sending` only if it is still in a state this kind of
19
+ * attempt may start from. Returns the claimed row, or undefined when someone else holds it or it already went out.
20
+ */
21
+ claim(turnId: string, kind: DeliveryClaim): AgentDelivery | undefined;
22
+ };
23
+ export declare function deliveryLedger(store: AgentStore, nowIso: () => string): DeliveryLedger;
24
+ /** What the scheduler needs of storage: when each scheduled agent runs next. */
25
+ export type ScheduleLedger = {
26
+ nextRun(projectRoot: string, agentId: string): string | undefined;
27
+ setNextRun(projectRoot: string, agentId: string, nextRunAt: string): void;
28
+ };
29
+ export declare function scheduleLedger(store: AgentStore): ScheduleLedger;
@@ -0,0 +1,31 @@
1
+ import { agentProjectKey } from "./store.js";
2
+ export function deliveryLedger(store, nowIso) {
3
+ return {
4
+ create(input) {
5
+ const now = nowIso();
6
+ const delivery = { ...input, state: "pending", attempts: 0, errorCode: null, errorMessage: null, retryable: false, nextAttemptAt: null, createdAt: now, updatedAt: now };
7
+ store.insertDelivery(delivery);
8
+ return delivery;
9
+ },
10
+ get: (turnId) => store.getDelivery(turnId),
11
+ patch(turnId, patch) {
12
+ store.updateDelivery(turnId, patch, nowIso());
13
+ return store.getDelivery(turnId);
14
+ },
15
+ due: () => store.dueDeliveries(nowIso()),
16
+ claim(turnId, kind) {
17
+ const now = nowIso();
18
+ return kind === "first"
19
+ ? store.claimDelivery(turnId, ["pending"], now)
20
+ : kind === "manual"
21
+ ? store.claimDelivery(turnId, ["failed", "unknown"], now)
22
+ : store.claimDelivery(turnId, ["failed"], now, { dueRetry: true });
23
+ },
24
+ };
25
+ }
26
+ export function scheduleLedger(store) {
27
+ return {
28
+ nextRun: (projectRoot, agentId) => store.getScheduleNextRun(agentProjectKey(projectRoot), agentId),
29
+ setNextRun: (projectRoot, agentId, nextRunAt) => store.setScheduleNextRun(agentProjectKey(projectRoot), agentId, nextRunAt),
30
+ };
31
+ }
@@ -0,0 +1,12 @@
1
+ export type DiffLine = {
2
+ kind: "same" | "added" | "removed";
3
+ text: string;
4
+ before?: number;
5
+ after?: number;
6
+ };
7
+ /**
8
+ * Line diff by longest common subsequence. Agent YAML files are small (tens to
9
+ * low hundreds of lines), so the O(n·m) table is fine and keeps this
10
+ * dependency-free.
11
+ */
12
+ export declare function diffLines(before: string, after: string): DiffLine[];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Line diff by longest common subsequence. Agent YAML files are small (tens to
3
+ * low hundreds of lines), so the O(n·m) table is fine and keeps this
4
+ * dependency-free.
5
+ */
6
+ export function diffLines(before, after) {
7
+ const a = splitLines(before);
8
+ const b = splitLines(after);
9
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
10
+ for (let i = a.length - 1; i >= 0; i -= 1) {
11
+ for (let j = b.length - 1; j >= 0; j -= 1) {
12
+ lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
13
+ }
14
+ }
15
+ const out = [];
16
+ let i = 0;
17
+ let j = 0;
18
+ while (i < a.length && j < b.length) {
19
+ if (a[i] === b[j]) {
20
+ out.push({ kind: "same", text: a[i], before: i + 1, after: j + 1 });
21
+ i += 1;
22
+ j += 1;
23
+ }
24
+ else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
25
+ out.push({ kind: "removed", text: a[i], before: i + 1 });
26
+ i += 1;
27
+ }
28
+ else {
29
+ out.push({ kind: "added", text: b[j], after: j + 1 });
30
+ j += 1;
31
+ }
32
+ }
33
+ for (; i < a.length; i += 1)
34
+ out.push({ kind: "removed", text: a[i], before: i + 1 });
35
+ for (; j < b.length; j += 1)
36
+ out.push({ kind: "added", text: b[j], after: j + 1 });
37
+ return out;
38
+ }
39
+ function splitLines(text) {
40
+ if (text === "")
41
+ return [];
42
+ const lines = text.split("\n");
43
+ if (lines.at(-1) === "")
44
+ lines.pop();
45
+ return lines;
46
+ }
@@ -0,0 +1,9 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ /** Write one NDJSON frame, waiting for drain. (The request is already consumed here, so only the response state matters.) */
3
+ export declare function writeFrame(res: ServerResponse, frame: unknown): Promise<void>;
4
+ export declare class BodyError extends Error {
5
+ readonly reason: "too_large" | "not_json" | "not_object";
6
+ constructor(reason: "too_large" | "not_json" | "not_object");
7
+ }
8
+ /** Read a JSON object body. An empty body is `{}`. Throws BodyError so each surface can answer in its own words. */
9
+ export declare function readJsonBody(req: IncomingMessage, maxBytes: number): Promise<Record<string, unknown>>;
@@ -0,0 +1,47 @@
1
+ /** Write one NDJSON frame, waiting for drain. (The request is already consumed here, so only the response state matters.) */
2
+ export async function writeFrame(res, frame) {
3
+ if (res.writableEnded || res.destroyed)
4
+ return;
5
+ if (res.write(`${JSON.stringify(frame)}\n`))
6
+ return;
7
+ await new Promise((resolve) => {
8
+ const done = () => {
9
+ res.off("drain", done);
10
+ res.off("close", done);
11
+ resolve();
12
+ };
13
+ res.once("drain", done);
14
+ res.once("close", done);
15
+ });
16
+ }
17
+ export class BodyError extends Error {
18
+ reason;
19
+ constructor(reason) {
20
+ super(reason);
21
+ this.reason = reason;
22
+ }
23
+ }
24
+ /** Read a JSON object body. An empty body is `{}`. Throws BodyError so each surface can answer in its own words. */
25
+ export async function readJsonBody(req, maxBytes) {
26
+ const chunks = [];
27
+ let size = 0;
28
+ for await (const chunk of req) {
29
+ const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
30
+ size += buffer.length;
31
+ if (size > maxBytes)
32
+ throw new BodyError("too_large");
33
+ chunks.push(buffer);
34
+ }
35
+ if (chunks.length === 0)
36
+ return {};
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
40
+ }
41
+ catch {
42
+ throw new BodyError("not_json");
43
+ }
44
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
45
+ throw new BodyError("not_object");
46
+ return parsed;
47
+ }
@@ -0,0 +1,9 @@
1
+ import type { AgentSpan, AgentTurn } from "./store.js";
2
+ /** OTLP/JSON `ExportTraceServiceRequest` for the given turns. */
3
+ export declare function exportTurnsToOtlp(turns: Array<{
4
+ turn: AgentTurn;
5
+ spans: AgentSpan[];
6
+ }>, resource: {
7
+ serviceName: string;
8
+ agentId: string;
9
+ }): Record<string, unknown>;
@@ -0,0 +1,58 @@
1
+ import { redactSecrets } from "../agent/streamLogRedact.js";
2
+ function attribute(key, value) {
3
+ if (typeof value === "string")
4
+ return { key, value: { stringValue: value } };
5
+ if (typeof value === "boolean")
6
+ return { key, value: { boolValue: value } };
7
+ if (typeof value === "number") {
8
+ return Number.isInteger(value) ? { key, value: { intValue: String(value) } } : { key, value: { doubleValue: value } };
9
+ }
10
+ return undefined;
11
+ }
12
+ function unixNanos(iso) {
13
+ return `${BigInt(Date.parse(iso)) * 1000000n}`;
14
+ }
15
+ const STATUS_CODE = { ok: 1, error: 2, cancelled: 2 };
16
+ function spanAttributes(span) {
17
+ const attrs = [
18
+ attribute("gen_ai.operation.name", span.name),
19
+ attribute("stageflow.span.kind", span.kind),
20
+ span.inputTokens !== null ? attribute("gen_ai.usage.input_tokens", span.inputTokens) : undefined,
21
+ span.outputTokens !== null ? attribute("gen_ai.usage.output_tokens", span.outputTokens) : undefined,
22
+ ...Object.entries(span.attributes).map(([key, value]) => attribute(key, value)),
23
+ // Bodies were redacted when stored; redact again so a future store path can't leak through export.
24
+ span.inputText !== null ? attribute("gen_ai.input.messages", redactSecrets(span.inputText)) : undefined,
25
+ span.outputText !== null ? attribute("gen_ai.output.messages", redactSecrets(span.outputText)) : undefined,
26
+ ];
27
+ return attrs.filter((item) => item !== undefined);
28
+ }
29
+ /** OTLP/JSON `ExportTraceServiceRequest` for the given turns. */
30
+ export function exportTurnsToOtlp(turns, resource) {
31
+ return {
32
+ resourceSpans: [
33
+ {
34
+ resource: {
35
+ attributes: [
36
+ attribute("service.name", resource.serviceName),
37
+ attribute("gen_ai.agent.name", resource.agentId),
38
+ ].filter(Boolean),
39
+ },
40
+ scopeSpans: [
41
+ {
42
+ scope: { name: "stageflow.agents" },
43
+ spans: turns.flatMap(({ turn, spans }) => spans.map((span) => ({
44
+ traceId: turn.traceId,
45
+ spanId: span.id,
46
+ ...(span.parentSpanId ? { parentSpanId: span.parentSpanId } : {}),
47
+ name: span.name,
48
+ startTimeUnixNano: unixNanos(span.startedAt),
49
+ endTimeUnixNano: unixNanos(span.endedAt ?? span.startedAt),
50
+ status: { code: STATUS_CODE[span.status] ?? 0 },
51
+ attributes: spanAttributes(span),
52
+ }))),
53
+ },
54
+ ],
55
+ },
56
+ ],
57
+ };
58
+ }
@@ -0,0 +1,7 @@
1
+ export type OtlpSink = (payload: Record<string, unknown>) => Promise<void>;
2
+ /**
3
+ * Collector from the standard OTel environment (`OTEL_EXPORTER_OTLP_ENDPOINT` or
4
+ * `..._TRACES_ENDPOINT`, plus `OTEL_EXPORTER_OTLP_HEADERS`). Undefined when none
5
+ * is configured, so `observability.otlp` is a no-op without a destination.
6
+ */
7
+ export declare function otlpSinkFromEnv(env?: NodeJS.ProcessEnv, fetchImpl?: typeof fetch): OtlpSink | undefined;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Collector from the standard OTel environment (`OTEL_EXPORTER_OTLP_ENDPOINT` or
3
+ * `..._TRACES_ENDPOINT`, plus `OTEL_EXPORTER_OTLP_HEADERS`). Undefined when none
4
+ * is configured, so `observability.otlp` is a no-op without a destination.
5
+ */
6
+ export function otlpSinkFromEnv(env = process.env, fetchImpl = fetch) {
7
+ const traces = env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim();
8
+ const base = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
9
+ const url = traces || (base ? `${base.replace(/\/$/, "")}/v1/traces` : undefined);
10
+ if (!url)
11
+ return undefined;
12
+ const headers = { "content-type": "application/json" };
13
+ for (const pair of (env.OTEL_EXPORTER_OTLP_HEADERS ?? "").split(",")) {
14
+ const eq = pair.indexOf("=");
15
+ if (eq > 0)
16
+ headers[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
17
+ }
18
+ return async (payload) => {
19
+ const res = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(payload), signal: AbortSignal.timeout(10_000) });
20
+ if (!res.ok)
21
+ throw new Error(`OTLP collector responded ${res.status}`);
22
+ };
23
+ }
@@ -0,0 +1,31 @@
1
+ import { type AgentDefinition } from "../config/loadAgent.js";
2
+ import type { ApprovalActor } from "./store.js";
3
+ export type { ApprovalActor };
4
+ type Grants = Pick<AgentDefinition, "tools" | "sub_agents">;
5
+ /** True when the revision grants this tool id (builtin name, `<server>.<tool>`, or `agent:<id>`). */
6
+ export declare function isToolGranted(definition: Grants, tool: string): boolean;
7
+ /** Tools that always need a human decision, whatever the agent's `ask_first` list says. */
8
+ export declare const ALWAYS_ASK_FIRST: readonly ["bash"];
9
+ /** The tools this agent asks about: its `ask_first` list plus any granted tool that always needs a person. */
10
+ export declare function effectiveAskFirst(definition: Grants & Pick<AgentDefinition, "approvals">): string[];
11
+ export declare function requiresAskFirst(definition: Pick<AgentDefinition, "approvals">, tool: string): boolean;
12
+ export declare function canApprove(definition: Pick<AgentDefinition, "approvals">, actor: ApprovalActor): boolean;
13
+ /**
14
+ * Agent definitions and Stageflow state are never writable by an agent through a tool that names the path, whatever its
15
+ * grants. This does not cover `bash` (see `forbiddenWriteTarget`).
16
+ */
17
+ export declare function isWritePathForbidden(projectRoot: string, stageflowHome: string, targetPath: string): boolean;
18
+ /** Every path-like string the runtime can see in a tool call's arguments (including `paths: [...]`). */
19
+ export declare function pathArgumentsOf(input: Record<string, unknown>): string[];
20
+ /**
21
+ * The first forbidden path a tool call would write, or undefined. Applies to every tool that can write a path it is
22
+ * given: `write`, `edit`, and any other tool (an MCP file tool, say) with a path argument. Only Pi's read-only builtins
23
+ * are exempt. This is a check on arguments, not a sandbox: it cannot see what a tool does with them.
24
+ *
25
+ * `bash` is NOT path-confined. The runtime sees a command string, not its effects, so a granted `bash` can modify agent
26
+ * definitions and Stageflow state however this check is configured. Because of that every `bash` call is ask-first
27
+ * (ALWAYS_ASK_FIRST) and a person reads the command before it runs. Do not grant `bash` to an agent you need confined;
28
+ * the editor warns when it is granted (agent.shell_not_path_confined).
29
+ */
30
+ export declare function forbiddenWriteTarget(projectRoot: string, stageflowHome: string, toolName: string, input: Record<string, unknown>): string | undefined;
31
+ export declare function trustedLocalNotice(): string;
@@ -0,0 +1,84 @@
1
+ import path from "node:path";
2
+ import { grantedToolIds } from "../config/loadAgent.js";
3
+ /** True when the revision grants this tool id (builtin name, `<server>.<tool>`, or `agent:<id>`). */
4
+ export function isToolGranted(definition, tool) {
5
+ if (grantedToolIds(definition).has(tool))
6
+ return true;
7
+ const dot = tool.indexOf(".");
8
+ if (dot <= 0)
9
+ return false;
10
+ return definition.tools.mcp[tool.slice(0, dot)]?.includes("*") ?? false;
11
+ }
12
+ /** Tools that always need a human decision, whatever the agent's `ask_first` list says. */
13
+ export const ALWAYS_ASK_FIRST = ["bash"];
14
+ /** The tools this agent asks about: its `ask_first` list plus any granted tool that always needs a person. */
15
+ export function effectiveAskFirst(definition) {
16
+ const forced = ALWAYS_ASK_FIRST.filter((tool) => isToolGranted(definition, tool));
17
+ return [...new Set([...definition.approvals.ask_first, ...forced])];
18
+ }
19
+ export function requiresAskFirst(definition, tool) {
20
+ return ALWAYS_ASK_FIRST.includes(tool) || definition.approvals.ask_first.includes(tool);
21
+ }
22
+ export function canApprove(definition, actor) {
23
+ const { approvers } = definition.approvals;
24
+ if (actor.kind === "operator")
25
+ return approvers.operators;
26
+ return approvers.channel_ids.includes(actor.channelId);
27
+ }
28
+ /**
29
+ * Agent definitions and Stageflow state are never writable by an agent through a tool that names the path, whatever its
30
+ * grants. This does not cover `bash` (see `forbiddenWriteTarget`).
31
+ */
32
+ export function isWritePathForbidden(projectRoot, stageflowHome, targetPath) {
33
+ const resolved = path.resolve(targetPath);
34
+ const within = (dir) => resolved === dir || resolved.startsWith(`${dir}${path.sep}`);
35
+ const root = path.resolve(projectRoot);
36
+ // The project may live under the Stageflow home (container workspace); only outside it is the home off limits.
37
+ if (!within(root) && within(path.resolve(stageflowHome)))
38
+ return true;
39
+ const relative = path.relative(path.join(root, "agents"), resolved);
40
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
41
+ return relative.endsWith(".agent.yaml");
42
+ }
43
+ return false;
44
+ }
45
+ const READ_ONLY_BUILTINS = new Set(["read", "grep", "find", "ls"]);
46
+ /** Argument names that carry a filesystem path in Pi's builtin tools and in common MCP file tools. */
47
+ const PATH_ARGUMENTS = ["path", "file_path", "filePath", "file", "target", "destination", "dest", "output_path"];
48
+ /** Every path-like string the runtime can see in a tool call's arguments (including `paths: [...]`). */
49
+ export function pathArgumentsOf(input) {
50
+ const found = [];
51
+ for (const key of PATH_ARGUMENTS) {
52
+ const value = input[key];
53
+ if (typeof value === "string" && value.length > 0)
54
+ found.push(value);
55
+ }
56
+ if (Array.isArray(input.paths))
57
+ for (const value of input.paths)
58
+ if (typeof value === "string" && value.length > 0)
59
+ found.push(value);
60
+ return found;
61
+ }
62
+ /**
63
+ * The first forbidden path a tool call would write, or undefined. Applies to every tool that can write a path it is
64
+ * given: `write`, `edit`, and any other tool (an MCP file tool, say) with a path argument. Only Pi's read-only builtins
65
+ * are exempt. This is a check on arguments, not a sandbox: it cannot see what a tool does with them.
66
+ *
67
+ * `bash` is NOT path-confined. The runtime sees a command string, not its effects, so a granted `bash` can modify agent
68
+ * definitions and Stageflow state however this check is configured. Because of that every `bash` call is ask-first
69
+ * (ALWAYS_ASK_FIRST) and a person reads the command before it runs. Do not grant `bash` to an agent you need confined;
70
+ * the editor warns when it is granted (agent.shell_not_path_confined).
71
+ */
72
+ export function forbiddenWriteTarget(projectRoot, stageflowHome, toolName, input) {
73
+ if (READ_ONLY_BUILTINS.has(toolName))
74
+ return undefined;
75
+ for (const target of pathArgumentsOf(input)) {
76
+ const resolved = path.isAbsolute(target) ? target : path.resolve(projectRoot, target);
77
+ if (isWritePathForbidden(projectRoot, stageflowHome, resolved))
78
+ return target;
79
+ }
80
+ return undefined;
81
+ }
82
+ export function trustedLocalNotice() {
83
+ return "This agent runs in trusted-local mode on the host filesystem. Use sandbox execution for isolation.";
84
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Where agents live when no project is named: the Host's own project, or the workspace catalog
3
+ * when that directory is not writable (a container runs from a read-only install dir).
4
+ */
5
+ export declare function defaultAgentProjectRoot(cwd: string): string;
@@ -0,0 +1,15 @@
1
+ import { accessSync, constants } from "node:fs";
2
+ import { workspaceCatalogPath } from "../project/workspaceCatalog.js";
3
+ /**
4
+ * Where agents live when no project is named: the Host's own project, or the workspace catalog
5
+ * when that directory is not writable (a container runs from a read-only install dir).
6
+ */
7
+ export function defaultAgentProjectRoot(cwd) {
8
+ try {
9
+ accessSync(cwd, constants.W_OK);
10
+ return cwd;
11
+ }
12
+ catch {
13
+ return workspaceCatalogPath();
14
+ }
15
+ }