@maincode-ai/matilda-code 0.21.13 → 0.22.0-nightly.20261102.db95a17c4

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 (252) hide show
  1. package/chunks/agent-4GIFJO5G.js +3 -0
  2. package/chunks/agent-headless-DXJESNI4.js +3 -0
  3. package/chunks/{askUserQuestion-BNSWGD26.js → askUserQuestion-7PM7G5YF.js} +3 -3
  4. package/chunks/ca-JKEHLTNL.js +23 -0
  5. package/chunks/chunk-225CZWD7.js +89 -0
  6. package/chunks/chunk-2QIC3OCV.js +9 -0
  7. package/chunks/chunk-33UVJMVE.js +120 -0
  8. package/chunks/chunk-35BBGAHD.js +2604 -0
  9. package/chunks/{chunk-DHDD5YJC.js → chunk-4KS2KEXK.js} +69 -69
  10. package/chunks/chunk-4MLFC664.js +3 -0
  11. package/chunks/{chunk-RNKDFE2S.js → chunk-5B2QERWR.js} +1 -1
  12. package/chunks/chunk-66WJUORA.js +8 -0
  13. package/chunks/chunk-6U5VOTWB.js +56 -0
  14. package/chunks/chunk-7MUWF4DK.js +80 -0
  15. package/chunks/{chunk-6M5G5FBH.js → chunk-BVTXZTL3.js} +1 -1
  16. package/chunks/chunk-CVGGYRNK.js +31 -0
  17. package/chunks/chunk-DUB7JMQV.js +13 -0
  18. package/chunks/chunk-ESNPNLMQ.js +8 -0
  19. package/chunks/chunk-GT6MSEK4.js +3 -0
  20. package/chunks/chunk-HEJO7OBH.js +3 -0
  21. package/chunks/chunk-JNHB73E3.js +9 -0
  22. package/chunks/chunk-JR5CSCG4.js +8 -0
  23. package/chunks/{chunk-EZDV7A76.js → chunk-KRVXBQRC.js} +1 -1
  24. package/chunks/{chunk-AOOZREGV.js → chunk-LBQNFSZ2.js} +1 -1
  25. package/chunks/{chunk-MEV5XQI2.js → chunk-QBXL5UBB.js} +1 -1
  26. package/chunks/{chunk-P5TG5CON.js → chunk-QDWS3VZT.js} +1 -1
  27. package/chunks/{chunk-QZI2ZGZY.js → chunk-QMZREVY3.js} +1 -1
  28. package/chunks/chunk-QUYQBKDE.js +8 -0
  29. package/chunks/chunk-QXSBPCBA.js +8 -0
  30. package/chunks/chunk-S2E5ZRPH.js +14 -0
  31. package/chunks/chunk-TUKNB5CQ.js +33 -0
  32. package/chunks/chunk-UM6CLO7D.js +7 -0
  33. package/chunks/{chunk-6T2GIM76.js → chunk-VVUY7VKR.js} +1 -1
  34. package/chunks/{chunk-5OHGYMKP.js → chunk-WENVHSCJ.js} +1 -1
  35. package/chunks/{chunk-27Q5KW3A.js → chunk-ZABH4QIL.js} +10 -10
  36. package/chunks/chunk-ZSGBDQXU.js +11 -0
  37. package/chunks/{computer-use-J4GAM4M6.js → computer-use-IZGLKENV.js} +1 -1
  38. package/chunks/{cron-create-IDWSZENI.js → cron-create-BTSJUVNT.js} +1 -1
  39. package/chunks/cron-delete-PTE3RC2W.js +3 -0
  40. package/chunks/cron-list-SINKHOIZ.js +5 -0
  41. package/chunks/de-GMGNR5CZ.js +23 -0
  42. package/chunks/{devtools-XRQ4YJ5S.js → devtools-7YNCRYZV.js} +1 -1
  43. package/chunks/{chunk-OOEH6ZSV.js → dist-UWUWLWTO.js} +2 -2
  44. package/chunks/{edit-X2S2ZXRN.js → edit-MDQEZ7BA.js} +1 -1
  45. package/chunks/en-3NQMLVKW.js +23 -0
  46. package/chunks/{enter-worktree-TUCGNCLO.js → enter-worktree-SZK65VXF.js} +1 -1
  47. package/chunks/enterPlanMode-5VWIIVHI.js +18 -0
  48. package/chunks/{exit-worktree-7FLITFZI.js → exit-worktree-OBHUK4TF.js} +1 -1
  49. package/chunks/{exitPlanMode-O5UUJ3GM.js → exitPlanMode-SXFQC4G2.js} +2 -2
  50. package/chunks/fr-WS3EBB36.js +23 -0
  51. package/chunks/{getMachineId-bsd-ZYBNK2JG.js → getMachineId-bsd-ETKQTB6C.js} +1 -1
  52. package/chunks/{getMachineId-darwin-CM2MQGUI.js → getMachineId-darwin-AKNRWOWB.js} +1 -1
  53. package/chunks/{getMachineId-linux-SYE2KMCW.js → getMachineId-linux-2QSD7FTP.js} +1 -1
  54. package/chunks/{getMachineId-unsupported-6IHVG2XO.js → getMachineId-unsupported-UXSV7I24.js} +1 -1
  55. package/chunks/{getMachineId-win-S3J3JSUM.js → getMachineId-win-4MTFXGP3.js} +1 -1
  56. package/chunks/glob-JKIZ3ZJQ.js +22 -0
  57. package/chunks/{goal-tools-YQOA6OKU.js → goal-tools-YJMECZYD.js} +3 -3
  58. package/chunks/{grep-VAB2VSXM.js → grep-Q4T5AE6K.js} +1 -1
  59. package/chunks/ja-GBCDQ43R.js +23 -0
  60. package/chunks/keychain-token-storage-4PY5J6U6.js +3 -0
  61. package/chunks/ls-BQCCCPIX.js +15 -0
  62. package/chunks/{lsp-NGLDRHZU.js → lsp-MHJJJ7K3.js} +1 -1
  63. package/chunks/mcp-client-CSF5QZEY.js +3 -0
  64. package/chunks/monitor-JUAVWVLY.js +32 -0
  65. package/chunks/{multipart-parser-JI75T2O5.js → multipart-parser-FKHYAVGR.js} +1 -1
  66. package/chunks/notebook-edit-NNOMEUER.js +17 -0
  67. package/chunks/openaiContentGenerator-DTKOYRBL.js +47 -0
  68. package/chunks/pt-VID6J2CM.js +23 -0
  69. package/chunks/read-file-4EX4B3TB.js +3 -0
  70. package/chunks/ripGrep-JL6UDNCR.js +3 -0
  71. package/chunks/ru-EB6V6EAN.js +23 -0
  72. package/chunks/scheduler-GCGZAI7D.js +8 -0
  73. package/chunks/send-message-S4P5QKSW.js +8 -0
  74. package/chunks/shell-J7EX7B57.js +3 -0
  75. package/chunks/{skill-ETQM5NI3.js → skill-V6NTJ4O4.js} +1 -1
  76. package/chunks/src-LZEU4AYF.js +5 -0
  77. package/chunks/symbol-search-5QJNW7PY.js +19 -0
  78. package/chunks/{syntheticOutput-WZDPECYS.js → syntheticOutput-3XIAIUIR.js} +1 -1
  79. package/chunks/task-create-KGPY7O4J.js +3 -0
  80. package/chunks/{task-detach-TBAVUOTC.js → task-detach-ZD3A7225.js} +1 -1
  81. package/chunks/task-list-P7GQWYAD.js +9 -0
  82. package/chunks/{task-stop-HDSARSJ6.js → task-stop-VFJRBA6P.js} +1 -1
  83. package/chunks/{task-update-MW2VT2XV.js → task-update-OOMH4WRA.js} +1 -1
  84. package/chunks/{team-create-VMYNLZCC.js → team-create-POMZOM44.js} +2 -2
  85. package/chunks/team-delete-ZJBEQDPE.js +8 -0
  86. package/chunks/todoWrite-EWYRFK7W.js +3 -0
  87. package/chunks/{tool-search-WX6FOE35.js → tool-search-5Z7MO565.js} +1 -1
  88. package/chunks/{web-fetch-CKYBHGLF.js → web-fetch-M4TUYAFG.js} +17 -17
  89. package/chunks/web-search-CBKF36JM.js +26 -0
  90. package/chunks/{workflow-WBSRQG3N.js → workflow-F57TWWPS.js} +2 -2
  91. package/chunks/write-file-UK4BVL3M.js +15 -0
  92. package/cli.js +642 -678
  93. package/examples/starter/README.md +0 -3
  94. package/locales/ca.js +3 -87
  95. package/locales/de.js +3 -91
  96. package/locales/en.js +3 -91
  97. package/locales/fr.js +3 -88
  98. package/locales/ja.js +3 -92
  99. package/locales/pt.js +3 -92
  100. package/locales/ru.js +3 -91
  101. package/package.json +2 -2
  102. package/sandbox-macos-permissive-proxied.sb +1 -1
  103. package/sandbox-macos-restrictive-closed.sb +14 -1
  104. package/sandbox-macos-restrictive-open.sb +14 -1
  105. package/sandbox-macos-restrictive-proxied.sb +15 -2
  106. package/bundled/mainy/SKILL.md +0 -157
  107. package/bundled/mainy/docs/_meta.ts +0 -32
  108. package/bundled/mainy/docs/common-workflow.md +0 -571
  109. package/bundled/mainy/docs/configuration/_meta.ts +0 -7
  110. package/bundled/mainy/docs/configuration/auth.md +0 -82
  111. package/bundled/mainy/docs/configuration/matilda-ignore.md +0 -55
  112. package/bundled/mainy/docs/configuration/model-providers.md +0 -516
  113. package/bundled/mainy/docs/configuration/settings.md +0 -818
  114. package/bundled/mainy/docs/configuration/themes.md +0 -199
  115. package/bundled/mainy/docs/configuration/trusted-folders.md +0 -67
  116. package/bundled/mainy/docs/extension/_meta.ts +0 -9
  117. package/bundled/mainy/docs/extension/extension-releasing.md +0 -204
  118. package/bundled/mainy/docs/extension/getting-started-extensions.md +0 -299
  119. package/bundled/mainy/docs/extension/introduction.md +0 -338
  120. package/bundled/mainy/docs/features/_meta.ts +0 -27
  121. package/bundled/mainy/docs/features/approval-mode.md +0 -388
  122. package/bundled/mainy/docs/features/arena.md +0 -219
  123. package/bundled/mainy/docs/features/auto-mode.md +0 -308
  124. package/bundled/mainy/docs/features/channels/_meta.ts +0 -5
  125. package/bundled/mainy/docs/features/channels/overview.md +0 -328
  126. package/bundled/mainy/docs/features/channels/plugins.md +0 -87
  127. package/bundled/mainy/docs/features/channels/telegram.md +0 -120
  128. package/bundled/mainy/docs/features/code-review.md +0 -291
  129. package/bundled/mainy/docs/features/commands.md +0 -528
  130. package/bundled/mainy/docs/features/dual-output.md +0 -627
  131. package/bundled/mainy/docs/features/followup-suggestions.md +0 -109
  132. package/bundled/mainy/docs/features/headless.md +0 -448
  133. package/bundled/mainy/docs/features/hooks.md +0 -1308
  134. package/bundled/mainy/docs/features/language.md +0 -144
  135. package/bundled/mainy/docs/features/lsp.md +0 -496
  136. package/bundled/mainy/docs/features/markdown-rendering.md +0 -183
  137. package/bundled/mainy/docs/features/mcp.md +0 -423
  138. package/bundled/mainy/docs/features/memory.md +0 -185
  139. package/bundled/mainy/docs/features/sandbox.md +0 -249
  140. package/bundled/mainy/docs/features/scheduled-tasks.md +0 -139
  141. package/bundled/mainy/docs/features/skills.md +0 -347
  142. package/bundled/mainy/docs/features/status-line.md +0 -397
  143. package/bundled/mainy/docs/features/structured-output.md +0 -309
  144. package/bundled/mainy/docs/features/sub-agents.md +0 -737
  145. package/bundled/mainy/docs/features/tips.md +0 -54
  146. package/bundled/mainy/docs/features/token-caching.md +0 -29
  147. package/bundled/mainy/docs/features/tool-use-summaries.md +0 -178
  148. package/bundled/mainy/docs/features/worktree.md +0 -343
  149. package/bundled/mainy/docs/ide-integration/_meta.ts +0 -4
  150. package/bundled/mainy/docs/ide-integration/ide-companion-spec.md +0 -182
  151. package/bundled/mainy/docs/ide-integration/ide-integration.md +0 -144
  152. package/bundled/mainy/docs/integration-github-action.md +0 -241
  153. package/bundled/mainy/docs/integration-jetbrains.md +0 -81
  154. package/bundled/mainy/docs/integration-vscode.md +0 -39
  155. package/bundled/mainy/docs/integration-zed.md +0 -72
  156. package/bundled/mainy/docs/matilda-serve-deploy-local.md +0 -221
  157. package/bundled/mainy/docs/matilda-serve.md +0 -661
  158. package/bundled/mainy/docs/overview.md +0 -64
  159. package/bundled/mainy/docs/quickstart.md +0 -234
  160. package/bundled/mainy/docs/reference/_meta.ts +0 -3
  161. package/bundled/mainy/docs/reference/keyboard-shortcuts.md +0 -91
  162. package/bundled/mainy/docs/support/Uninstall.md +0 -65
  163. package/bundled/mainy/docs/support/_meta.ts +0 -6
  164. package/bundled/mainy/docs/support/tos-privacy.md +0 -94
  165. package/bundled/mainy/docs/support/troubleshooting.md +0 -127
  166. package/chunks/agent-SRAIVZEE.js +0 -3
  167. package/chunks/agent-headless-XRDOZESG.js +0 -3
  168. package/chunks/anthropicContentGenerator-LLDIKDMQ.js +0 -39
  169. package/chunks/ca-NPYL24ME.js +0 -25
  170. package/chunks/chunk-27HUYW23.js +0 -22
  171. package/chunks/chunk-2HTPVSGT.js +0 -3
  172. package/chunks/chunk-3B2HABK7.js +0 -30
  173. package/chunks/chunk-3MTZTNAU.js +0 -101
  174. package/chunks/chunk-3YLXGE6T.js +0 -8
  175. package/chunks/chunk-47XR7IMP.js +0 -8
  176. package/chunks/chunk-4F4DA5UI.js +0 -8
  177. package/chunks/chunk-4Q2SNBUH.js +0 -18
  178. package/chunks/chunk-6EQMZNIY.js +0 -8
  179. package/chunks/chunk-6I6VGTKD.js +0 -8
  180. package/chunks/chunk-DC7LAFZZ.js +0 -8
  181. package/chunks/chunk-DE3HKC2W.js +0 -9
  182. package/chunks/chunk-FWDMPRKU.js +0 -3
  183. package/chunks/chunk-G7R2XEEF.js +0 -7
  184. package/chunks/chunk-GOJCMVJJ.js +0 -8
  185. package/chunks/chunk-GTVCJCHR.js +0 -8
  186. package/chunks/chunk-IJEGHQ7Z.js +0 -32
  187. package/chunks/chunk-JCE3JAVC.js +0 -25
  188. package/chunks/chunk-KHHJYBR5.js +0 -9
  189. package/chunks/chunk-LUNDTQ3G.js +0 -8
  190. package/chunks/chunk-LWARA7JY.js +0 -37
  191. package/chunks/chunk-M53RP3YP.js +0 -31
  192. package/chunks/chunk-MTMU7JYZ.js +0 -11
  193. package/chunks/chunk-O44XK65C.js +0 -8
  194. package/chunks/chunk-OA56BXPD.js +0 -3
  195. package/chunks/chunk-PEKAUGYV.js +0 -2621
  196. package/chunks/chunk-PGUG43HG.js +0 -24
  197. package/chunks/chunk-PGXQZVLX.js +0 -8
  198. package/chunks/chunk-QKYGQLWC.js +0 -8
  199. package/chunks/chunk-QN6Y6NPA.js +0 -3
  200. package/chunks/chunk-RI54U73U.js +0 -56
  201. package/chunks/chunk-RPFSFS7Z.js +0 -39
  202. package/chunks/chunk-SDTWBD62.js +0 -8
  203. package/chunks/chunk-SOEIVSM5.js +0 -113
  204. package/chunks/chunk-SVSY6AWS.js +0 -59
  205. package/chunks/chunk-T2T2UEPY.js +0 -4
  206. package/chunks/chunk-TISQNFB5.js +0 -3
  207. package/chunks/chunk-TWJLOEAI.js +0 -31
  208. package/chunks/chunk-VI74YQQO.js +0 -8
  209. package/chunks/chunk-VYZOOSMP.js +0 -30
  210. package/chunks/chunk-W4QSMIWO.js +0 -9
  211. package/chunks/chunk-WC2VTVCE.js +0 -27
  212. package/chunks/chunk-YV3PUOBU.js +0 -65
  213. package/chunks/chunk-ZX34M2GF.js +0 -14
  214. package/chunks/contextCommand-5F5M7NA4.js +0 -3
  215. package/chunks/cron-delete-NQCDZDUI.js +0 -3
  216. package/chunks/cron-list-Z4XMC3TO.js +0 -5
  217. package/chunks/de-34YZHGC6.js +0 -25
  218. package/chunks/dist-7UYRLSLE.js +0 -3
  219. package/chunks/dist-PH6JW7XD.js +0 -42
  220. package/chunks/en-QLGMD6OS.js +0 -25
  221. package/chunks/enterPlanMode-SQEZAAIR.js +0 -18
  222. package/chunks/fileFromPath-7UCWPD4W.js +0 -3
  223. package/chunks/fr-EDNBQACJ.js +0 -25
  224. package/chunks/geminiContentGenerator-FIZFLPU3.js +0 -8
  225. package/chunks/glob-GSFTYAS2.js +0 -22
  226. package/chunks/ja-LV4RLM3M.js +0 -25
  227. package/chunks/keychain-token-storage-KQK6MNOJ.js +0 -3
  228. package/chunks/ls-IZJ3CKZY.js +0 -15
  229. package/chunks/matildaAgentContentGenerator-QQHPCFJ5.js +0 -8
  230. package/chunks/matildaContentGenerator-W7Z33ONU.js +0 -8
  231. package/chunks/matildaOAuth2-7COTQQR2.js +0 -3
  232. package/chunks/mcp-client-DXUNIEIB.js +0 -3
  233. package/chunks/monitor-4PFIGAGG.js +0 -32
  234. package/chunks/notebook-edit-MWJYAATJ.js +0 -17
  235. package/chunks/openaiContentGenerator-NDVJL6QL.js +0 -3
  236. package/chunks/pt-5SQ4DGL7.js +0 -25
  237. package/chunks/read-file-WPZKPXWA.js +0 -3
  238. package/chunks/ripGrep-7LZDZFTX.js +0 -3
  239. package/chunks/ru-7K24S53N.js +0 -25
  240. package/chunks/scheduler-DK2MVKXX.js +0 -8
  241. package/chunks/send-message-TCPX5UDU.js +0 -8
  242. package/chunks/serve-GSZPJI2O.js +0 -1026
  243. package/chunks/shell-PRS47PDP.js +0 -3
  244. package/chunks/src-UB7DZRYK.js +0 -3
  245. package/chunks/src-WZTRMETQ.js +0 -5
  246. package/chunks/symbol-search-HARHZ5AH.js +0 -19
  247. package/chunks/task-create-XRNIDT4T.js +0 -3
  248. package/chunks/task-list-Z6BDZZID.js +0 -9
  249. package/chunks/team-delete-F7QDC2TE.js +0 -8
  250. package/chunks/todoWrite-SCF5M24M.js +0 -3
  251. package/chunks/web-search-67DFNS5V.js +0 -26
  252. package/chunks/write-file-S5FAX4JJ.js +0 -15
