@mono-agent/agent-app 0.19.1 → 0.20.1

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 (317) hide show
  1. package/README.md +307 -32
  2. package/dist/acp-bridge.d.ts.map +1 -1
  3. package/dist/acp-bridge.js +113 -54
  4. package/dist/acp-bridge.js.map +1 -1
  5. package/dist/acp-session-store.d.ts +12 -0
  6. package/dist/acp-session-store.d.ts.map +1 -0
  7. package/dist/acp-session-store.js +74 -0
  8. package/dist/acp-session-store.js.map +1 -0
  9. package/dist/agent-artifact-paths.d.ts +8 -0
  10. package/dist/agent-artifact-paths.d.ts.map +1 -0
  11. package/dist/agent-artifact-paths.js +11 -0
  12. package/dist/agent-artifact-paths.js.map +1 -0
  13. package/dist/agent-root-coordinator.d.ts +101 -0
  14. package/dist/agent-root-coordinator.d.ts.map +1 -0
  15. package/dist/agent-root-coordinator.js +470 -0
  16. package/dist/agent-root-coordinator.js.map +1 -0
  17. package/dist/app-controller-channels.d.ts +13 -1
  18. package/dist/app-controller-channels.d.ts.map +1 -1
  19. package/dist/app-controller-channels.js +152 -20
  20. package/dist/app-controller-channels.js.map +1 -1
  21. package/dist/app-controller-continuation.d.ts +2 -0
  22. package/dist/app-controller-continuation.d.ts.map +1 -1
  23. package/dist/app-controller-continuation.js.map +1 -1
  24. package/dist/app-controller-lifecycle.d.ts +5 -0
  25. package/dist/app-controller-lifecycle.d.ts.map +1 -1
  26. package/dist/app-controller-lifecycle.js +24 -5
  27. package/dist/app-controller-lifecycle.js.map +1 -1
  28. package/dist/app-controller-maintenance.d.ts +4 -0
  29. package/dist/app-controller-maintenance.d.ts.map +1 -1
  30. package/dist/app-controller-maintenance.js +34 -2
  31. package/dist/app-controller-maintenance.js.map +1 -1
  32. package/dist/app-controller-memory.d.ts +2 -0
  33. package/dist/app-controller-memory.d.ts.map +1 -1
  34. package/dist/app-controller-memory.js +6 -3
  35. package/dist/app-controller-memory.js.map +1 -1
  36. package/dist/app-controller-process-jobs.d.ts +49 -0
  37. package/dist/app-controller-process-jobs.d.ts.map +1 -0
  38. package/dist/app-controller-process-jobs.js +294 -0
  39. package/dist/app-controller-process-jobs.js.map +1 -0
  40. package/dist/app-controller-responder.d.ts +17 -0
  41. package/dist/app-controller-responder.d.ts.map +1 -1
  42. package/dist/app-controller-responder.js +164 -13
  43. package/dist/app-controller-responder.js.map +1 -1
  44. package/dist/app-controller-traceability.d.ts +8 -0
  45. package/dist/app-controller-traceability.d.ts.map +1 -1
  46. package/dist/app-controller-traceability.js +22 -1
  47. package/dist/app-controller-traceability.js.map +1 -1
  48. package/dist/app-controller-types.d.ts +1 -1
  49. package/dist/app-controller-types.d.ts.map +1 -1
  50. package/dist/app-controller-utils.d.ts +8 -0
  51. package/dist/app-controller-utils.d.ts.map +1 -1
  52. package/dist/app-controller-utils.js +24 -1
  53. package/dist/app-controller-utils.js.map +1 -1
  54. package/dist/app-controller.d.ts +45 -1
  55. package/dist/app-controller.d.ts.map +1 -1
  56. package/dist/app-controller.js +135 -39
  57. package/dist/app-controller.js.map +1 -1
  58. package/dist/background-log-maintenance.d.ts +85 -2
  59. package/dist/background-log-maintenance.d.ts.map +1 -1
  60. package/dist/background-log-maintenance.js +407 -136
  61. package/dist/background-log-maintenance.js.map +1 -1
  62. package/dist/background-runtime.d.ts.map +1 -1
  63. package/dist/background-runtime.js +22 -41
  64. package/dist/background-runtime.js.map +1 -1
  65. package/dist/background.d.ts +18 -26
  66. package/dist/background.d.ts.map +1 -1
  67. package/dist/background.js +105 -138
  68. package/dist/background.js.map +1 -1
  69. package/dist/channel-drivers/cron.d.ts +12 -1
  70. package/dist/channel-drivers/cron.d.ts.map +1 -1
  71. package/dist/channel-drivers/cron.js +121 -18
  72. package/dist/channel-drivers/cron.js.map +1 -1
  73. package/dist/channel-drivers/native-notify.d.ts +3 -0
  74. package/dist/channel-drivers/native-notify.d.ts.map +1 -1
  75. package/dist/channel-drivers/native-notify.js +2 -1
  76. package/dist/channel-drivers/native-notify.js.map +1 -1
  77. package/dist/channel-drivers/slack.d.ts.map +1 -1
  78. package/dist/channel-drivers/slack.js +41 -4
  79. package/dist/channel-drivers/slack.js.map +1 -1
  80. package/dist/channel-drivers/telegram.d.ts.map +1 -1
  81. package/dist/channel-drivers/telegram.js +46 -3
  82. package/dist/channel-drivers/telegram.js.map +1 -1
  83. package/dist/channel-drivers/tui.d.ts +13 -2
  84. package/dist/channel-drivers/tui.d.ts.map +1 -1
  85. package/dist/channel-drivers/tui.js +112 -3
  86. package/dist/channel-drivers/tui.js.map +1 -1
  87. package/dist/channels.d.ts +1 -1
  88. package/dist/channels.d.ts.map +1 -1
  89. package/dist/channels.js +4 -2
  90. package/dist/channels.js.map +1 -1
  91. package/dist/cli-args.d.ts +15 -1
  92. package/dist/cli-args.d.ts.map +1 -1
  93. package/dist/cli-args.js +82 -12
  94. package/dist/cli-args.js.map +1 -1
  95. package/dist/cli-background-command.d.ts +9 -2
  96. package/dist/cli-background-command.d.ts.map +1 -1
  97. package/dist/cli-background-command.js +179 -66
  98. package/dist/cli-background-command.js.map +1 -1
  99. package/dist/cli-commands.d.ts.map +1 -1
  100. package/dist/cli-commands.js +50 -12
  101. package/dist/cli-commands.js.map +1 -1
  102. package/dist/cli-help.d.ts.map +1 -1
  103. package/dist/cli-help.js +21 -1
  104. package/dist/cli-help.js.map +1 -1
  105. package/dist/config-reference.d.ts.map +1 -1
  106. package/dist/config-reference.js +105 -2
  107. package/dist/config-reference.js.map +1 -1
  108. package/dist/configured-agent.d.ts +64 -21
  109. package/dist/configured-agent.d.ts.map +1 -1
  110. package/dist/configured-agent.js +840 -156
  111. package/dist/configured-agent.js.map +1 -1
  112. package/dist/conversation-state-roots.d.ts +59 -0
  113. package/dist/conversation-state-roots.d.ts.map +1 -0
  114. package/dist/conversation-state-roots.js +248 -0
  115. package/dist/conversation-state-roots.js.map +1 -0
  116. package/dist/cron-control-store.d.ts +91 -0
  117. package/dist/cron-control-store.d.ts.map +1 -0
  118. package/dist/cron-control-store.js +1251 -0
  119. package/dist/cron-control-store.js.map +1 -0
  120. package/dist/cron-operator-service.d.ts +54 -0
  121. package/dist/cron-operator-service.d.ts.map +1 -0
  122. package/dist/cron-operator-service.js +352 -0
  123. package/dist/cron-operator-service.js.map +1 -0
  124. package/dist/doctor-session-history.d.ts +9 -0
  125. package/dist/doctor-session-history.d.ts.map +1 -0
  126. package/dist/doctor-session-history.js +313 -0
  127. package/dist/doctor-session-history.js.map +1 -0
  128. package/dist/doctor.d.ts +2 -1
  129. package/dist/doctor.d.ts.map +1 -1
  130. package/dist/doctor.js +356 -9
  131. package/dist/doctor.js.map +1 -1
  132. package/dist/index.d.ts +4 -0
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +2 -0
  135. package/dist/index.js.map +1 -1
  136. package/dist/interaction-bridge.d.ts +1 -0
  137. package/dist/interaction-bridge.d.ts.map +1 -1
  138. package/dist/interaction-bridge.js +54 -6
  139. package/dist/interaction-bridge.js.map +1 -1
  140. package/dist/jobs-command.d.ts +17 -0
  141. package/dist/jobs-command.d.ts.map +1 -0
  142. package/dist/jobs-command.js +207 -0
  143. package/dist/jobs-command.js.map +1 -0
  144. package/dist/launchd-lifecycle-lock.d.ts +36 -0
  145. package/dist/launchd-lifecycle-lock.d.ts.map +1 -0
  146. package/dist/launchd-lifecycle-lock.js +124 -0
  147. package/dist/launchd-lifecycle-lock.js.map +1 -0
  148. package/dist/launchd-log-monitor-status.d.ts +7 -0
  149. package/dist/launchd-log-monitor-status.d.ts.map +1 -0
  150. package/dist/launchd-log-monitor-status.js +224 -0
  151. package/dist/launchd-log-monitor-status.js.map +1 -0
  152. package/dist/launchd-logs.d.ts +9 -1
  153. package/dist/launchd-logs.d.ts.map +1 -1
  154. package/dist/launchd-logs.js +69 -16
  155. package/dist/launchd-logs.js.map +1 -1
  156. package/dist/launchd-maintenance-command.d.ts +10 -0
  157. package/dist/launchd-maintenance-command.d.ts.map +1 -0
  158. package/dist/launchd-maintenance-command.js +2 -0
  159. package/dist/launchd-maintenance-command.js.map +1 -0
  160. package/dist/launchd-maintenance-entry.d.ts +30 -0
  161. package/dist/launchd-maintenance-entry.d.ts.map +1 -0
  162. package/dist/launchd-maintenance-entry.js +206 -0
  163. package/dist/launchd-maintenance-entry.js.map +1 -0
  164. package/dist/launchd-maintenance-gate.d.ts +28 -0
  165. package/dist/launchd-maintenance-gate.d.ts.map +1 -0
  166. package/dist/launchd-maintenance-gate.js +80 -0
  167. package/dist/launchd-maintenance-gate.js.map +1 -0
  168. package/dist/launchd-private-files.d.ts +3 -0
  169. package/dist/launchd-private-files.d.ts.map +1 -0
  170. package/dist/launchd-private-files.js +58 -0
  171. package/dist/launchd-private-files.js.map +1 -0
  172. package/dist/launchd.d.ts +56 -2
  173. package/dist/launchd.d.ts.map +1 -1
  174. package/dist/launchd.js +210 -5
  175. package/dist/launchd.js.map +1 -1
  176. package/dist/local-configuration.d.ts.map +1 -1
  177. package/dist/local-configuration.js +34 -7
  178. package/dist/local-configuration.js.map +1 -1
  179. package/dist/managed-launchd-maintenance-environment.d.ts +3 -0
  180. package/dist/managed-launchd-maintenance-environment.d.ts.map +1 -0
  181. package/dist/managed-launchd-maintenance-environment.js +12 -0
  182. package/dist/managed-launchd-maintenance-environment.js.map +1 -0
  183. package/dist/managed-runtime-launch-proof.d.ts +11 -0
  184. package/dist/managed-runtime-launch-proof.d.ts.map +1 -0
  185. package/dist/managed-runtime-launch-proof.js +59 -0
  186. package/dist/managed-runtime-launch-proof.js.map +1 -0
  187. package/dist/managed-runtime-maintenance-entry.d.ts +10 -0
  188. package/dist/managed-runtime-maintenance-entry.d.ts.map +1 -0
  189. package/dist/managed-runtime-maintenance-entry.js +244 -0
  190. package/dist/managed-runtime-maintenance-entry.js.map +1 -0
  191. package/dist/managed-web-logs.d.ts +29 -11
  192. package/dist/managed-web-logs.d.ts.map +1 -1
  193. package/dist/managed-web-logs.js +244 -121
  194. package/dist/managed-web-logs.js.map +1 -1
  195. package/dist/managed-web-maintenance-environment.d.ts +6 -0
  196. package/dist/managed-web-maintenance-environment.d.ts.map +1 -0
  197. package/dist/managed-web-maintenance-environment.js +19 -0
  198. package/dist/managed-web-maintenance-environment.js.map +1 -0
  199. package/dist/mcp-apps.d.ts +37 -0
  200. package/dist/mcp-apps.d.ts.map +1 -0
  201. package/dist/mcp-apps.js +1753 -0
  202. package/dist/mcp-apps.js.map +1 -0
  203. package/dist/memory-command.d.ts +12 -0
  204. package/dist/memory-command.d.ts.map +1 -1
  205. package/dist/memory-command.js +510 -1
  206. package/dist/memory-command.js.map +1 -1
  207. package/dist/modules/known-tools.d.ts +1 -1
  208. package/dist/modules/known-tools.d.ts.map +1 -1
  209. package/dist/modules/known-tools.js +2 -0
  210. package/dist/modules/known-tools.js.map +1 -1
  211. package/dist/posted-reply-history.d.ts.map +1 -1
  212. package/dist/posted-reply-history.js +12 -0
  213. package/dist/posted-reply-history.js.map +1 -1
  214. package/dist/proactive-notify.d.ts +4 -1
  215. package/dist/proactive-notify.d.ts.map +1 -1
  216. package/dist/proactive-notify.js +43 -5
  217. package/dist/proactive-notify.js.map +1 -1
  218. package/dist/process-jobs-config.d.ts +84 -0
  219. package/dist/process-jobs-config.d.ts.map +1 -0
  220. package/dist/process-jobs-config.js +427 -0
  221. package/dist/process-jobs-config.js.map +1 -0
  222. package/dist/process-jobs-context.d.ts +24 -0
  223. package/dist/process-jobs-context.d.ts.map +1 -0
  224. package/dist/process-jobs-context.js +135 -0
  225. package/dist/process-jobs-context.js.map +1 -0
  226. package/dist/process-jobs-protection.d.ts +33 -0
  227. package/dist/process-jobs-protection.d.ts.map +1 -0
  228. package/dist/process-jobs-protection.js +74 -0
  229. package/dist/process-jobs-protection.js.map +1 -0
  230. package/dist/process-jobs-root-registry.d.ts +115 -0
  231. package/dist/process-jobs-root-registry.d.ts.map +1 -0
  232. package/dist/process-jobs-root-registry.js +978 -0
  233. package/dist/process-jobs-root-registry.js.map +1 -0
  234. package/dist/process-jobs-runtime.d.ts +38 -0
  235. package/dist/process-jobs-runtime.d.ts.map +1 -0
  236. package/dist/process-jobs-runtime.js +178 -0
  237. package/dist/process-jobs-runtime.js.map +1 -0
  238. package/dist/process-jobs-service.d.ts +70 -0
  239. package/dist/process-jobs-service.d.ts.map +1 -0
  240. package/dist/process-jobs-service.js +1989 -0
  241. package/dist/process-jobs-service.js.map +1 -0
  242. package/dist/process-jobs-store.d.ts +151 -0
  243. package/dist/process-jobs-store.d.ts.map +1 -0
  244. package/dist/process-jobs-store.js +1290 -0
  245. package/dist/process-jobs-store.js.map +1 -0
  246. package/dist/reply-artifacts.d.ts +65 -0
  247. package/dist/reply-artifacts.d.ts.map +1 -0
  248. package/dist/reply-artifacts.js +1147 -0
  249. package/dist/reply-artifacts.js.map +1 -0
  250. package/dist/reply-part-budget.d.ts +16 -0
  251. package/dist/reply-part-budget.d.ts.map +1 -0
  252. package/dist/reply-part-budget.js +57 -0
  253. package/dist/reply-part-budget.js.map +1 -0
  254. package/dist/request-model-override.d.ts +19 -0
  255. package/dist/request-model-override.d.ts.map +1 -1
  256. package/dist/request-model-override.js +54 -1
  257. package/dist/request-model-override.js.map +1 -1
  258. package/dist/request-scoped-mcp.d.ts +31 -0
  259. package/dist/request-scoped-mcp.d.ts.map +1 -0
  260. package/dist/request-scoped-mcp.js +197 -0
  261. package/dist/request-scoped-mcp.js.map +1 -0
  262. package/dist/run-history.d.ts.map +1 -1
  263. package/dist/run-history.js +50 -254
  264. package/dist/run-history.js.map +1 -1
  265. package/dist/runtime-option-extensions.d.ts +23 -0
  266. package/dist/runtime-option-extensions.d.ts.map +1 -1
  267. package/dist/runtime-option-extensions.js +55 -0
  268. package/dist/runtime-option-extensions.js.map +1 -1
  269. package/dist/runtime-routes.d.ts +7 -1
  270. package/dist/runtime-routes.d.ts.map +1 -1
  271. package/dist/runtime-routes.js +25 -0
  272. package/dist/runtime-routes.js.map +1 -1
  273. package/dist/secure-file-replace.d.ts +3 -0
  274. package/dist/secure-file-replace.d.ts.map +1 -1
  275. package/dist/secure-file-replace.js +31 -7
  276. package/dist/secure-file-replace.js.map +1 -1
  277. package/dist/session-history.d.ts +146 -0
  278. package/dist/session-history.d.ts.map +1 -0
  279. package/dist/session-history.js +347 -0
  280. package/dist/session-history.js.map +1 -0
  281. package/dist/sessions.d.ts +45 -2
  282. package/dist/sessions.d.ts.map +1 -1
  283. package/dist/sessions.js +849 -30
  284. package/dist/sessions.js.map +1 -1
  285. package/dist/web-command.d.ts +12 -11
  286. package/dist/web-command.d.ts.map +1 -1
  287. package/dist/web-command.js +554 -80
  288. package/dist/web-command.js.map +1 -1
  289. package/dist/web-log-maintenance-command.d.ts +7 -0
  290. package/dist/web-log-maintenance-command.d.ts.map +1 -0
  291. package/dist/web-log-maintenance-command.js +21 -0
  292. package/dist/web-log-maintenance-command.js.map +1 -0
  293. package/dist/web-log-maintenance-status.d.ts +26 -0
  294. package/dist/web-log-maintenance-status.d.ts.map +1 -0
  295. package/dist/web-log-maintenance-status.js +171 -0
  296. package/dist/web-log-maintenance-status.js.map +1 -0
  297. package/dist/web-log-maintenance.d.ts +32 -0
  298. package/dist/web-log-maintenance.d.ts.map +1 -0
  299. package/dist/web-log-maintenance.js +163 -0
  300. package/dist/web-log-maintenance.js.map +1 -0
  301. package/dist/web-maintenance-paths.d.ts +12 -0
  302. package/dist/web-maintenance-paths.d.ts.map +1 -0
  303. package/dist/web-maintenance-paths.js +24 -0
  304. package/dist/web-maintenance-paths.js.map +1 -0
  305. package/dist/wizard/prompts.d.ts.map +1 -1
  306. package/dist/wizard/prompts.js +1 -0
  307. package/dist/wizard/prompts.js.map +1 -1
  308. package/dist/wizard/run.js +1 -1
  309. package/dist/wizard/run.js.map +1 -1
  310. package/package.json +15 -15
  311. package/schema/mono-agent.config.schema.json +106 -1
  312. package/skills/mono-agent-composer/references/config-blueprint.md +27 -2
  313. package/skills/mono-agent-composer/references/discovery-questions.md +2 -2
  314. package/skills/mono-agent-composer/references/feature-coverage.md +6 -4
  315. package/skills/mono-agent-composer/references/package-map.md +19 -3
  316. package/skills/mono-agent-composer/references/playbooks.md +3 -3
  317. package/skills/mono-agent-composer/references/validation.md +4 -2
@@ -0,0 +1,1147 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { close as closeFd, constants, fstat, openSync, read as readFd, } from "node:fs";
3
+ import { lstat, open as openFile, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
4
+ import { createServer } from "node:http";
5
+ import { basename, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { AsyncLocalStorage } from "node:async_hooks";
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
9
+ import { CodedError, DEFAULT_AGENT_ATTACHMENT_MAX_BYTES, } from "@mono-agent/agent-contracts";
10
+ import * as z from "zod/v4";
11
+ import { agentArtifactDerivedRoots } from "./agent-artifact-paths.js";
12
+ import { createReplyPartBudget, mergeReplyParts, } from "./reply-part-budget.js";
13
+ const PRIVATE_CAPABILITY_URL = Symbol.for("@mono-agent/private-capability-url");
14
+ export const REPLY_ARTIFACT_MCP_SERVER_NAME = "mono-agent-reply-artifacts";
15
+ export const PUBLISH_REPLY_FILE_TOOL_NAME = "PublishReplyFile";
16
+ const PUBLISH_REPLY_FILE_ALIASES = [
17
+ PUBLISH_REPLY_FILE_TOOL_NAME,
18
+ "publish_reply_file",
19
+ `mcp__${REPLY_ARTIFACT_MCP_SERVER_NAME}__${PUBLISH_REPLY_FILE_TOOL_NAME}`,
20
+ `mcp__${REPLY_ARTIFACT_MCP_SERVER_NAME}__*`,
21
+ ];
22
+ const ARTIFACT_ID = /^[0-9a-f]{8}-[0-9a-f-]{27,35}$/u;
23
+ const MEDIA_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$/u;
24
+ const MAX_DISPLAY_NAME_BYTES = 240;
25
+ const MAX_MANIFEST_BYTES = 16 * 1024;
26
+ const DEFAULT_RETENTION_DAYS = 30;
27
+ const READ_CHUNK_BYTES = 64 * 1024;
28
+ const STAGING_NAMESPACE = ".staging";
29
+ const DEFAULT_STAGING_GRACE_MS = 10 * 60 * 1000;
30
+ export const DEFAULT_REPLY_ARTIFACT_STORAGE_MAX_BYTES = 256 * 1024 * 1024;
31
+ const REPLY_ARTIFACT_STORAGE_NAMESPACES = ["reply-files", "mcp-apps"];
32
+ const BIDI_CONTROL = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/gu;
33
+ const DEFAULT_IGNORABLE = /\p{Default_Ignorable_Code_Point}/gu;
34
+ const SENSITIVE_FILE_EXTENSIONS = [
35
+ ".cer",
36
+ ".crt",
37
+ ".csr",
38
+ ".db",
39
+ ".db-journal",
40
+ ".db-shm",
41
+ ".db-wal",
42
+ ".der",
43
+ ".dirty.json",
44
+ ".env",
45
+ ".history.json",
46
+ ".jks",
47
+ ".kdb",
48
+ ".kdbx",
49
+ ".key",
50
+ ".keystore",
51
+ ".mobileprovision",
52
+ ".ovpn",
53
+ ".p12",
54
+ ".p8",
55
+ ".pem",
56
+ ".pfx",
57
+ ".pkcs12",
58
+ ".pkcs8",
59
+ ".ppk",
60
+ ".sqlite",
61
+ ".sqlite-journal",
62
+ ".sqlite-shm",
63
+ ".sqlite-wal",
64
+ ".sqlite3",
65
+ ".sqlite3-journal",
66
+ ".sqlite3-shm",
67
+ ".sqlite3-wal",
68
+ ];
69
+ const SENSITIVE_NAME_SEGMENTS = new Set([
70
+ "apikey",
71
+ "auth",
72
+ "authorization",
73
+ "cert",
74
+ "certificate",
75
+ "certificates",
76
+ "credential",
77
+ "credentials",
78
+ "keystore",
79
+ "keyring",
80
+ "oauth",
81
+ "oauth1",
82
+ "oauth2",
83
+ "secret",
84
+ "secrets",
85
+ "token",
86
+ "tokens",
87
+ "truststore",
88
+ ]);
89
+ // Darwin's O_NOFOLLOW_ANY rejects symlinks in every path component. Node does
90
+ // not currently expose the constant, so use the stable fcntl.h value only on
91
+ // Darwin and retain post-open identity verification everywhere else.
92
+ const DARWIN_O_NOFOLLOW_ANY = 0x20000000;
93
+ const PUBLISH_INPUT = z.object({
94
+ path: z.string().min(1),
95
+ name: z.string().optional(),
96
+ mediaType: z.string().optional(),
97
+ }).strict();
98
+ const replyArtifactStorageStates = new Map();
99
+ /**
100
+ * One process-wide coordinator per artifact root. Every admission re-inventories
101
+ * both durable rich-reply namespaces, so a fresh process accounts for content
102
+ * left by a previous one before accepting any new bytes.
103
+ */
104
+ export function replyArtifactStorageBudgetFor(artifactDir, maxBytes = DEFAULT_REPLY_ARTIFACT_STORAGE_MAX_BYTES) {
105
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
106
+ throw new RangeError("reply artifact storage maxBytes must be a positive safe integer.");
107
+ }
108
+ const canonicalArtifactDir = resolve(artifactDir);
109
+ const stateKey = `${canonicalArtifactDir}\0${String(maxBytes)}`;
110
+ const state = replyArtifactStorageStates.get(stateKey) ?? {
111
+ artifactDir: canonicalArtifactDir,
112
+ maxBytes,
113
+ protections: new Map(),
114
+ reservedBytes: 0,
115
+ gate: Promise.resolve(),
116
+ };
117
+ replyArtifactStorageStates.set(stateKey, state);
118
+ return {
119
+ maxBytes,
120
+ async reserve(maximumBytes) {
121
+ if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 0) {
122
+ throw new RangeError("reply artifact storage reservation must be a non-negative safe integer.");
123
+ }
124
+ return await runStorageExclusive(state, async () => {
125
+ const storedBytes = await inventoryReplyArtifactBytes(state.artifactDir);
126
+ if (maximumBytes > state.maxBytes
127
+ || storedBytes > state.maxBytes - state.reservedBytes - maximumBytes)
128
+ return undefined;
129
+ state.reservedBytes += maximumBytes;
130
+ let released = false;
131
+ return {
132
+ release: async () => {
133
+ if (released)
134
+ return;
135
+ released = true;
136
+ await runStorageExclusive(state, async () => {
137
+ state.reservedBytes = Math.max(0, state.reservedBytes - maximumBytes);
138
+ });
139
+ },
140
+ };
141
+ });
142
+ },
143
+ async protect(namespace, id) {
144
+ const key = storageProtectionKey(namespace, id);
145
+ await runStorageExclusive(state, async () => {
146
+ state.protections.set(key, (state.protections.get(key) ?? 0) + 1);
147
+ });
148
+ let released = false;
149
+ return {
150
+ release: async () => {
151
+ if (released)
152
+ return;
153
+ released = true;
154
+ await runStorageExclusive(state, async () => {
155
+ const count = state.protections.get(key) ?? 0;
156
+ if (count <= 1)
157
+ state.protections.delete(key);
158
+ else
159
+ state.protections.set(key, count - 1);
160
+ });
161
+ },
162
+ };
163
+ },
164
+ async runExclusive(operation) {
165
+ return await runStorageExclusive(state, async () => await operation((namespace, id) => state.protections.has(storageProtectionKey(namespace, id))));
166
+ },
167
+ };
168
+ }
169
+ export class ReplyArtifactStorageFullError extends Error {
170
+ constructor() {
171
+ super("The aggregate reply artifact storage ceiling was reached.");
172
+ this.name = "ReplyArtifactStorageFullError";
173
+ }
174
+ }
175
+ /** Resolve the same policy aliases accepted by other app-owned MCP tools. */
176
+ export function isPublishReplyFileToolAllowed(policy) {
177
+ const allowed = policy?.allowedTools ?? [];
178
+ const denied = policy?.disallowedTools ?? [];
179
+ if (denied.includes("*") || PUBLISH_REPLY_FILE_ALIASES.some((name) => denied.includes(name)))
180
+ return false;
181
+ return allowed.includes("*") || PUBLISH_REPLY_FILE_ALIASES.some((name) => allowed.includes(name));
182
+ }
183
+ /**
184
+ * Durable generated-file publisher. Its normal reply surface contains only
185
+ * metadata and opaque ids; bytes stay below the private artifact root.
186
+ */
187
+ export function createReplyArtifactService(options) {
188
+ const artifactDir = resolve(options.artifactDir);
189
+ const root = resolve(artifactDir, "reply-files");
190
+ const stagingRoot = join(root, STAGING_NAMESPACE);
191
+ const workspace = resolve(options.workspace);
192
+ const outboundRoot = agentArtifactDerivedRoots(artifactDir).outbound;
193
+ const privateRoots = [
194
+ artifactDir,
195
+ resolve(workspace, ".mono-agent"),
196
+ ...(options.privateRoots ?? []).map((path) => resolve(path)),
197
+ ];
198
+ const maxFileBytes = options.maxFileBytes ?? DEFAULT_AGENT_ATTACHMENT_MAX_BYTES;
199
+ const retentionDays = options.retentionDays ?? DEFAULT_RETENTION_DAYS;
200
+ const stagingGraceMs = options.stagingGraceMs ?? DEFAULT_STAGING_GRACE_MS;
201
+ const now = options.now ?? (() => new Date());
202
+ const budget = options.replyPartBudget ?? createReplyPartBudget();
203
+ const storage = options.storageBudget ?? replyArtifactStorageBudgetFor(artifactDir);
204
+ const partsByRun = new Map();
205
+ const partsByIdentityByRun = new Map();
206
+ const artifactIdsByRun = new Map();
207
+ const artifactProtectionsByRun = new Map();
208
+ const responseContext = new AsyncLocalStorage();
209
+ const runOwners = new Map();
210
+ if (!Number.isSafeInteger(maxFileBytes) || maxFileBytes < 1) {
211
+ throw new RangeError("reply artifact maxFileBytes must be a positive safe integer.");
212
+ }
213
+ if (!Number.isFinite(retentionDays) || retentionDays <= 0) {
214
+ throw new RangeError("reply artifact retentionDays must be positive.");
215
+ }
216
+ if (!Number.isSafeInteger(stagingGraceMs) || stagingGraceMs < 1) {
217
+ throw new RangeError("reply artifact stagingGraceMs must be a positive safe integer.");
218
+ }
219
+ const capacityFailure = (runId) => ({
220
+ type: "failure",
221
+ id: stablePartId("reply-files-limit", runId),
222
+ code: "artifact_publish_failed",
223
+ message: "This reply already reached the maximum number of rich parts.",
224
+ });
225
+ const recordPart = (runId, identity, part) => {
226
+ const claim = budget.claim(runId, identity);
227
+ if (claim === "limit")
228
+ return { part: capacityFailure(runId), accepted: false };
229
+ const byIdentity = partsByIdentityByRun.get(runId) ?? new Map();
230
+ if (claim === "duplicate") {
231
+ return { part: byIdentity.get(identity) ?? capacityFailure(runId), accepted: false };
232
+ }
233
+ const current = partsByRun.get(runId) ?? [];
234
+ current.push(part);
235
+ byIdentity.set(identity, part);
236
+ partsByRun.set(runId, current);
237
+ partsByIdentityByRun.set(runId, byIdentity);
238
+ return { part, accepted: true };
239
+ };
240
+ const publish = async (binding, input) => {
241
+ let claimedIdentity;
242
+ try {
243
+ const source = await openAuthorizedSource(input.path, workspace, runOutboundRoot(outboundRoot, binding.runId), privateRoots, options.beforeSourceOpen);
244
+ try {
245
+ const sourceStat = await source.stat();
246
+ if (!sourceStat.isFile())
247
+ throw new CodedError("artifact_publish_failed", "Only regular files can be published.");
248
+ if (sourceStat.size > maxFileBytes) {
249
+ throw new CodedError("artifact_too_large", `Generated files may not exceed ${maxFileBytes} bytes.`);
250
+ }
251
+ const storageReservation = await storage.reserve(sourceStat.size + MAX_MANIFEST_BYTES);
252
+ if (storageReservation === undefined)
253
+ throw new ReplyArtifactStorageFullError();
254
+ const id = randomUUID();
255
+ const directory = join(root, id);
256
+ const stagingDirectory = join(stagingRoot, id);
257
+ const contentPath = join(stagingDirectory, "content");
258
+ const storageProtection = await storage.protect("reply-files", id);
259
+ let published = false;
260
+ try {
261
+ await mkdir(stagingRoot, { recursive: true, mode: 0o700 });
262
+ await mkdir(stagingDirectory, { recursive: false, mode: 0o700 });
263
+ const target = await openFile(contentPath, "wx", 0o600);
264
+ const hash = createHash("sha256");
265
+ let copied = 0;
266
+ try {
267
+ const buffer = Buffer.allocUnsafe(Math.min(READ_CHUNK_BYTES, maxFileBytes));
268
+ for (;;) {
269
+ const { bytesRead } = await source.read(buffer, 0, buffer.byteLength, copied);
270
+ if (bytesRead === 0)
271
+ break;
272
+ copied += bytesRead;
273
+ if (copied > maxFileBytes) {
274
+ throw new CodedError("artifact_too_large", `Generated files may not exceed ${maxFileBytes} bytes.`);
275
+ }
276
+ const chunk = buffer.subarray(0, bytesRead);
277
+ hash.update(chunk);
278
+ await target.write(chunk);
279
+ }
280
+ await target.sync();
281
+ }
282
+ finally {
283
+ await target.close();
284
+ }
285
+ if (copied !== sourceStat.size) {
286
+ throw new CodedError("artifact_publish_failed", "The generated file changed while it was being published.");
287
+ }
288
+ const finalSourceStat = await source.verify();
289
+ if (!sameSourceSnapshot(sourceStat, finalSourceStat)) {
290
+ throw new CodedError("artifact_publish_failed", "The generated file changed while it was being published.");
291
+ }
292
+ const createdAt = now();
293
+ const manifest = {
294
+ schema: 1,
295
+ id,
296
+ runId: binding.runId,
297
+ conversationId: binding.conversationId,
298
+ name: sanitizeDisplayName(input.name ?? basename(input.path), id),
299
+ mediaType: normalizeMediaType(input.mediaType, input.path),
300
+ sizeBytes: copied,
301
+ integrityId: `sha256:${hash.digest("hex")}`,
302
+ createdAt: createdAt.toISOString(),
303
+ expiresAt: new Date(createdAt.getTime() + retentionDays * 86_400_000).toISOString(),
304
+ };
305
+ const manifestJson = serializeBoundedJsonLine(manifest, MAX_MANIFEST_BYTES);
306
+ await writeFile(join(stagingDirectory, "metadata.json"), manifestJson, {
307
+ encoding: "utf8",
308
+ mode: 0o600,
309
+ flag: "wx",
310
+ });
311
+ await options.beforePublicationCommit?.();
312
+ const identity = `attachment:${manifest.integrityId}`;
313
+ const part = attachmentPart(manifest, id);
314
+ const recorded = recordPart(binding.runId, identity, part);
315
+ if (!recorded.accepted)
316
+ return recorded.part;
317
+ claimedIdentity = identity;
318
+ await rename(stagingDirectory, directory);
319
+ const ids = artifactIdsByRun.get(binding.runId) ?? new Set();
320
+ ids.add(id);
321
+ artifactIdsByRun.set(binding.runId, ids);
322
+ const protections = artifactProtectionsByRun.get(binding.runId)
323
+ ?? new Map();
324
+ protections.set(id, storageProtection);
325
+ artifactProtectionsByRun.set(binding.runId, protections);
326
+ published = true;
327
+ return part;
328
+ }
329
+ finally {
330
+ if (!published)
331
+ await rm(stagingDirectory, { recursive: true, force: true }).catch(() => undefined);
332
+ if (!published)
333
+ await storageProtection.release();
334
+ await storageReservation.release();
335
+ }
336
+ }
337
+ finally {
338
+ await source.close();
339
+ }
340
+ }
341
+ catch (error) {
342
+ if (claimedIdentity !== undefined)
343
+ rollbackRecordedPart(binding.runId, claimedIdentity);
344
+ const code = error instanceof CodedError && error.code === "artifact_too_large"
345
+ ? "artifact_too_large"
346
+ : "artifact_publish_failed";
347
+ const failureIdentity = `attachment-failure:${stableDigest({
348
+ path: input.path,
349
+ name: input.name,
350
+ mediaType: input.mediaType,
351
+ code,
352
+ })}`;
353
+ const storageFull = error instanceof ReplyArtifactStorageFullError;
354
+ const failure = {
355
+ type: "failure",
356
+ id: stablePartId("reply-file-failure", failureIdentity),
357
+ code,
358
+ message: storageFull
359
+ ? "Reply artifact storage is full; this file was not published."
360
+ : code === "artifact_too_large"
361
+ ? errorMessage(error)
362
+ : "The generated file could not be published safely.",
363
+ };
364
+ return recordPart(binding.runId, failureIdentity, failure).part;
365
+ }
366
+ };
367
+ const createExtension = async ({ request, runId }) => {
368
+ await mkdir(stagingRoot, { recursive: true, mode: 0o700 });
369
+ const owner = responseContext.getStore();
370
+ owner?.runIds.add(runId);
371
+ runOwners.set(runId, owner);
372
+ await cleanupExpired().catch(() => undefined);
373
+ const endpointPath = `/mcp/${randomUUID()}`;
374
+ let port;
375
+ const http = createServer((incoming, response) => {
376
+ if (incoming.url !== endpointPath || !isLoopbackHost(incoming.headers.host) || port === undefined) {
377
+ response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
378
+ response.end("Not found");
379
+ return;
380
+ }
381
+ const boundPort = port;
382
+ void (async () => {
383
+ const parsedBody = incoming.method === "POST" ? await readJsonBody(incoming) : undefined;
384
+ const server = createPublishServer(async (input) => {
385
+ return await publish({ runId, conversationId: request.conversationId }, input);
386
+ });
387
+ const transport = new WebStandardStreamableHTTPServerTransport({
388
+ enableJsonResponse: true,
389
+ allowedHosts: [`127.0.0.1:${boundPort}`],
390
+ enableDnsRebindingProtection: true,
391
+ });
392
+ try {
393
+ await server.connect(transport);
394
+ const webResponse = await transport.handleRequest(nodeRequestAsWebRequest(incoming), { parsedBody });
395
+ if (webResponse === undefined)
396
+ throw new Error("Reply artifact MCP transport is unavailable.");
397
+ await writeWebResponse(response, webResponse);
398
+ }
399
+ finally {
400
+ await server.close().catch(() => undefined);
401
+ }
402
+ })().catch(() => {
403
+ if (!response.headersSent)
404
+ response.writeHead(500);
405
+ response.end();
406
+ });
407
+ });
408
+ await listenLoopback(http);
409
+ port = http.address().port;
410
+ let closed = false;
411
+ const serverSpec = privateLoopbackHttpServerSpec(`http://127.0.0.1:${port}${endpointPath}`);
412
+ return {
413
+ runtimeOptions: {
414
+ mcpServers: {
415
+ [REPLY_ARTIFACT_MCP_SERVER_NAME]: serverSpec,
416
+ },
417
+ },
418
+ cleanup: async () => {
419
+ if (closed)
420
+ return;
421
+ closed = true;
422
+ await closeHttpServer(http);
423
+ },
424
+ settleCleanup: async () => {
425
+ if (runOwners.get(runId) === undefined)
426
+ await discardRun(runId);
427
+ },
428
+ };
429
+ };
430
+ async function open(request) {
431
+ const protection = await storage.protect("reply-files", request.reference.id);
432
+ try {
433
+ const manifest = await readManifest(root, request.reference.id);
434
+ if (request.conversationId !== manifest.conversationId
435
+ && request.conversationId !== manifest.deliveryConversationId) {
436
+ throw new CodedError("artifact_forbidden", "The artifact does not belong to this conversation.");
437
+ }
438
+ if (Date.parse(manifest.expiresAt) <= now().getTime()) {
439
+ throw new CodedError("artifact_expired", "The generated file has expired.");
440
+ }
441
+ if (request.expectedIntegrityId !== undefined && request.expectedIntegrityId !== manifest.integrityId) {
442
+ throw new CodedError("artifact_integrity_failed", "The message integrity id does not match the artifact.");
443
+ }
444
+ const contentPath = join(root, manifest.id, "content");
445
+ const handle = await openFile(contentPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
446
+ try {
447
+ const fileStat = await handle.stat();
448
+ if (!fileStat.isFile() || fileStat.size !== manifest.sizeBytes || fileStat.size > maxFileBytes) {
449
+ throw new CodedError("artifact_integrity_failed", "The generated file size no longer matches its manifest.");
450
+ }
451
+ const actualIntegrity = await hashHandle(handle, fileStat.size);
452
+ if (actualIntegrity !== manifest.integrityId) {
453
+ throw new CodedError("artifact_integrity_failed", "The generated file failed its integrity check.");
454
+ }
455
+ const attachment = attachmentPart(manifest, manifest.id);
456
+ return {
457
+ attachment,
458
+ body: streamHandle(handle, fileStat.size, async () => await protection.release()),
459
+ };
460
+ }
461
+ catch (error) {
462
+ await handle.close().catch(() => undefined);
463
+ throw error;
464
+ }
465
+ }
466
+ catch (error) {
467
+ await protection.release();
468
+ throw error;
469
+ }
470
+ }
471
+ async function cleanupExpired() {
472
+ await storage.runExclusive(async (isProtected) => {
473
+ let entries;
474
+ try {
475
+ entries = await readdir(root);
476
+ }
477
+ catch (error) {
478
+ if (error.code === "ENOENT")
479
+ return;
480
+ throw error;
481
+ }
482
+ const cutoff = now().getTime();
483
+ await Promise.all(entries.filter((entry) => ARTIFACT_ID.test(entry)).map(async (entry) => {
484
+ if (isProtected("reply-files", entry))
485
+ return;
486
+ try {
487
+ const manifest = await readManifest(root, entry);
488
+ if (Date.parse(manifest.expiresAt) <= cutoff) {
489
+ await rm(join(root, entry), { recursive: true, force: true });
490
+ }
491
+ }
492
+ catch {
493
+ // Partial/corrupt publications are never served. Remove only directories
494
+ // whose names were minted by this service.
495
+ await rm(join(root, entry), { recursive: true, force: true }).catch(() => undefined);
496
+ }
497
+ }));
498
+ await cleanupStaging(stagingRoot, cutoff - stagingGraceMs, (id) => isProtected("reply-files", id));
499
+ });
500
+ }
501
+ function wrapResponder(responder) {
502
+ return {
503
+ ...responder,
504
+ async respond(request, stream) {
505
+ const context = { runIds: new Set() };
506
+ let retainedRunId;
507
+ try {
508
+ const response = await responseContext.run(context, async () => await responder.respond(request, stream));
509
+ const runId = typeof response.metadata?.runId === "string" ? response.metadata.runId : undefined;
510
+ if (runId === undefined || (context.runIds.size > 0 && !context.runIds.has(runId)))
511
+ return response;
512
+ const published = partsByRun.get(runId) ?? [];
513
+ if (published.length === 0) {
514
+ await retainRun(runId);
515
+ retainedRunId = runId;
516
+ return response;
517
+ }
518
+ const finalized = await finalizeDeliveryParts({ runId, conversationId: request.conversationId }, published);
519
+ const parts = mergeReplyParts(response.parts, finalized);
520
+ await retainRun(runId);
521
+ retainedRunId = runId;
522
+ return { ...response, parts };
523
+ }
524
+ finally {
525
+ await Promise.all([...context.runIds]
526
+ .filter((runId) => runId !== retainedRunId)
527
+ .map(async (runId) => await discardRun(runId)));
528
+ }
529
+ },
530
+ openReplyArtifact: open,
531
+ };
532
+ }
533
+ async function retainRun(runId) {
534
+ const protections = [...(artifactProtectionsByRun.get(runId)?.values() ?? [])];
535
+ partsByRun.delete(runId);
536
+ partsByIdentityByRun.delete(runId);
537
+ artifactIdsByRun.delete(runId);
538
+ artifactProtectionsByRun.delete(runId);
539
+ runOwners.delete(runId);
540
+ budget.release(runId);
541
+ await Promise.all(protections.map(async (protection) => await protection.release()));
542
+ }
543
+ async function discardRun(runId) {
544
+ const ids = [...(artifactIdsByRun.get(runId) ?? [])];
545
+ await retainRun(runId);
546
+ await Promise.all(ids.map(async (id) => {
547
+ await rm(join(root, id), { recursive: true, force: true }).catch(() => undefined);
548
+ }));
549
+ }
550
+ function rollbackRecordedPart(runId, identity) {
551
+ const byIdentity = partsByIdentityByRun.get(runId);
552
+ const part = byIdentity?.get(identity);
553
+ byIdentity?.delete(identity);
554
+ if (byIdentity?.size === 0)
555
+ partsByIdentityByRun.delete(runId);
556
+ if (part !== undefined) {
557
+ const parts = partsByRun.get(runId)?.filter((candidate) => candidate !== part) ?? [];
558
+ if (parts.length === 0)
559
+ partsByRun.delete(runId);
560
+ else
561
+ partsByRun.set(runId, parts);
562
+ }
563
+ budget.unclaim(runId, identity);
564
+ }
565
+ async function finalizeDeliveryParts(binding, parts) {
566
+ const finalized = [];
567
+ for (const part of parts) {
568
+ if (part.type !== "attachment") {
569
+ finalized.push(part);
570
+ continue;
571
+ }
572
+ const protection = await storage.protect("reply-files", part.reference.id);
573
+ try {
574
+ await bindDeliveryConversation(root, part, binding.conversationId, storage);
575
+ finalized.push(part);
576
+ }
577
+ catch {
578
+ const failure = replaceAttachmentWithDeliveryFailure(binding.runId, part);
579
+ artifactIdsByRun.get(binding.runId)?.delete(part.reference.id);
580
+ await rm(join(root, part.reference.id), { recursive: true, force: true }).catch(() => undefined);
581
+ finalized.push(failure);
582
+ }
583
+ finally {
584
+ await protection.release();
585
+ }
586
+ }
587
+ return finalized;
588
+ }
589
+ function replaceAttachmentWithDeliveryFailure(runId, part) {
590
+ const failureIdentity = `attachment-delivery-failure:${part.reference.id}`;
591
+ const failure = {
592
+ type: "failure",
593
+ id: stablePartId("reply-file-delivery-failure", failureIdentity),
594
+ code: "artifact_publish_failed",
595
+ message: "The generated file could not be finalized for delivery.",
596
+ relatedPartId: part.id,
597
+ };
598
+ replaceRecordedPart(runId, part, failureIdentity, failure);
599
+ return failure;
600
+ }
601
+ function replaceRecordedPart(runId, replaced, replacementIdentity, replacement) {
602
+ const byIdentity = partsByIdentityByRun.get(runId) ?? new Map();
603
+ const oldIdentity = [...byIdentity].find(([, part]) => part === replaced)?.[0];
604
+ if (oldIdentity !== undefined) {
605
+ byIdentity.delete(oldIdentity);
606
+ budget.unclaim(runId, oldIdentity);
607
+ }
608
+ if (budget.claim(runId, replacementIdentity) === "accepted") {
609
+ byIdentity.set(replacementIdentity, replacement);
610
+ }
611
+ const current = partsByRun.get(runId) ?? [];
612
+ const index = current.indexOf(replaced);
613
+ if (index >= 0)
614
+ current[index] = replacement;
615
+ partsByRun.set(runId, current);
616
+ partsByIdentityByRun.set(runId, byIdentity);
617
+ }
618
+ return { createExtension, wrapResponder, open, cleanupExpired };
619
+ }
620
+ /**
621
+ * Keep a request capability available to the in-process runtime without making
622
+ * it part of ordinary option serialization or error reporting. Shallow MCP
623
+ * server-map merges preserve the spec object by identity.
624
+ */
625
+ function privateLoopbackHttpServerSpec(url) {
626
+ const spec = { type: "http" };
627
+ Object.defineProperty(spec, "url", { value: url, enumerable: false });
628
+ Object.defineProperty(spec, PRIVATE_CAPABILITY_URL, { value: true, enumerable: false });
629
+ return spec;
630
+ }
631
+ function createPublishServer(publish) {
632
+ const server = new McpServer({ name: REPLY_ARTIFACT_MCP_SERVER_NAME, version: "1.0.0" });
633
+ server.registerTool(PUBLISH_REPLY_FILE_TOOL_NAME, {
634
+ title: "Attach a generated file",
635
+ description: "Publish one generated workspace file with the assistant reply. Call once per file after writing it. The host copies, hashes, retains, and authorizes the file; the response carries only an opaque id and sanitized metadata. Paths outside the workspace or this run's MCP output directory, symlinks, non-files, and files over 20 MiB are rejected.",
636
+ inputSchema: PUBLISH_INPUT,
637
+ }, async (input) => {
638
+ const part = await publish(input);
639
+ if (part.type === "attachment") {
640
+ return {
641
+ content: [{ type: "text", text: `Published ${part.name} (${part.sizeBytes} bytes, ${part.integrityId}). It will be attached to this reply.` }],
642
+ structuredContent: {
643
+ published: true,
644
+ attachmentId: part.reference.id,
645
+ name: part.name,
646
+ mediaType: part.mediaType,
647
+ sizeBytes: part.sizeBytes,
648
+ integrityId: part.integrityId,
649
+ },
650
+ };
651
+ }
652
+ return {
653
+ isError: true,
654
+ content: [{ type: "text", text: part.message }],
655
+ structuredContent: { published: false, code: part.code },
656
+ };
657
+ });
658
+ return server;
659
+ }
660
+ async function openAuthorizedSource(pathInput, workspace, currentRunOutboundRoot, privateRoots, beforeOpen) {
661
+ if (pathInput.includes("\0"))
662
+ throw new CodedError("artifact_publish_failed", "Invalid generated file path.");
663
+ const candidate = resolve(isAbsolute(pathInput) ? pathInput : join(workspace, pathInput));
664
+ const outboundCandidate = currentRunOutboundRoot !== undefined
665
+ && isPathInside(currentRunOutboundRoot, candidate);
666
+ const authorizationRoot = outboundCandidate ? currentRunOutboundRoot : workspace;
667
+ if (!isPathInside(authorizationRoot, candidate)) {
668
+ throw new CodedError("artifact_publish_failed", "Generated file path is outside the authorized roots.");
669
+ }
670
+ assertSafePublicationComponents(authorizationRoot, candidate);
671
+ if (isBlockedByPrivateRoot(candidate, privateRoots, outboundCandidate ? authorizationRoot : undefined)) {
672
+ throw new CodedError("artifact_publish_failed", "Generated file path is private.");
673
+ }
674
+ const canonical = await realpath(candidate);
675
+ const canonicalAuthorizationRoot = await realpath(authorizationRoot).catch(() => authorizationRoot);
676
+ const canonicalPrivateRoots = await Promise.all(privateRoots.map(async (root) => await realpath(root).catch(() => root)));
677
+ if (!isPathInside(canonicalAuthorizationRoot, canonical)) {
678
+ throw new CodedError("artifact_publish_failed", "Generated file path resolves outside the authorized roots.");
679
+ }
680
+ assertSafePublicationComponents(canonicalAuthorizationRoot, canonical);
681
+ if (isBlockedByPrivateRoot(canonical, canonicalPrivateRoots, outboundCandidate ? canonicalAuthorizationRoot : undefined)) {
682
+ throw new CodedError("artifact_publish_failed", "Generated file path resolves into private state.");
683
+ }
684
+ await beforeOpen?.();
685
+ // Darwin rejects combining O_NOFOLLOW and O_NOFOLLOW_ANY; the latter is the
686
+ // stronger flag because it covers the final component as well as parents.
687
+ const flags = constants.O_RDONLY | (process.platform === "darwin"
688
+ ? DARWIN_O_NOFOLLOW_ANY
689
+ : (constants.O_NOFOLLOW ?? 0));
690
+ // Node's promise-based fs.open currently rejects Darwin's O_NOFOLLOW_ANY
691
+ // even though the kernel accepts it. Use the raw numeric flag through the
692
+ // synchronous open syscall (the only synchronous operation is opening one
693
+ // already-resolved path), then keep all copying on the owned descriptor.
694
+ const handle = process.platform === "darwin"
695
+ ? sourceFromFd(openSync(canonical, flags))
696
+ : await openFile(canonical, flags);
697
+ const verify = async () => {
698
+ const [opened, currentPath, currentCanonical] = await Promise.all([
699
+ handle.stat(),
700
+ lstat(canonical),
701
+ realpath(candidate),
702
+ ]);
703
+ if (!opened.isFile()
704
+ || opened.nlink !== 1
705
+ || !currentPath.isFile()
706
+ || currentPath.isSymbolicLink()
707
+ || currentPath.nlink !== 1
708
+ || currentCanonical !== canonical
709
+ || opened.dev !== currentPath.dev
710
+ || opened.ino !== currentPath.ino) {
711
+ throw new CodedError("artifact_publish_failed", "The generated file path changed during authorization.");
712
+ }
713
+ return opened;
714
+ };
715
+ try {
716
+ await verify();
717
+ return {
718
+ stat: async () => await handle.stat(),
719
+ read: async (buffer, offset, length, position) => await handle.read(buffer, offset, length, position),
720
+ verify,
721
+ close: async () => await handle.close(),
722
+ };
723
+ }
724
+ catch (error) {
725
+ await handle.close().catch(() => undefined);
726
+ throw error;
727
+ }
728
+ }
729
+ function sourceFromFd(fd) {
730
+ let closed = false;
731
+ return {
732
+ stat: async () => await new Promise((resolveStat, reject) => {
733
+ fstat(fd, (error, value) => error === null ? resolveStat(value) : reject(error));
734
+ }),
735
+ read: async (buffer, offset, length, position) => await new Promise((resolveRead, reject) => {
736
+ readFd(fd, buffer, offset, length, position, (error, bytesRead) => {
737
+ if (error !== null)
738
+ reject(error);
739
+ else
740
+ resolveRead({ bytesRead });
741
+ });
742
+ }),
743
+ close: async () => {
744
+ if (closed)
745
+ return;
746
+ closed = true;
747
+ await new Promise((resolveClose, reject) => {
748
+ closeFd(fd, (error) => error === null ? resolveClose() : reject(error));
749
+ });
750
+ },
751
+ };
752
+ }
753
+ function isPathInside(root, candidate) {
754
+ const path = relative(root, candidate);
755
+ return path === "" || (!path.startsWith(`..${sep}`) && path !== ".." && !isAbsolute(path));
756
+ }
757
+ function runOutboundRoot(outboundRoot, runId) {
758
+ if (runId.length === 0
759
+ || runId.includes("\0")
760
+ || runId.includes("/")
761
+ || runId.includes("\\")
762
+ || runId === "."
763
+ || runId === "..")
764
+ return undefined;
765
+ const candidate = resolve(outboundRoot, runId);
766
+ return isPathInside(outboundRoot, candidate) && candidate !== outboundRoot ? candidate : undefined;
767
+ }
768
+ function isBlockedByPrivateRoot(candidate, privateRoots, outboundExceptionRoot) {
769
+ return privateRoots.some((privateRoot) => {
770
+ if (!isPathInside(privateRoot, candidate))
771
+ return false;
772
+ return outboundExceptionRoot === undefined || !isPathInside(privateRoot, outboundExceptionRoot);
773
+ });
774
+ }
775
+ function assertSafePublicationComponents(root, candidate) {
776
+ const path = relative(root, candidate);
777
+ for (const component of path.split(/[\\/]/u).filter((value) => value.length > 0)) {
778
+ if (isSensitivePublicationComponent(component)) {
779
+ throw new CodedError("artifact_publish_failed", "Generated file path contains a private component.");
780
+ }
781
+ }
782
+ }
783
+ function isSensitivePublicationComponent(component) {
784
+ const skeleton = wellFormed(component)
785
+ .normalize("NFKD")
786
+ .replace(/\p{Mark}/gu, "")
787
+ .replace(DEFAULT_IGNORABLE, "")
788
+ .toLowerCase();
789
+ if (skeleton.startsWith("."))
790
+ return true;
791
+ if (/^id_[a-z0-9]/u.test(skeleton))
792
+ return true;
793
+ if (SENSITIVE_FILE_EXTENSIONS.some((extension) => skeleton.endsWith(extension)))
794
+ return true;
795
+ if (/^(?:authorized_keys|cacerts|kubeconfig|known_hosts|netrc|npmrc)$/u.test(skeleton))
796
+ return true;
797
+ if (/^mono-agent(?:[._-][a-z0-9-]+)*[._-]config(?:[._-]|$)/u.test(skeleton))
798
+ return true;
799
+ if (/^mcp(?:[._-](?:config|servers?|auth|credentials?))?\.(?:json|jsonc|ya?ml|toml)$/u.test(skeleton)) {
800
+ return true;
801
+ }
802
+ const segments = skeleton.split(/[^a-z0-9]+/u).filter((value) => value.length > 0);
803
+ if (segments.some((segment) => SENSITIVE_NAME_SEGMENTS.has(segment)))
804
+ return true;
805
+ const joined = segments.join("");
806
+ return joined.includes("apikey")
807
+ || joined.includes("clientsecret")
808
+ || joined.includes("keystore")
809
+ || joined.includes("keyring")
810
+ || joined.includes("privatekey")
811
+ || joined.includes("serviceaccount")
812
+ || joined.includes("applicationdefaultcredentials")
813
+ || joined.includes("truststore");
814
+ }
815
+ function sanitizeDisplayName(input, fallbackId) {
816
+ let name = wellFormed(basename(input)).normalize("NFC")
817
+ .replace(BIDI_CONTROL, "")
818
+ .replace(/[\u0000-\u001f\u007f]/gu, "_")
819
+ .replace(/[\\/:]/gu, "_")
820
+ .trim()
821
+ .replace(/^\.+$/u, "");
822
+ if (name.length === 0)
823
+ name = `attachment-${fallbackId.slice(0, 8)}`;
824
+ return truncateUtf8ByCodePoint(name, MAX_DISPLAY_NAME_BYTES);
825
+ }
826
+ function wellFormed(input) {
827
+ let output = "";
828
+ for (const codePoint of input) {
829
+ const unit = codePoint.charCodeAt(0);
830
+ output += codePoint.length === 1 && unit >= 0xd800 && unit <= 0xdfff ? "\ufffd" : codePoint;
831
+ }
832
+ return output;
833
+ }
834
+ function truncateUtf8ByCodePoint(input, maxBytes) {
835
+ let output = "";
836
+ let bytes = 0;
837
+ for (const codePoint of input) {
838
+ const codePointBytes = Buffer.byteLength(codePoint, "utf8");
839
+ if (bytes + codePointBytes > maxBytes)
840
+ break;
841
+ output += codePoint;
842
+ bytes += codePointBytes;
843
+ }
844
+ return output;
845
+ }
846
+ function sameSourceSnapshot(before, after) {
847
+ return before.dev === after.dev
848
+ && before.ino === after.ino
849
+ && before.nlink === 1
850
+ && after.nlink === 1
851
+ && before.size === after.size
852
+ && before.mtimeMs === after.mtimeMs
853
+ && before.ctimeMs === after.ctimeMs;
854
+ }
855
+ function normalizeMediaType(input, path) {
856
+ const normalized = input?.trim().toLowerCase();
857
+ if (normalized !== undefined && MEDIA_TYPE.test(normalized))
858
+ return normalized;
859
+ return {
860
+ ".csv": "text/csv",
861
+ ".json": "application/json",
862
+ ".md": "text/markdown",
863
+ ".pdf": "application/pdf",
864
+ ".png": "image/png",
865
+ ".jpg": "image/jpeg",
866
+ ".jpeg": "image/jpeg",
867
+ ".txt": "text/plain",
868
+ ".zip": "application/zip",
869
+ }[extname(path).toLowerCase()] ?? "application/octet-stream";
870
+ }
871
+ function attachmentPart(manifest, partId) {
872
+ return {
873
+ type: "attachment",
874
+ id: partId,
875
+ reference: { scheme: "mono-agent-artifact", id: manifest.id },
876
+ name: manifest.name,
877
+ mediaType: manifest.mediaType,
878
+ sizeBytes: manifest.sizeBytes,
879
+ integrityId: manifest.integrityId,
880
+ expiresAt: manifest.expiresAt,
881
+ };
882
+ }
883
+ function stableDigest(value) {
884
+ return createHash("sha256").update(JSON.stringify(value) ?? "null").digest("hex");
885
+ }
886
+ function stablePartId(prefix, identity) {
887
+ return `${prefix}-${stableDigest(identity).slice(0, 24)}`;
888
+ }
889
+ async function cleanupStaging(stagingRoot, staleBeforeMs, isProtected) {
890
+ let entries;
891
+ try {
892
+ entries = await readdir(stagingRoot);
893
+ }
894
+ catch (error) {
895
+ if (error.code === "ENOENT")
896
+ return;
897
+ throw error;
898
+ }
899
+ await Promise.all(entries.filter((entry) => ARTIFACT_ID.test(entry) && !isProtected(entry)).map(async (entry) => {
900
+ const path = join(stagingRoot, entry);
901
+ try {
902
+ const entryStat = await lstat(path);
903
+ if (entryStat.mtimeMs > staleBeforeMs)
904
+ return;
905
+ await rm(path, { recursive: true, force: true });
906
+ }
907
+ catch (error) {
908
+ if (error.code !== "ENOENT")
909
+ throw error;
910
+ }
911
+ }));
912
+ }
913
+ async function bindDeliveryConversation(root, part, conversationId, storage) {
914
+ const manifest = await readManifest(root, part.reference.id);
915
+ if (manifest.deliveryConversationId === conversationId)
916
+ return;
917
+ const directory = join(root, manifest.id);
918
+ const temp = join(directory, `metadata.${randomUUID()}.partial`);
919
+ const manifestJson = serializeBoundedJsonLine({ ...manifest, deliveryConversationId: conversationId }, MAX_MANIFEST_BYTES);
920
+ const reservation = await storage.reserve(Buffer.byteLength(manifestJson, "utf8"));
921
+ if (reservation === undefined)
922
+ throw new ReplyArtifactStorageFullError();
923
+ let committed = false;
924
+ try {
925
+ await writeFile(temp, manifestJson, {
926
+ encoding: "utf8",
927
+ mode: 0o600,
928
+ flag: "wx",
929
+ });
930
+ await rename(temp, join(directory, "metadata.json"));
931
+ committed = true;
932
+ }
933
+ finally {
934
+ if (!committed)
935
+ await rm(temp, { force: true }).catch(() => undefined);
936
+ await reservation.release();
937
+ }
938
+ }
939
+ async function readManifest(root, id) {
940
+ if (!ARTIFACT_ID.test(id))
941
+ throw new CodedError("artifact_missing", "The generated file reference is invalid.");
942
+ const path = join(root, id, "metadata.json");
943
+ let data;
944
+ try {
945
+ const fileStat = await stat(path);
946
+ if (!fileStat.isFile() || fileStat.size > MAX_MANIFEST_BYTES)
947
+ throw new Error("invalid manifest");
948
+ data = await readFile(path);
949
+ }
950
+ catch {
951
+ throw new CodedError("artifact_missing", "The generated file is missing.");
952
+ }
953
+ let raw;
954
+ try {
955
+ raw = JSON.parse(data.toString("utf8"));
956
+ }
957
+ catch {
958
+ throw new CodedError("artifact_missing", "The generated file metadata is unavailable.");
959
+ }
960
+ if (!isManifest(raw) || raw.id !== id) {
961
+ throw new CodedError("artifact_missing", "The generated file metadata is invalid.");
962
+ }
963
+ return raw;
964
+ }
965
+ function isManifest(value) {
966
+ if (!isRecord(value))
967
+ return false;
968
+ return value.schema === 1
969
+ && typeof value.id === "string"
970
+ && typeof value.runId === "string"
971
+ && typeof value.conversationId === "string"
972
+ && (value.deliveryConversationId === undefined || typeof value.deliveryConversationId === "string")
973
+ && typeof value.name === "string"
974
+ && typeof value.mediaType === "string"
975
+ && typeof value.sizeBytes === "number"
976
+ && Number.isSafeInteger(value.sizeBytes)
977
+ && value.sizeBytes >= 0
978
+ && typeof value.integrityId === "string"
979
+ && /^sha256:[0-9a-f]{64}$/u.test(value.integrityId)
980
+ && typeof value.createdAt === "string"
981
+ && Number.isFinite(Date.parse(value.createdAt))
982
+ && typeof value.expiresAt === "string"
983
+ && Number.isFinite(Date.parse(value.expiresAt));
984
+ }
985
+ function serializeBoundedJsonLine(value, maxBytes) {
986
+ const serialized = `${JSON.stringify(value) ?? "null"}\n`;
987
+ if (Buffer.byteLength(serialized, "utf8") > maxBytes) {
988
+ throw new RangeError("Reply artifact metadata exceeded its byte limit.");
989
+ }
990
+ return serialized;
991
+ }
992
+ async function hashHandle(handle, size) {
993
+ const hash = createHash("sha256");
994
+ const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
995
+ let offset = 0;
996
+ while (offset < size) {
997
+ const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.byteLength, size - offset), offset);
998
+ if (bytesRead === 0)
999
+ break;
1000
+ hash.update(buffer.subarray(0, bytesRead));
1001
+ offset += bytesRead;
1002
+ }
1003
+ if (offset !== size)
1004
+ throw new CodedError("artifact_integrity_failed", "The generated file could not be read completely.");
1005
+ return `sha256:${hash.digest("hex")}`;
1006
+ }
1007
+ async function* streamHandle(handle, size, releaseProtection) {
1008
+ const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
1009
+ let offset = 0;
1010
+ try {
1011
+ while (offset < size) {
1012
+ const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.byteLength, size - offset), offset);
1013
+ if (bytesRead === 0)
1014
+ throw new CodedError("artifact_interrupted", "The generated file stream ended early.");
1015
+ offset += bytesRead;
1016
+ yield Uint8Array.from(buffer.subarray(0, bytesRead));
1017
+ }
1018
+ }
1019
+ finally {
1020
+ await handle.close().catch(() => undefined);
1021
+ await releaseProtection().catch(() => undefined);
1022
+ }
1023
+ }
1024
+ function errorMessage(error) {
1025
+ return error instanceof Error ? error.message : String(error);
1026
+ }
1027
+ function isRecord(value) {
1028
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1029
+ }
1030
+ function storageProtectionKey(namespace, id) {
1031
+ return `${namespace}:${id}`;
1032
+ }
1033
+ async function runStorageExclusive(state, operation) {
1034
+ const previous = state.gate;
1035
+ let release;
1036
+ state.gate = new Promise((resolveGate) => { release = resolveGate; });
1037
+ await previous;
1038
+ try {
1039
+ return await operation();
1040
+ }
1041
+ finally {
1042
+ release();
1043
+ }
1044
+ }
1045
+ async function inventoryReplyArtifactBytes(artifactDir) {
1046
+ let total = 0;
1047
+ for (const namespace of REPLY_ARTIFACT_STORAGE_NAMESPACES) {
1048
+ total = addStorageBytes(total, await inventoryTreeBytes(join(artifactDir, namespace)));
1049
+ }
1050
+ return total;
1051
+ }
1052
+ async function inventoryTreeBytes(root) {
1053
+ let entries;
1054
+ try {
1055
+ entries = await readdir(root, { withFileTypes: true });
1056
+ }
1057
+ catch (error) {
1058
+ if (error.code === "ENOENT")
1059
+ return 0;
1060
+ throw error;
1061
+ }
1062
+ let total = 0;
1063
+ for (const entry of entries) {
1064
+ const path = join(root, entry.name);
1065
+ let entryStat;
1066
+ try {
1067
+ entryStat = await lstat(path);
1068
+ }
1069
+ catch (error) {
1070
+ if (error.code === "ENOENT")
1071
+ continue;
1072
+ throw error;
1073
+ }
1074
+ total = entryStat.isDirectory() && !entryStat.isSymbolicLink()
1075
+ ? addStorageBytes(total, await inventoryTreeBytes(path))
1076
+ : addStorageBytes(total, entryStat.size);
1077
+ }
1078
+ return total;
1079
+ }
1080
+ function addStorageBytes(left, right) {
1081
+ return left > Number.MAX_SAFE_INTEGER - right ? Number.MAX_SAFE_INTEGER : left + right;
1082
+ }
1083
+ function isLoopbackHost(host) {
1084
+ return host !== undefined && /^127\.0\.0\.1:\d+$/u.test(host);
1085
+ }
1086
+ async function readJsonBody(request) {
1087
+ const chunks = [];
1088
+ let bytes = 0;
1089
+ for await (const chunk of request) {
1090
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1091
+ bytes += buffer.byteLength;
1092
+ if (bytes > 1_000_000)
1093
+ throw new Error("Reply artifact MCP request exceeds 1 MB.");
1094
+ chunks.push(buffer);
1095
+ }
1096
+ if (chunks.length === 0)
1097
+ return undefined;
1098
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1099
+ }
1100
+ function nodeRequestAsWebRequest(request) {
1101
+ const headers = new Headers();
1102
+ for (const [name, value] of Object.entries(request.headers)) {
1103
+ if (Array.isArray(value))
1104
+ for (const item of value)
1105
+ headers.append(name, item);
1106
+ else if (value !== undefined)
1107
+ headers.set(name, value);
1108
+ }
1109
+ return new Request(`http://${String(request.headers.host)}${request.url ?? "/"}`, {
1110
+ method: request.method ?? "GET",
1111
+ headers,
1112
+ });
1113
+ }
1114
+ async function writeWebResponse(response, webResponse) {
1115
+ const headers = {};
1116
+ webResponse.headers.forEach((value, name) => { headers[name] = value; });
1117
+ response.writeHead(webResponse.status, headers);
1118
+ if (webResponse.body === null) {
1119
+ response.end();
1120
+ return;
1121
+ }
1122
+ const reader = webResponse.body.getReader();
1123
+ for (;;) {
1124
+ const { done, value } = await reader.read();
1125
+ if (done)
1126
+ break;
1127
+ response.write(Buffer.from(value));
1128
+ }
1129
+ response.end();
1130
+ }
1131
+ async function listenLoopback(server) {
1132
+ await new Promise((resolveListen, reject) => {
1133
+ const onError = (error) => reject(error);
1134
+ server.once("error", onError);
1135
+ server.listen(0, "127.0.0.1", () => {
1136
+ server.off("error", onError);
1137
+ resolveListen();
1138
+ });
1139
+ });
1140
+ }
1141
+ async function closeHttpServer(server) {
1142
+ server.closeAllConnections?.();
1143
+ if (!server.listening)
1144
+ return;
1145
+ await new Promise((resolveClose) => server.close(() => resolveClose()));
1146
+ }
1147
+ //# sourceMappingURL=reply-artifacts.js.map