@@ -1,661 +0,0 @@
1
- # Daemon mode (`matilda serve`)
2
-
3
- Run Matilda Code as a local HTTP daemon so multiple clients (IDE plugins, web UIs, CI scripts, custom CLIs) share one agent session over HTTP + Server-Sent Events instead of each spawning their own subprocess.
4
-
5
- > **🚧 v0.16-alpha**: `matilda serve` first ships to npm in v0.16-alpha as **text-only chat / coding** with **local-only deployment**. Image / file attachments on the prompt path, containerized deployment (Docker / k8s / nginx reverse-proxy), and remote / multi-daemon hardening land in a follow-up patch when an enterprise pilot is committed. See [v0.16-alpha known limits](#v016-alpha-known-limits) for the full deferred list.
6
-
7
- > **Status:** Stage 1 (experimental). The protocol surface is locked at the §04 routes table from issue [#3803](https://github.com/MaincodeHQ/matilda-code/issues/3803). Stage 1.5 (`matilda --serve` flag — TUI co-hosts the same HTTP server) and Stage 2 (in-process refactor + `mDNS`/OpenAPI/WebSocket/Prometheus polish) are immediately downstream.
8
- >
9
- > **Scope honesty:** Stage 1 is sized for **developers prototyping clients against the protocol surface** and for **local single-user / small-team collaboration**. Production-grade multi-client / long-running / network-flaky workloads (mobile companions, IM bots reaching 1000+ chats) need Stage 1.5+ guarantees that aren't in this release. See [Stage 1.5+ runtime guarantees](#stage-15-runtime-guarantees) for the full gap list and #3803 for the convergence roadmap.
10
-
11
- ## What it gives you
12
-
13
- - **One agent process, many clients** — under the default `sessionScope: 'single'`, every client connecting to the daemon shares one ACP session. Live cross-client collaboration on the same conversation, the same file diffs, the same permission prompts.
14
- - **Reconnect-safe streaming** — SSE with `Last-Event-ID` reconnect lets a client drop and pick up exactly where it left off (within the ring's replay window).
15
- - **First-responder permissions** — when the agent asks for permission to run a tool, every connected client sees the request; whichever client answers first wins.
16
- - **One daemon, one workspace** — each `matilda serve` process binds to exactly one workspace at boot (per [#3803](https://github.com/MaincodeHQ/matilda-code/issues/3803) §02). Multi-workspace deployments run one daemon per workspace on separate ports (or behind an orchestrator).
17
- - **Remote runtime control** ([#4175](https://github.com/MaincodeHQ/matilda-code/issues/4175) PR 17) — change a session's approval mode (`POST /session/:id/approval-mode`), toggle a tool per workspace (`POST /workspace/tools/:name/enable`), scaffold an empty `MATILDA.md` (`POST /workspace/init`, mechanical only — does NOT call the model; for AI-fill, follow up with `POST /session/:id/prompt`), restart a single MCP server with a budget pre-check (`POST /workspace/mcp/:server/restart`), or add/remove MCP servers at runtime without a daemon restart (`POST /workspace/mcp/servers`, `DELETE /workspace/mcp/servers/:name`). All strict-gated — configure `--token` first.
18
- - **Session recap** ([#4175](https://github.com/MaincodeHQ/matilda-code/issues/4175) follow-up) — fetch a one-sentence "where did I leave off" summary of an active session (`POST /session/:id/recap`). Wraps core's `generateSessionRecap` as a side-query against the fast model; pollutes neither the main chat history nor the SSE stream. Non-strict gate (same posture as `/prompt`); SDK helper `client.recapSession(sessionId)`.
19
- - **Known limit — token-cost amplification:** the route is a pure-cost endpoint (each call is an LLM side-query, no state benefit) and the daemon has no per-route rate limit in v1. On a no-token loopback default a buggy or malicious local client can spam it to burn tokens. Configure `--token` (and optionally `--require-auth`) on shared dev hosts before exposing the daemon.
20
- - **Concurrent recap safety:** two simultaneous `/recap` calls on the same session run two independent side-queries. `generateSessionRecap` reads a snapshot of the chat history via `GeminiClient.getChat().getHistory()` and feeds it to a separate `BaseLlmClient.generateText` call (via `runSideQuery`); it never appends to or mutates the session's `GeminiChat`. Safe to call from multiple clients without coordination.
21
-
22
- ## v0.16-alpha known limits
23
-
24
- The first npm release of `matilda serve` (v0.16-alpha) is intentionally narrow — text-only chat / coding for developers running the daemon on their own machine. The list below makes the deferred surface explicit so adopters can plan around it; everything here is on the v0.16.x patch roadmap or a near-term follow-up release.
25
-
26
- **Product surface — text-only:**
27
-
28
- - ✅ Text prompts and text responses (chat, coding, tool calls, MCP integration)
29
- - ❌ **Image / file attachments on the prompt path** — `MessageEmitter` currently only renders text; multimodal echo lands when an alpha target with image needs is committed (#4175 chiga0 #27 P0 item)
30
- - ❌ **Streaming uploads** — same gating as multimodal
31
-
32
- **Deployment surface — local-only:**
33
-
34
- - ✅ Loopback (`127.0.0.1`, default) — no auth required, suitable for dev workstations
35
- - ✅ Local launch via `systemd` / `launchd` / `nohup &` / `tmux` — see [Local launch templates](./matilda-serve-deploy-local.md)
36
- - ✅ Bring-your-own bearer token via `MATILDA_SERVER_TOKEN` env var ([Authentication](#authentication) for setup)
37
- - ❌ **Containerized deployment** — Docker / Compose / Kubernetes / nginx reverse-proxy with TLS termination NOT in v0.16-alpha. Defers to v0.16.x once an enterprise pilot is committed (would otherwise rot from no-one-validating).
38
- - ❌ **Multi-daemon coordination on one host** — `1 daemon = 1 workspace × N sessions` is enforced. Cross-host federation, instance-path token keying, and stale-token cleanup defer to v0.16.x.
39
- - ❌ **Auto-generated daemon tokens** — alpha is BYO-token (one `openssl rand -hex 32` away). Auto-gen + token-store infrastructure defers to v0.16.x.
40
-
41
- **Hardening — minimum viable for local single-user:**
42
-
43
- - ✅ Boot-time security gate (refuses non-loopback bind without a token, [PR 15 / #4236](https://github.com/MaincodeHQ/matilda-code/pull/4236))
44
- - ✅ Mutation-route auth gate, session-scoped permission routing (Wave 4 PRs)
45
- - ✅ MCP guardrails + multi-client permission coordination (F2 / F3)
46
- - ✅ **Prompt absolute deadline + SSE writer idle timeout** — opt-in via `--prompt-deadline-ms` and `--writer-idle-timeout-ms`; advertised through `prompt_absolute_deadline` and `writer_idle_timeout` when enabled.
47
- - ✅ **HTTP rate limiting** — opt-in via `--rate-limit` and per-tier thresholds; advertised through `rate_limit` when enabled.
48
- - ⏸️ **Prometheus metrics + load test harness** — defers to v0.17 F4 Phase-1 scale instrumentation when 30-50 active sessions becomes a real target.
49
- - ⏸️ **`--max-body-size` CLI flag** — daemon enforces `express.json({ limit: '10mb' })` by default which comfortably covers text-only prompts (model context windows are well under 10 MiB of chars). Tunable via flag in v0.16.x.
50
-
51
- For the deeper "what we won't fix in Stage 1" enumeration (single-host session-state mutation model + N-parallel-sessions sharing one ACP child), see [Stage 1 scope boundaries](#stage-1-scope-boundaries--what-we-wont-fix-in-stage-15) below.
52
-
53
- ## Quickstart
54
-
55
- ### 1. Start the daemon (loopback, no auth)
56
-
57
- ```bash
58
- cd your-project/
59
- matilda serve
60
- # → matilda serve listening on http://127.0.0.1:4170 (mode=http-bridge, workspace=/path/to/your-project)
61
- # → matilda serve: bearer auth disabled (loopback default). Set MATILDA_SERVER_TOKEN to enable.
62
- ```
63
-
64
- The default bind is `127.0.0.1:4170`. Bearer auth is **off** on loopback so local development "just works". The daemon binds to the current working directory; use `--workspace /path/to/dir` to override.
65
-
66
- ### 2. Sanity-check it
67
-
68
- ```bash
69
- curl http://127.0.0.1:4170/health
70
- # → {"status":"ok"}
71
-
72
- curl http://127.0.0.1:4170/capabilities
73
- # → {"v":1,"mode":"http-bridge","features":["health","daemon_status","capabilities","session_create",...],"workspaceCwd":"/path/to/your-project"}
74
-
75
- curl http://127.0.0.1:4170/daemon/status
76
- # → {"v":1,"detail":"summary","status":"ok","runtime":{...}}
77
- ```
78
-
79
- The `workspaceCwd` field surfaces the bound workspace so clients can pre-flight check + omit `cwd` on `POST /session`.
80
- The `limits.maxPendingPromptsPerSession` field advertises the active per-session prompt admission cap; `null` means the cap is disabled.
81
-
82
- The daemon also exposes read-only runtime snapshots for client UIs and
83
- operators: `GET /daemon/status`, `GET /workspace/mcp`,
84
- `GET /workspace/skills`, `GET /workspace/providers`, `GET /workspace/env`,
85
- `GET /workspace/preflight`,
86
- `GET /session/:id/context`, `GET /session/:id/supported-commands`, and
87
- `GET /session/:id/tasks`.
88
-
89
- `GET /daemon/status` is the consolidated troubleshooting snapshot. The default
90
- `detail=summary` reads only in-memory daemon state (sessions, permissions,
91
- SSE/ACP transport counts, rate limit rejects, process memory, resolved limits)
92
- and does not start the ACP child. Use `GET /daemon/status?detail=full` for
93
- per-session diagnostics, ACP connection details, auth device-flow counts, and
94
- workspace status sections when you are actively investigating a problem.
95
-
96
- `GET /workspace/mcp`, `GET /workspace/skills`, and `GET /workspace/providers`
97
- report the live ACP runtime and do not start the ACP child when idle; an
98
- idle daemon returns `initialized: false` with an empty snapshot. Once a
99
- session is alive they switch to `initialized: true` and surface the real
100
- state.
101
-
102
- `GET /workspace/env` and `GET /workspace/preflight` always answer with
103
- `initialized: true` regardless of ACP state. `env` never consults ACP
104
- (daemon-process info only); `preflight` answers daemon-level cells from
105
- `process.*` and emits `status: 'not_started'` placeholders for ACP-level
106
- cells when the child is idle.
107
-
108
- `GET /workspace/env` reports the daemon process's runtime, platform, sandbox,
109
- proxy, and the **presence** (never the value) of whitelisted secret env vars
110
- such as `OPENAI_API_KEY`. Proxy URLs are stripped of credentials and reduced
111
- to `host:port` before they hit the wire. The route always answers from the
112
- daemon process directly and never spawns an ACP child.
113
-
114
- `GET /workspace/preflight` returns a list of readiness checks. **Daemon-level
115
- cells** (Node version, CLI entry, workspace directory, ripgrep, git, npm)
116
- always render. **ACP-level cells** (auth, MCP discovery, skills, providers,
117
- tool registry, egress) require a live ACP child — when the daemon is idle
118
- they emit `status: 'not_started'` placeholders rather than spawning ACP just
119
- to populate them. Failures map to a closed `errorKind` enum (`missing_binary`,
120
- `auth_env_error`, `init_timeout`, `protocol_error`, `missing_file`,
121
- `parse_error`, `blocked_egress`) so client UIs can render structured
122
- remediation.
123
-
124
- The daemon also exposes workspace file helpers:
125
-
126
- - `GET /file` reads text files and returns a raw-byte `sha256:<hex>` hash.
127
- - `GET /file/bytes` reads bounded raw byte windows and returns base64 content.
128
- - `POST /file/write` creates or replaces text files.
129
- - `POST /file/edit` applies one exact text replacement.
130
-
131
- Write/edit are **strict mutation routes**: even on loopback they require a
132
- configured bearer token, otherwise they return `token_required`. Replacements
133
- and edits require the latest `expectedHash` from `GET /file` (or a full-window
134
- `GET /file/bytes`). `create` never overwrites. Explicit writes to ignored paths
135
- are allowed but audited. Binary writes, delete/move/mkdir, and recursive parent
136
- creation are not part of this surface.
137
-
138
- ### 3. Open a session
139
-
140
- ```bash
141
- curl -X POST http://127.0.0.1:4170/session \
142
- -H 'Content-Type: application/json' \
143
- -d '{}'
144
- # → {"sessionId":"<uuid>","workspaceCwd":"…","attached":false}
145
- ```
146
-
147
- `cwd` may be omitted — the route falls back to the daemon's bound workspace. Posting a `cwd` that doesn't match the bound workspace returns `400 workspace_mismatch` (the daemon is bound to exactly one workspace; start a separate daemon for a different one).
148
-
149
- A second client posting to `/session` (any matching `cwd` or none) gets `"attached": true` — they're now sharing the agent.
150
-
151
- ### 4. Subscribe to the event stream (in another terminal first)
152
-
153
- ```bash
154
- SESSION_ID="<from step 3>"
155
- curl -N http://127.0.0.1:4170/session/$SESSION_ID/events
156
- # → id: 1
157
- # event: session_update
158
- # data: {"id":1,"v":1,"type":"session_update","data":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"…"}}}
159
- ```
160
-
161
- The `data:` line is the **full event envelope** — `{id?, v, type, data, originatorClientId?}` — JSON-stringified on a single line. The ACP payload (the `sessionUpdate` block in this example) sits under `data` inside that envelope. The SSE-level `id:` / `event:` lines are convenience for EventSource clients; the same values appear inside the JSON envelope so raw-`fetch` consumers get them too.
162
-
163
- Open this **before** sending the prompt — the SSE replay buffer holds the
164
- last 4000 events so a late subscriber can catch up via `Last-Event-ID`,
165
- but for the simple "watch a single prompt" case it's easiest to subscribe
166
- first and let it stream live.
167
-
168
- The stream emits `session_update` (LLM chunks, tool calls, usage),
169
- `permission_request` (tool needs approval), `permission_resolved`
170
- (someone voted), `model_switched`, `model_switch_failed`, and the terminal
171
- frames `session_died` (agent child crashed — SSE then closes) and
172
- `client_evicted` (your queue overflowed — SSE then closes).
173
-
174
- ### 5. Send a prompt (back in the original terminal)
175
-
176
- ```bash
177
- curl -X POST http://127.0.0.1:4170/session/$SESSION_ID/prompt \
178
- -H 'Content-Type: application/json' \
179
- -d '{"prompt":[{"type":"text","text":"What does src/main.ts do?"}]}'
180
- # → {"stopReason":"end_turn"}
181
- ```
182
-
183
- The `curl -N` from step 4 will print frames as they arrive.
184
-
185
- ## Authentication
186
-
187
- For anything beyond loopback, you **must** pass a bearer token:
188
-
189
- ```bash
190
- export MATILDA_SERVER_TOKEN="$(openssl rand -hex 32)"
191
- matilda serve --hostname 0.0.0.0 --port 4170
192
- # → boot refuses without MATILDA_SERVER_TOKEN
193
- ```
194
-
195
- Clients then send `Authorization: Bearer $MATILDA_SERVER_TOKEN` on every request. `/health` is exempted **only on loopback binds** so k8s/Compose liveness probes inside the pod (where the daemon listens on `127.0.0.1`) don't need credentials. On non-loopback binds (`--hostname 0.0.0.0` etc.) `/health` requires the token like every other route — otherwise an attacker can probe arbitrary addresses to confirm the daemon's existence. Use `/capabilities` to verify your token is correct end-to-end (it always requires auth):
196
-
197
- > **Hardened loopback (`--require-auth`).** The default loopback no-token behavior is fine for a single-user laptop but unsafe on shared dev hosts, CI runners, or multi-tenant workstations where any local user can `curl 127.0.0.1:4170`. Pass `--require-auth` to make the bearer token mandatory on every route — including `/health` and `/capabilities` — even when bound to `127.0.0.1`. Boot fails without a token. With the flag on, an **unauthenticated** client can't read `/capabilities` to discover that auth is required; the discovery surface is the 401 response body itself. Once authenticated, the `caps.features.require_auth` tag is a post-auth confirmation that the deployment is hardened (useful for audit / compliance UIs):
198
- >
199
- > ```bash
200
- > matilda serve --require-auth --token "$(openssl rand -hex 32)"
201
- > # → /health, /capabilities, /session, … all require Authorization: Bearer …
202
- > curl http://127.0.0.1:4170/health
203
- > # → 401
204
- > curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:4170/capabilities | jq '.features | index("require_auth")'
205
- > # → 13 (or whatever index — non-null after authenticating means the tag is present)
206
- > ```
207
-
208
- ```bash
209
- curl -H "Authorization: Bearer $MATILDA_SERVER_TOKEN" http://your-host:4170/capabilities
210
- # → {"v":1,"mode":"http-bridge","features":[...],"modelServices":[],"workspaceCwd":"/path/to/your-project"}
211
- # Wrong token → 401
212
- ```
213
-
214
- The token comparison is constant-time (SHA-256 + `crypto.timingSafeEqual`); 401 responses are uniform across "missing header", "wrong scheme", and "wrong token" so a side-channel can't distinguish.
215
-
216
- ## CLI flags
217
-
218
- | Flag | Default | Purpose |
219
- | --------------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
220
- | `--port <n>` | `4170` | TCP port. `0` = OS-assigned ephemeral port. |
221
- | `--hostname <addr>` | `127.0.0.1` | Bind interface. Anything beyond loopback requires a token. |
222
- | `--token <str>` | — | Bearer token. Falls back to `MATILDA_SERVER_TOKEN` env var (with leading/trailing whitespace stripped — handy for `$(cat token.txt)`). |
223
- | `--require-auth` | `false` | Refuse to start without a bearer token, even on loopback. Hardens the `127.0.0.1` developer default for shared dev hosts / CI runners / multi-tenant workstations where any local user can hit the listener. Boots only with `--token` or `MATILDA_SERVER_TOKEN` set; gates `/health` behind the bearer too. |
224
- | `--max-sessions <n>` | `20` | Cap on concurrent live sessions. New `POST /session` requests that would spawn a fresh child return `503` (with `Retry-After: 5`) when the cap is hit; attaches to existing sessions are NOT counted. Set to `0` to disable. Sized for single-user / small-team usage; raise it if your deployment has the RAM/FD headroom (~30–50 MB per session). |
225
- | `--max-pending-prompts-per-session <n>` | `5` | Per-session cap on prompts accepted by `POST /session/:id/prompt` but not yet settled, including queued prompts and the active prompt. The bridge rejects overflow synchronously with `503`, `Retry-After: 5`, and `code: "prompt_queue_full"` before returning a `promptId`. Set to `0` to disable. `branchSession` serializes on the same FIFO but does not count against this prompt cap. |
226
- | `--workspace <path>` | `process.cwd()` | Absolute workspace path this daemon binds to (per [#3803](https://github.com/MaincodeHQ/matilda-code/issues/3803) §02 — 1 daemon = 1 workspace). `POST /session` requests with a mismatched `cwd` return `400 workspace_mismatch`. For multi-workspace deployments, run one `matilda serve` per workspace on separate ports. |
227
- | `--max-connections <n>` | `256` | Listener-level TCP connection cap (`server.maxConnections`). Bounds raw socket count irrespective of session count — slow / phantom SSE clients get rejected at accept time once full. Raise alongside `--max-sessions` if your deployment expects many SSE subscribers per session. |
228
- | `--event-ring-size <n>` | `8000` | Per-session SSE replay ring depth (#3803 §02 target). Sets the backlog available to `GET /session/:id/events` with `Last-Event-ID: N`. Larger = more reconnect headroom at the cost of a few hundred KB extra RAM per session. SDK clients can additionally request a larger per-subscriber backlog cap on a specific subscription via `?maxQueued=N` (range `[16, 2048]`, default 256). Daemons also emit a non-terminal `slow_client_warning` SSE frame at 75% queue fill so clients can drain / reconnect before getting evicted. Pre-flight `caps.features.slow_client_warning`. |
229
- | `--mcp-client-budget <n>` | — | Positive integer cap on live MCP clients **per ACP session** (issue [#4175](https://github.com/MaincodeHQ/matilda-code/issues/4175) PR 14 v1; PR 23 graduates this to per-workspace via the shared MCP pool). Combine with `--mcp-budget-mode`. When unset, no accounting-driven enforcement (but `GET /workspace/mcp` still reports `clientCount`). Distinct from claude-code's `MCP_SERVER_CONNECTION_BATCH_SIZE` which gates startup concurrency, not the total client count. Pre-flight `caps.features.mcp_guardrails`. |
230
- | `--mcp-budget-mode <m>` | `warn` / `off` | How `--mcp-client-budget` is enforced. `warn` (default when budget set): no refusal, snapshot's `budgets[0].status` flips to `warning` at ≥75% of budget. `enforce`: connects past the cap are refused, per-server cell shows `disabledReason: 'budget'`, deterministic by `mcpServers` declaration order. `off` (default when budget unset): pure observability. Boot rejects `enforce` without a budget. |
231
- | `--http-bridge` | `true` | Stage 1 mode: one `matilda --acp` child per daemon (bound to one workspace at boot, per [#3803](https://github.com/MaincodeHQ/matilda-code/issues/3803) §02); N sessions multiplex onto that child via ACP `newSession()`. Stage 2 native in-process becomes available later. |
232
- | `--allow-origin <pat>` | — | T2.4 ([#4514](https://github.com/MaincodeHQ/matilda-code/issues/4514)). Cross-origin allowlist for browser webui clients. Repeatable. Each value is `*` (any origin — boot refuses if no bearer token is configured; `--require-auth` on loopback is recommended so `/health` and `/demo` are also bearer-gated, since both are pre-auth on loopback by default) or a canonical URL origin (`<scheme>://<host>[:<port>]`, no trailing slash / path / userinfo / query). **Subdomain wildcards (`https://*.example.com`) are intentionally unsupported** — list each subdomain explicitly, or use `*` with a configured token (and `--require-auth` for full hardening). Matched origins receive CORS response headers (`Access-Control-Allow-Origin`, `Vary: Origin`, methods, headers, max-age, and exposed `Retry-After`); unmatched origins still get a 403 with the same envelope as today's wall. `Origin: null` (sandboxed iframes, file:// docs) is always rejected, even under `*`. Pre-flight via `caps.features.allow_origin`. Loopback self-origin hits are unaffected. |
233
-
234
- > **Sizing the load knobs.** `--max-sessions` is the **new-child** cap.
235
- > Three other layers also limit load — when sizing for a high-concurrency
236
- > deployment, tune them together:
237
- >
238
- > - **listener-level**: `--max-connections` / `server.maxConnections=256`
239
- > bounds raw TCP connections (slow-client back-pressure).
240
- > - **per-session subscribers**: the EventBus caps SSE subscribers at
241
- > 64 per session by default; the 65th client gets a terminal
242
- > `stream_error` and is closed.
243
- > - **per-session prompt admissions**:
244
- > `--max-pending-prompts-per-session=5` bounds queued + active prompts
245
- > accepted for one session. Overflow gets `503` with `Retry-After: 5`.
246
- > - **per-subscriber backlog**: a 256-frame queue per SSE client; an
247
- > over-capacity client gets a terminal `client_evicted` frame and is
248
- > closed (one slow consumer can't pin the daemon).
249
- >
250
- > These caps interact: `--max-sessions × 64 subscribers × 256 frames`
251
- > is the worst-case in-flight memory at the EventBus layer, while
252
- > `--max-sessions × --max-pending-prompts-per-session` bounds accepted
253
- > prompt work at the admission layer. Default sizing assumes single-user /
254
- > small-team load; raise progressively (and watch RSS) for multi-tenant
255
- > deployments.
256
-
257
- > **MCP client guardrails (issue [#4175](https://github.com/MaincodeHQ/matilda-code/issues/4175) PR 14).** A workspace declaring 30 MCP servers in `mcpServers` will start 30 clients with no upstream cap unless you set one. `--mcp-client-budget=N` caps the live MCP client count; `--mcp-budget-mode={enforce,warn,off}` chooses the behavior. Default is `warn` when a budget is set (snapshot surfaces the warning but no client is refused — useful for measuring real-world fanout before flipping on enforcement). Refused servers under `enforce` mode get `disabledReason: 'budget'` on their per-server cell, and the `budgets[0]` cell shows `status: 'error'` + `errorKind: 'budget_exhausted'`. Slot reservation is by server name and survives reconnects / discovery timeouts — a refused server can't take a slot from a healthy one.
258
- >
259
- > ⚠️ **v1 scope: per-session, not per-workspace.** Each ACP session inside the daemon has its own `Config`/`McpClientManager` (created via `newSessionConfig` per session). The budget caps live MCP clients **per session**, not aggregated across all sessions in the workspace. Snapshot at `GET /workspace/mcp` reflects the bootstrap session's view (the cell carries `scope: 'session'` for honesty). If you run 5 concurrent ACP sessions with `--mcp-client-budget=10`, you may have up to 50 live MCP clients across the daemon — the cap holds per session. **Wave 5 PR 23 (shared MCP pool)** introduces a workspace-scoped manager and graduates this to true per-workspace enforcement.
260
- >
261
- > ```sh
262
- > matilda serve --mcp-client-budget=10 --mcp-budget-mode=warn
263
- > # later, after telemetry shows your real-world distribution:
264
- > matilda serve --mcp-client-budget=10 --mcp-budget-mode=enforce
265
- > ```
266
- >
267
- > This is **not** the same as claude-code's `MCP_SERVER_CONNECTION_BATCH_SIZE` (which gates startup concurrency); they're orthogonal. PR 23 will add a real shared MCP pool (a `scope: 'workspace'` cell in `budgets[]` alongside the per-session cell); PR 14 v1 is the in-process counter + soft enforcement on the existing per-session manager.
268
- >
269
- > **Push events (issue [#4175](https://github.com/MaincodeHQ/matilda-code/issues/4175) PR 14b).** SDK clients subscribed to `GET /session/:id/events` receive typed frames when budget thresholds cross — `mcp_budget_warning` (synthetic, fires once per upward 75% crossing with hysteresis re-arm at 37.5%, advertised via `mcp_guardrail_events`) and `mcp_child_refused_batch` (coalesced once per discovery pass under `enforce` mode; length-1 from `readResource` lazy-spawn refusal). The snapshot at `GET /workspace/mcp` is still the source-of-truth for state-after-reconnect; events are change-edges. Useful when dashboarding in real-time without polling.
270
-
271
- ## Default deployment threat model
272
-
273
- - **127.0.0.1 only** — loopback bind, no auth needed.
274
- - **`--hostname 0.0.0.0` requires a token** — boot refuses without one.
275
- - **`LOOPBACK_BINDS` includes IPv6** — `::1` and `[::1]` count as loopback for the no-token rule.
276
- - **Host header allowlist** — on **loopback** binds the daemon checks `Host:` matches `localhost:port` / `127.0.0.1:port` / `[::1]:port` / `host.docker.internal:port` (case-insensitive per RFC 7230 §5.4) to defend against DNS rebinding. **Non-loopback binds (`--hostname 0.0.0.0`) intentionally bypass the Host allowlist** — the operator has chosen the surface area, so the bearer-token gate is the sole authentication layer; reverse proxies / SNI / client cert pinning are the operator's responsibility, not the daemon's. If you need Host-based isolation on a non-loopback bind, terminate TLS + check Host at a front proxy.
277
- - **CORS denies any browser Origin by default** — returns `403` JSON. Pass **`--allow-origin <pattern>`** (repeatable, T2.4 #4514) to opt specific browser origins through. Each value is either the literal `*` (any origin — boot refuses if no bearer token is configured; `--require-auth` on loopback is recommended for full hardening since `/health` and `/demo` remain pre-auth on loopback by default) or a canonical URL origin (`<scheme>://<host>[:<port>]`, no trailing slash / path / userinfo). Matched origins receive proper CORS response headers (`Access-Control-Allow-Origin: <echoed>`, `Vary: Origin`, plus standard methods / headers / max-age and exposed `Retry-After`); unmatched origins still get a 403 with the same envelope as the default wall. `caps.features.allow_origin` is advertised conditionally so SDK / webui clients can pre-flight whether the daemon honors cross-origin hits before issuing them. Example: `matilda serve --allow-origin http://localhost:3000 --allow-origin http://localhost:5173`. Loopback self-origin hits (e.g. the `/demo` page) are unaffected — a separate Origin-strip shim handles them regardless of `--allow-origin`. **Browser webuis without `--allow-origin` configured** still fall back to the same Stage 1 options as before: package as a native shell (Electron/Tauri) so no `Origin` header is sent, or front the daemon with a same-origin reverse proxy.
278
- - **Spawned `matilda --acp` child inherits the daemon's environment** with one explicit scrub: `MATILDA_SERVER_TOKEN` is removed before the child starts (the daemon's own bearer; the agent doesn't need it). Everything else — `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `MATILDA_*` / `MATILDA_API_KEY` / your custom `modelProviders[].envKey` / etc. — passes through, because the agent legitimately needs those to authenticate to the LLM. **This is intentional, not a sandbox.** The agent runs as the same UID with shell-tool access, so anything in `~/.bashrc` / `~/.aws/credentials` / `~/.npmrc` is reachable by prompt injection regardless. The env passthrough is not the security boundary; the user-as-trust-root is. Don't run `matilda serve` under an identity that has env-resident credentials you wouldn't trust the agent with.
279
- - **Per-subscriber bounded SSE queues** — a slow client that overflows its queue gets a `client_evicted` terminal frame and is closed; one stuck consumer can't pin the daemon.
280
- - **Per-session prompt admission cap** — defaults to 5 accepted-but-unsettled prompts per session. A buggy client cannot enqueue unbounded prompt promises or temporary SSE waits for one session.
281
- - **Graceful shutdown** — SIGINT/SIGTERM drain the agent children before closing the listener (10s deadline per child).
282
-
283
- > ⚠️ **Stage 1 known gap — permissions are daemon-global, not per-session (BUy4H).** `pendingPermissions` lives at daemon scope; any client holding the bearer token can vote on any `requestId` for any session it can see (and SSE `permission_request` events carry the requestId in their payload). This is acceptable under the single-user / small-team trust model where every authenticated client is the same human or collaborators they trust. Stage 1.5 will move to `POST /session/:id/permission/:requestId` + session-scoped pending map + per-client identity (must-have #3 from the downstream review); until then, don't run `matilda serve` behind a bearer shared with untrusted parties.
284
- >
285
- > ⚠️ **Stage 1 known gap — `POST /session/:id/prompt` body capped at 10 MB (BUy4L).** Multimodal prompts containing images / PDFs / audio that exceed 10 MB will fail at body-parse time before route logic runs (no streaming, no mid-upload abort). Workaround: shrink the content client-side, or pass a path reference and let the agent read the file via `readTextFile`. Stage 1.5 will accept `multipart/form-data` or chunked encoding on `/prompt` so large prompts don't hit a cliff.
286
- >
287
- > ⚠️ **Stage 1 known gap — phantom SSE connections behind NAT.** The
288
- > daemon detects dead clients via TCP back-pressure on heartbeats
289
- > (15s interval). A client that vanishes WITHOUT a TCP RST (e.g. a
290
- > NAT box silently dropping idle flows) keeps the kernel-level socket
291
- > "alive" until Node's keepalive probes time out — typically ~2 hours
292
- > on Linux defaults. On `--hostname 0.0.0.0` deployments behind such
293
- > NATs, phantom SSE connections can accumulate and eventually hit the
294
- > 256 `server.maxConnections` ceiling.
295
- >
296
- > Set [`--writer-idle-timeout-ms <n>`](#deadlines-and-writer-idle-timeout)
297
- > (issue [#4514](https://github.com/MaincodeHQ/matilda-code/issues/4514) T2.9)
298
- > to close the gap with an explicit application-level idle deadline:
299
- > when no write has successfully flushed for `n` ms the daemon emits
300
- > a terminal `client_evicted` frame with
301
- > `reason: 'writer_idle_timeout'` and closes the stream. The flag is
302
- > off by default to preserve the legacy contract — operators on
303
- > networks that swallow RSTs should pick a value well above the 15s
304
- > heartbeat interval (e.g. `60000`–`300000`) so legitimate idle
305
- > connections aren't evicted while genuinely stuck writers are
306
- > reaped promptly. Pre-flight `caps.features.includes('writer_idle_timeout')`
307
- > from your SDK to confirm the daemon supports it.
308
-
309
- ### Deadlines and writer idle timeout
310
-
311
- Issue [#4514](https://github.com/MaincodeHQ/matilda-code/issues/4514) T2.9 ships two opt-in flags that close the long-running / remote-deployment gaps the 15s heartbeat + AbortSignal don't cover. Both are off by default — single-user loopback workflows stay bit-for-bit unchanged.
312
-
313
- | Flag | Env var | Default | What it does |
314
- | ------------------------------ | -------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
315
- | `--prompt-deadline-ms <n>` | `MATILDA_SERVE_PROMPT_DEADLINE_MS` | unset | Server-side wallclock cap on a single `POST /session/:id/prompt`. On expiry the daemon aborts the prompt's AbortController and returns HTTP `504` with `{code:"prompt_deadline_exceeded", errorKind:"prompt_deadline_exceeded", deadlineMs:n}`. A per-prompt request body field `deadlineMs` can SHORTEN the effective deadline below the flag but never extend it. Capability tag (conditional): `prompt_absolute_deadline`. |
316
- | `--writer-idle-timeout-ms <n>` | `MATILDA_SERVE_WRITER_IDLE_TIMEOUT_MS` | unset | Per-SSE-connection idle deadline. When no write has SUCCESSFULLY flushed for `n` ms — neither a real event nor the 15s heartbeat — the daemon emits a terminal `client_evicted` frame with `data.reason = 'writer_idle_timeout'` (mirrored on `data.errorKind`) and closes the stream. **Pick a value comfortably above the 15s heartbeat** (e.g. `30000`–`300000`) so legitimate idle streams aren't evicted; values `< 15000` WILL evict otherwise-healthy idle connections before the first heartbeat fires (intentional only for tests / short-lived dev sessions). Capability tag (conditional): `writer_idle_timeout`. |
317
-
318
- Both flags accept a positive integer in milliseconds; `0`, `NaN`, non-integer, or negative values are rejected at boot with a clear error message. CLI flag wins over env var; explicit `ServeOptions` field (embedded callers) wins over env. SDK consumers should pre-flight the matching capability tag before relying on either behavior — daemons predating this PR omit both tags and the request `deadlineMs` field is silently dropped.
319
-
320
- ## Multi-session & multi-workspace deployment
321
-
322
- Per [#3803](https://github.com/MaincodeHQ/matilda-code/issues/3803) §02, each `matilda serve` process binds to **one workspace** at boot. Within that workspace it multiplexes N sessions onto a single `matilda --acp` child via the agent's native session map — sessions share the child's process / OAuth state / file-read cache / hierarchy-memory parse.
323
-
324
- To host **multiple workspaces** (one user, several repos; or several users on the same host), run **multiple daemon processes** — one per workspace, each on its own port, supervised by systemd / docker-compose / k8s / a `matilda-coordinator` reference orchestrator. The trade-off is intentional: one workspace per child means `loadSettings(cwd)` / OAuth / MCP server scope stay aligned with the bound directory and don't drift across requests.
325
-
326
- > **Subscribe BEFORE posting `modelServiceId` on attach.** When a client `POST /session` with a `modelServiceId` and the workspace already has a session running a different model, the daemon issues an internal `setSessionModel` call — failures are NOT propagated as an HTTP error (the session stays operational on its current model). The visible failure signal is a `model_switch_failed` event on the session's SSE stream. If you call `POST /session` and only THEN open `GET /session/:id/events`, you'll miss the failure event and silently keep talking to the wrong model. Open the SSE stream first, or pass `Last-Event-ID: 0` on subscribe to replay the ring's oldest available event.
327
-
328
- To handle multiple **users** (each with their own quota, audit log, sandbox) or to scale beyond one process's reach (cold-start budget, FD count, RSS), spawn one daemon per workspace per user behind an external orchestrator. That orchestrator (multi-tenancy / OIDC / Quota / Audit / k8s) is **out of scope** for the matilda-code project — see issue [#3803](https://github.com/MaincodeHQ/matilda-code/issues/3803) "External Reference Architecture" for the design pointers.
329
-
330
- ## Loading and resuming a persisted session
331
-
332
- The daemon exposes ACP's `session/load` and resume flow over HTTP via two routes:
333
-
334
- | Route | Use when |
335
- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
336
- | `POST /session/:id/load` | The client has **no** history rendered (cold reconnect, picker-then-open). The daemon replays every persisted turn through SSE so subscribers see the full transcript. Capability tag: `session_load`. |
337
- | `POST /session/:id/resume` | The client already has the turns on screen and only needs the daemon-side handle back. Model context is restored on the agent side without UI replay — the SSE stream stays clean. Capability tag: `session_resume`. |
338
-
339
- The TypeScript SDK exposes both as static factories on `DaemonSessionClient`:
340
-
341
- ```ts
342
- import { DaemonClient, DaemonSessionClient } from '@matilda-code/sdk';
343
-
344
- const client = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170' });
345
-
346
- // Cold reconnect — daemon will replay history through SSE.
347
- const session = await DaemonSessionClient.load(client, 'persisted-id');
348
-
349
- // Or, if your UI already has the history, skip the replay:
350
- // const session = await DaemonSessionClient.resume(client, 'persisted-id');
351
-
352
- for await (const event of session.events()) {
353
- // First the replayed `session_update` frames (load only),
354
- // then live events.
355
- }
356
- ```
357
-
358
- Pre-flight `caps.features.session_load` / `caps.features.session_resume` before calling — older daemons return `404`. Concurrent same-action requests for the same id coalesce; cross-action races (a `load` racing a `resume`) get `409 restore_in_progress` with `Retry-After: 5`. See the [protocol reference](../developers/matilda-serve-protocol.md) for the full error envelope.
359
-
360
- Note: history replay is bounded by the SSE ring (default 4000 frames). Long histories with chatty turns can exceed that — earliest frames are dropped silently. For very long sessions, prefer `resume` and rely on the client's local persisted UI.
361
-
362
- ## Durability model
363
-
364
- **Sessions are still ephemeral in Stage 1 across daemon restarts**, but persisted sessions on disk can be reloaded:
365
-
366
- - A child process crash publishes `session_died` and removes the live session from the daemon's maps. The persisted on-disk session **can** be reloaded via `POST /session/:id/load` if a fresh agent child is spawnable.
367
- - A daemon restart loses every in-flight live session. The persisted sessions remain on disk and can be loaded against a new daemon process, subject to the same workspace binding rules.
368
- - Long client disconnects (>5 min on a chatty turn) can outrun the SSE replay ring (default 4000 frames) — `Last-Event-ID` reconnect succeeds but state may be incoherent. For mobile / flaky-network clients, plan to re-open SSE on long drops or call `POST /session/:id/load` to replay from disk.
369
- - File operations (`writeTextFile`) are atomic across crashes (write-then-rename); they aren't atomic across daemon restarts in the sense of replaying — the file write either landed or it didn't.
370
-
371
- If your integration needs server-side cross-restart durability beyond what `session/load` covers (e.g. server-managed retry queues), you still need application-level state recovery. Don't hold long-running, restart-sensitive state inside the daemon's session.
372
-
373
- ## Stage 1.5+ runtime guarantees
374
-
375
- Stage 1's contract is sized for prototyping. Per [#3889 chiga0 downstream-consumer review](https://github.com/MaincodeHQ/matilda-code/pull/3889#issuecomment-4427875644), the following are **not** in Stage 1 — production-grade integrations need Stage 1.5+ before relying on them:
376
-
377
- **Blockers for serious downstream use:**
378
-
379
- 1. **`loadSession` / `resumeSession` over HTTP** — without this, no integration can survive a child crash or daemon restart, and any orchestrator coordinating the daemon can't recover state either.
380
- 2. **Persistent client identity (pair tokens + per-client revocation)** — Stage 1 uses one shared bearer; a leaked token revokes everyone, and `originatorClientId` is client-self-declared rather than daemon-stamped from authenticated identity.
381
-
382
- **Reliability baseline:**
383
-
384
- 3. ~~**Client-initiated heartbeat path**~~ — shipped via [#4175](https://github.com/MaincodeHQ/matilda-code/issues/4175) PR 9. `POST /session/:id/heartbeat` records last-seen timestamps on the daemon (capability tag `client_heartbeat`); SDK helpers are `DaemonClient.heartbeat()` / `DaemonSessionClient.heartbeat()`.
385
- 4. **`permission_already_resolved` event** when a vote loses the first-responder race — currently UIs have to infer state from a `404`.
386
- 5. **Larger / per-session-configurable replay ring** — default 4000 covers short drops; mobile / chatty-turn workloads need 8000+ or per-session config.
387
- 6. **`slow_client_warning` event before `client_evicted`** — soft backpressure so well-behaved slow clients can self-throttle (trim render depth, drop chunks) before being terminated.
388
-
389
- **Integration ergonomics:**
390
-
391
- 7. **`POST /session/:id/_meta` for IM-style context** — per-session key-value attached to subsequent prompts (chat id, sender, thread id) replaces the per-channel improvisation.
392
- 8. **`/capabilities` actual feature negotiation** — `protocol_versions: { acp: '0.14.x', daemon_envelope: 1 }` so clients can detect drift instead of falling through to "unknown frame, ignore".
393
- 9. **First-class durability documentation** (this section) — already shipped above.
394
-
395
- The full convergence roadmap is tracked on [#3803](https://github.com/MaincodeHQ/matilda-code/issues/3803).
396
-
397
- ## Stage 1 scope boundaries — what we won't fix in Stage 1.5
398
-
399
- Two structural choices are explicit non-goals for the Stage 1 / 1.5 / 2 main-line roadmap. If your use case depends on either, plan around them rather than waiting for us.
400
-
401
- ### Session state is local-mutation-only (per [LaZzyMan review #4270256721](https://github.com/MaincodeHQ/matilda-code/pull/3889#pullrequestreview-4270256721))
402
-
403
- The Stage 1.5 plan describes TUI as an in-process EventBus subscriber. In practice **TUI UI is strictly larger than the wire protocol**:
404
-
405
- - **Local-only UI** — the ~15 Ink dialog components (`ModelDialog`, `MemoryDialog`, `PermissionsDialog`, `SessionPicker`, `WelcomeBackDialog`, `FolderTrustDialog`, …) and the `local-jsx` slash commands (`/ide`, `/auth`, `/init`, `/resume`, `/rename`, `/delete`, `/language`, `/arena`, …) render terminal-specific Ink JSX. Remote clients on HTTP/SSE can't equivalently render Ink, and these flows emit no wire event.
406
- - **Session-state mutations without wire events** — `/approval-mode`, `/memory add`, `/mcp add-server`, `/agents`, `/tools enable/disable`, `/auth`, `/init` (writing `CLAUDE.md`) all change agent behavior, but only `/model` currently publishes an event (`model_switched`).
407
-
408
- **Stage 1 choice — option (A) from the review**: don't promote these mutations to wire events. The two deployment modes have different consequences.
409
-
410
- #### Mode 1 — headless `matilda serve` (this PR)
411
-
412
- No TUI shell runs inside the daemon. The slash commands listed above **don't exist** in this mode — there's no terminal UI to issue them from. Session state is therefore:
413
-
414
- - **Boot-time-frozen** for `approval-mode` / `memory` / `agents` / `tools` allowlist / `auth` — all loaded from settings + disk when the daemon's `matilda --acp` child starts; immutable for the session's lifetime. Settings-defined MCP servers are likewise frozen at boot, but **runtime-added servers** (via `POST /workspace/mcp/servers`) can be added or removed without restart.
415
- - **Mutable over HTTP** via `POST /session/:id/model` (publishes `model_switched`), `POST /workspace/mcp/servers` / `DELETE /workspace/mcp/servers/:name` (publishes `mcp_server_added` / `mcp_server_removed`), and permission votes (`POST /permission/:requestId`).
416
-
417
- **Consequence:** remote clients in headless mode see the **full session state**. No TUI hides additional state; no drift is possible. If you want to change `approval-mode`, restart the daemon with new settings. MCP servers can now be added/removed at runtime via the mutation routes (`POST /workspace/mcp/servers`, `DELETE /workspace/mcp/servers/:name`) — see [Runtime MCP server management](#runtime-mcp-server-management-issue-4514).
418
-
419
- #### Mode 2 — Stage 1.5 `matilda --serve` co-hosted TUI (not in this PR)
420
-
421
- When Stage 1.5 lands `matilda --serve` (TUI process co-hosts the same HTTP server), the TUI **does** exist alongside remote clients. A local operator typing `/approval-mode yolo` or `/mcp add-server` mutates session state, and remote clients on HTTP have no event to observe the change.
422
-
423
- In this mode, TUI is a **"super-client"** — it observes the same agent conversation remote clients see, AND can mutate session state remote clients can't. The asymmetry is:
424
-
425
- - ✅ Both TUI and remote clients see the same agent messages, tool calls, file diffs, permission prompts.
426
- - ❌ Only TUI sees / mutates approval-mode / memory / MCP server list / agents / tools allowlist / auth state.
427
-
428
- **Consequence in Mode 2:** if a remote-client UI tries to mirror session settings, it can drift after any TUI slash command. Remote clients should **re-fetch state on attach / reconnect** (use `Last-Event-ID: 0` to replay the ring's oldest event for things like `model_switched`); they should NOT rely on incremental events for TUI-side mutations.
429
-
430
- #### Why (A) and not (B) (promote mutations to `session_state_changed` event family)
431
-
432
- (B) is the more ambitious answer but locks Stage 1.5 into a substantially larger wire surface that must also pass cleanly through the planned in-process refactor. We'd rather walk the smaller scope honestly. The session-state-event taxonomy work — enumerating which TUI flows are local-only by design vs. could plausibly graduate to wire under a future opt-in (B)-flavor extension — moves to [#3803](https://github.com/MaincodeHQ/matilda-code/issues/3803), not Stage 1.5 code.
433
-
434
- ### N parallel sessions share one `matilda --acp` child
435
-
436
- Multiple sessions on the same workspace **share one `matilda --acp` child process** via the agent's native multi-session support (`packages/cli/src/acp-integration/acpAgent.ts:194: private sessions: Map<string, Session>`). The bridge calls `connection.newSession({cwd, mcpServers})` for each session — the agent stores them in its sessions map and demultiplexes per-call sessionId.
437
-
438
- Concrete cost at N=5 sessions on the same workspace:
439
-
440
- | Resource | Per session | At N=5 |
441
- | ------------------------------------ | ----------- | ---------------------------- |
442
- | Daemon Node process | one | **30–50 MB** (one daemon) |
443
- | `matilda --acp` child | shared | **60–100 MB** (one child) |
444
- | MCP server children | per-session | 3×N if configs differ |
445
- | `FileReadCache` (in-child heap) | shared | parsed once |
446
- | `CLAUDE.md` / hierarchy memory parse | shared | parsed once |
447
- | OAuth refresh-token state | shared | **one refresh path** |
448
- | Auto-memory learned facts | shared | one knowledge base per child |
449
- | Cold start | first only | <200 ms after first session |
450
-
451
- The bridge keeps **one channel per daemon** (one daemon per workspace, per §02). The channel stays alive while at least one session is live; the last `killSession` (or a channel-level crash) kills the child.
452
-
453
- **MCP server children** are still per-session today — each session's config can specify different servers, so they're independently spawned. Stage 1.5 follow-up: refcount MCP server children by `(workspace, config-hash)` so identical configs share. Not in scope for this PR.
454
-
455
- **Peer agents (Cursor / Continue / Claude Code / OpenCode / Gemini CLI) all do single-process multi-session.** matilda-code matches them at the agent layer; the Stage 1 bridge in this PR makes the same architecture visible over HTTP.
456
-
457
- ## Logging in to a remote daemon (issue #4175 PR 21)
458
-
459
- When the daemon runs on a remote pod (no shared display with you), you can still log in to a Matilda account by triggering an OAuth device flow over HTTP. The daemon polls the IdP itself; your job is just to open a URL on whatever device has a browser.
460
-
461
- ```bash
462
- # 1. Start a flow. The daemon contacts the IdP, returns a code + URL.
463
- curl -X POST http://127.0.0.1:4170/workspace/auth/device-flow \
464
- -H "Authorization: Bearer $TOKEN" \
465
- -H "Content-Type: application/json" \
466
- -d '{"providerId":"matilda-oauth"}'
467
- # → 201 {
468
- # "deviceFlowId": "fa07c61b-…",
469
- # "userCode": "USER-1",
470
- # "verificationUri": "https://chat.matilda.ai/api/v1/oauth2/device",
471
- # "verificationUriComplete": "https://chat.matilda.ai/...?user_code=USER-1",
472
- # "expiresAt": 1700000600000,
473
- # "intervalMs": 5000,
474
- # "attached": false
475
- # }
476
-
477
- # 2. Visit the URL on your phone / laptop, enter the user code.
478
- # 3. Poll for completion (or subscribe to SSE for the auth_device_flow_authorized event):
479
- curl http://127.0.0.1:4170/workspace/auth/device-flow/fa07c61b-… \
480
- -H "Authorization: Bearer $TOKEN"
481
- # → status transitions: pending → authorized
482
- ```
483
-
484
- The TypeScript SDK wraps both steps into a single helper:
485
-
486
- ```ts
487
- import { DaemonClient } from '@matilda-code/sdk';
488
-
489
- const client = new DaemonClient({ baseUrl, token });
490
- const flow = await client.auth.start({ providerId: 'matilda-oauth' });
491
- console.log(`Open ${flow.verificationUri}\nCode: ${flow.userCode}`);
492
- const result = await flow.awaitCompletion({ signal: abortCtrl.signal });
493
- // result.status === 'authorized'
494
- ```
495
-
496
- **The daemon never opens a browser on your behalf.** Even when running locally, the daemon stays passive — it returns the URL and lets the SDK / user choose where to open it. This is intentional: a daemon on a headless pod that called `xdg-open` would silently fail, masking the actual auth surface. Mirror `gh auth login`'s "Press Enter to open browser" UX in your client.
497
-
498
- **`--require-auth` and dev convenience.** The device-flow routes use the strict mutation gate (PR 15), which means a token-less loopback default returns `401 token_required`. Locally, the simplest way around this during development is `matilda serve --token=dev-token`; you don't need `--require-auth` unless you're hardening the loopback default.
499
-
500
- **Cross-daemon limitation.** `oauth_creds.json` is daemon-shared (`~/.matilda/oauth_creds.json`), so a successful login in daemon A is automatically picked up by daemon B's next token refresh — but daemon B's SDK clients won't receive the `auth_device_flow_authorized` event (events are per-daemon).
501
-
502
- **Cross-client take-over.** Two SDK clients on the same daemon that both `POST /workspace/auth/device-flow` for the same provider get the per-provider singleton: the first call starts a fresh IdP request and returns `attached: false`; the second call returns the EXISTING in-flight entry with `attached: true`. The take-over is recorded on the audit trail (under the second client's `X-Matilda-Client-Id`) but does NOT emit a separate event — both clients eventually observe the SAME `auth_device_flow_authorized` once the user finishes the IdP page. If your UI distinguishes "I started this" from "someone else's flow I joined", branch on the `attached` field returned by `start()`.
503
-
504
- ## Daemon log file
505
-
506
- `matilda serve` writes a per-process diagnostic log to:
507
-
508
- ```
509
- ${MATILDA_RUNTIME_DIR or ~/.matilda}/debug/daemon/serve-<pid>-<workspaceHash>.log
510
- ```
511
-
512
- A `latest` symlink in the same directory always points at the current process's log, so `tail -f ~/.matilda/debug/daemon/latest` will follow whichever daemon is running.
513
-
514
- The log captures lifecycle messages, route errors (with `route=` and `sessionId=` context), ACP child stderr, and — when `MATILDA_SERVE_DEBUG=1` is set — extra bridge breadcrumbs. Lines that go to stderr today still go to stderr; the file log is **additive**, not a replacement.
515
-
516
- ### Disabling
517
-
518
- Set `MATILDA_DAEMON_LOG_FILE=0` (or `false`/`off`/`no`) to skip file logging entirely. Stderr output is unaffected.
519
-
520
- ### Relation to session debug logs
521
-
522
- Session-scoped debug logs (`~/.matilda/debug/<sessionId>.txt` and the `~/.matilda/debug/latest` symlink) are independent. The daemon log lives in a sibling `daemon/` subdirectory; per-session debug semantics are unchanged by this feature.
523
-
524
- ### No rotation
525
-
526
- The daemon log appends indefinitely. Rotate manually if it grows large. A future enhancement may add automatic rotation; track via [#4548](https://github.com/MaincodeHQ/matilda-code/issues/4548) follow-ups.
527
-
528
- ## Runtime MCP server management (issue [#4514](https://github.com/MaincodeHQ/matilda-code/issues/4514))
529
-
530
- Add or remove MCP servers at runtime without restarting the daemon. Runtime entries live in an ephemeral overlay that **shadows** settings-defined servers of the same name; the underlying `settings.json` / `mcpServers` config is never written to.
531
-
532
- **Pre-flight:** check `caps.features` for `mcp_server_runtime_mutation` before calling either route. Older daemons without this tag return `404`.
533
-
534
- ### `POST /workspace/mcp/servers` — add a runtime MCP server
535
-
536
- Strict-gated (bearer token required). Connects the server immediately via the live `McpClientManager` and discovers its tools.
537
-
538
- Request:
539
-
540
- ```json
541
- {
542
- "name": "my-server",
543
- "config": {
544
- "command": "npx",
545
- "args": ["-y", "@my-org/mcp-server"]
546
- }
547
- }
548
- ```
549
-
550
- `name` must be alphanumeric plus `_` and `-` (max 256 characters). `config` is the same MCP server configuration object used in `settings.json` `mcpServers` entries (transport-dependent fields: `command`/`args` for stdio, `url` for SSE/HTTP). Security-sensitive fields (`trust`, `env`, `cwd`, `oauth`, `headers`, `authProviderType`, `includeTools`, `excludeTools`, `type`) are stripped by the daemon and ignored.
551
-
552
- Response (200) — success:
553
-
554
- ```json
555
- {
556
- "name": "my-server",
557
- "transport": "stdio",
558
- "replaced": false,
559
- "shadowedSettings": false,
560
- "toolCount": 3,
561
- "originatorClientId": "client-1"
562
- }
563
- ```
564
-
565
- - `replaced: true` — a runtime entry with the same name already existed and the config fingerprint differs; old connection torn down, new one established. When the fingerprint matches (idempotent re-add), `replaced` is `false`.
566
- - `shadowedSettings: true` — a settings-defined server with the same name exists; the runtime entry now shadows it. The settings entry is untouched and re-emerges if the runtime entry is later removed.
567
- - `toolCount` — number of tools discovered on the newly connected server.
568
-
569
- Response (200) — soft refuse (budget warning mode):
570
-
571
- ```json
572
- {
573
- "name": "my-server",
574
- "skipped": true,
575
- "reason": "budget_warning_only"
576
- }
577
- ```
578
-
579
- Returned when `--mcp-budget-mode=warn` and adding the server would exceed the configured `--mcp-client-budget`. The server is NOT connected. Callers should surface the budget pressure to the user.
580
-
581
- Errors:
582
-
583
- | Status | Code | When |
584
- | ------ | ------------------------- | -------------------------------------------------------------------------------------------------- |
585
- | `400` | `invalid_server_name` | Name empty, exceeds 256 chars, or contains characters outside `[A-Za-z0-9_-]` |
586
- | `400` | `missing_required_field` | `config` missing or not a non-null object |
587
- | `400` | `invalid_client_id` | `X-Matilda-Client-Id` header present but not registered for this workspace |
588
- | `400` | `invalid_config` | Config shape rejected by the MCP transport validator |
589
- | `401` | `token_required` | No bearer token configured (strict gate) |
590
- | `409` | `mcp_budget_would_exceed` | `--mcp-budget-mode=enforce` and budget is full |
591
- | `502` | `mcp_server_spawn_failed` | Server process exited or timed out during connect; body carries `serverName`, `exitCode`, `stderr` |
592
- | `503` | `acp_channel_unavailable` | No live ACP child (no session has been created yet) |
593
-
594
- ### `DELETE /workspace/mcp/servers/:name` — remove a runtime MCP server
595
-
596
- Strict-gated. Disconnects the server and removes it from the runtime overlay. Idempotent — removing a name that was never added returns a skip response (not an error).
597
-
598
- The `:name` path parameter is the URL-encoded server name.
599
-
600
- Response (200) — success:
601
-
602
- ```json
603
- {
604
- "name": "my-server",
605
- "removed": true,
606
- "wasShadowingSettings": false,
607
- "originatorClientId": "client-1"
608
- }
609
- ```
610
-
611
- - `wasShadowingSettings: true` — the removed runtime entry was shadowing a settings-defined server of the same name. That settings entry is now un-shadowed and will be used on next discovery/restart.
612
-
613
- Response (200) — idempotent skip:
614
-
615
- ```json
616
- {
617
- "name": "ghost",
618
- "skipped": true,
619
- "reason": "not_present"
620
- }
621
- ```
622
-
623
- Returned when the name was not in the runtime overlay (it may still exist in settings — settings entries cannot be removed via this route).
624
-
625
- Errors:
626
-
627
- | Status | Code | When |
628
- | ------ | ------------------------- | ----------------------------------------------------------------------------- |
629
- | `400` | `invalid_server_name` | Name empty, exceeds 256 chars, or contains characters outside `[A-Za-z0-9_-]` |
630
- | `400` | `invalid_client_id` | `X-Matilda-Client-Id` header present but not registered for this workspace |
631
- | `401` | `token_required` | No bearer token configured (strict gate) |
632
- | `503` | `acp_channel_unavailable` | No live ACP child |
633
-
634
- ### Shadow semantics
635
-
636
- Runtime entries form an ephemeral overlay on top of settings-defined MCP servers:
637
-
638
- - **Adding** a runtime server with the same name as a settings entry **shadows** it — the runtime config takes precedence. The original settings entry is not modified.
639
- - **Removing** a runtime server that was shadowing a settings entry **un-shadows** it — the settings-defined config becomes active again on next connection.
640
- - **Daemon restart** loses all runtime entries. Only settings-defined servers survive across restarts. Runtime servers are session-lifetime scoped.
641
- - **`GET /workspace/mcp`** reports the merged view — both settings-defined and runtime servers appear in the `servers[]` array. There is no wire-level distinction between the two origins in the snapshot today.
642
-
643
- ### Events
644
-
645
- Both routes emit **workspace-scoped** SSE events (all active session buses receive them):
646
-
647
- | Event | Emitted when | Payload fields |
648
- | -------------------- | ------------------------------- | -------------------------------------------------------------------------------------- |
649
- | `mcp_server_added` | `POST` succeeds (not skipped) | `name`, `transport`, `replaced`, `shadowedSettings`, `toolCount`, `originatorClientId` |
650
- | `mcp_server_removed` | `DELETE` succeeds (not skipped) | `name`, `wasShadowingSettings`, `originatorClientId` |
651
-
652
- Skipped responses (`budget_warning_only`, `not_present`) do NOT emit events.
653
-
654
- Budget-related events from the existing `mcp_guardrail_events` surface (`mcp_budget_warning`, `mcp_child_refused_batch`) also fire when runtime additions cross the budget threshold.
655
-
656
- ## What's next
657
-
658
- - **Setting up a long-running daemon?** [Local launch templates (systemd / launchd / nohup / tmux)](./matilda-serve-deploy-local.md) for v0.16-alpha (local-only).
659
- - **Build a client?** See the [DaemonClient TypeScript quickstart](../developers/examples/daemon-client-quickstart.md) and the [HTTP protocol reference](../developers/matilda-serve-protocol.md).
660
- - **Reading the source?** Bridge code lives at `packages/cli/src/serve/`; SDK client at `packages/sdk-typescript/src/daemon/`.
661
- - **Tracking the roadmap?** Stage 1.5 / Stage 2 progress is tracked on issue [#3803](https://github.com/MaincodeHQ/matilda-code/issues/3803).