zidane 7.2.5 → 8.0.4

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 (376) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -6
  3. package/dist/{acp-D-tVyUjw.js → acp-DQDhi2dj.js} +230 -77
  4. package/dist/acp-DQDhi2dj.js.map +1 -0
  5. package/dist/acp-cli.d.ts +1 -1
  6. package/dist/acp-cli.js +9 -66
  7. package/dist/acp-cli.js.map +1 -1
  8. package/dist/acp.d.ts +10 -590
  9. package/dist/acp.d.ts.map +1 -1
  10. package/dist/acp.js +3 -3
  11. package/dist/{agent-D0zZnGbt.js → agent-DSLNmBp0.js} +14401 -13214
  12. package/dist/agent-DSLNmBp0.js.map +1 -0
  13. package/dist/agent.d.ts +1 -1
  14. package/dist/agent.js +3 -2
  15. package/dist/aliasing-lVFbm81n.js +217 -0
  16. package/dist/aliasing-lVFbm81n.js.map +1 -0
  17. package/dist/{anthropic-C2aole0G.js → anthropic-CD00LhUP.js} +1442 -1264
  18. package/dist/anthropic-CD00LhUP.js.map +1 -0
  19. package/dist/artifacts-CV2hUKnL.js.map +1 -1
  20. package/dist/{atomic-write-B7zr1ddF.js → atomic-write-Ijgf2Mhc.js} +13 -3
  21. package/dist/atomic-write-Ijgf2Mhc.js.map +1 -0
  22. package/dist/call-tool-dHlbGOIY.js +70 -0
  23. package/dist/call-tool-dHlbGOIY.js.map +1 -0
  24. package/dist/chat/pure.d.ts +372 -6
  25. package/dist/chat/pure.d.ts.map +1 -1
  26. package/dist/chat/pure.js +35 -244
  27. package/dist/chat/pure.js.map +1 -1
  28. package/dist/{approvals-DKaJZYlm.js → chat-JPJJ8uu5.js} +15879 -14631
  29. package/dist/chat-JPJJ8uu5.js.map +1 -0
  30. package/dist/chat.d.ts +4 -6
  31. package/dist/chat.js +23 -12
  32. package/dist/cli-shared-C-YCaglL.js +159 -0
  33. package/dist/cli-shared-C-YCaglL.js.map +1 -0
  34. package/dist/client-core-BQSyDALT.d.ts +2115 -0
  35. package/dist/client-core-BQSyDALT.d.ts.map +1 -0
  36. package/dist/codemode-worker.d.ts +1 -0
  37. package/dist/codemode-worker.js +155 -0
  38. package/dist/codemode-worker.js.map +1 -0
  39. package/dist/codemode.d.ts +352 -0
  40. package/dist/codemode.d.ts.map +1 -0
  41. package/dist/codemode.js +5 -0
  42. package/dist/connection-Dr-Js4yZ.d.ts +28 -0
  43. package/dist/connection-Dr-Js4yZ.d.ts.map +1 -0
  44. package/dist/connection-loop-gdh22LTI.js +112 -0
  45. package/dist/connection-loop-gdh22LTI.js.map +1 -0
  46. package/dist/context-segments-BHKlagL6.js +1016 -0
  47. package/dist/context-segments-BHKlagL6.js.map +1 -0
  48. package/dist/{context-transport-T_6AYIvy.js → context-transport-DSMTUsK1.js} +3 -3
  49. package/dist/{context-transport-T_6AYIvy.js.map → context-transport-DSMTUsK1.js.map} +1 -1
  50. package/dist/contexts/daytona.d.ts +19 -8
  51. package/dist/contexts/daytona.d.ts.map +1 -1
  52. package/dist/contexts/daytona.js +1 -440
  53. package/dist/contexts/docker.d.ts +36 -7
  54. package/dist/contexts/docker.d.ts.map +1 -1
  55. package/dist/contexts/docker.js +1 -456
  56. package/dist/contexts/e2b.d.ts +68 -13
  57. package/dist/contexts/e2b.d.ts.map +1 -1
  58. package/dist/contexts/e2b.js +1 -382
  59. package/dist/contexts/sandbox.d.ts +1 -1
  60. package/dist/contexts/sandbox.js +5 -0
  61. package/dist/contexts/sandbox.js.map +1 -1
  62. package/dist/{contexts-CRE9pw07.js → contexts-d7UolYot.js} +1059 -524
  63. package/dist/contexts-d7UolYot.js.map +1 -0
  64. package/dist/contexts.d.ts +3 -3
  65. package/dist/contexts.js +1 -1
  66. package/dist/cred-keys-GpBg71cP.js +105 -0
  67. package/dist/cred-keys-GpBg71cP.js.map +1 -0
  68. package/dist/{auth-BG2rVKSt.js → credentials-JttEuRON.js} +55 -206
  69. package/dist/credentials-JttEuRON.js.map +1 -0
  70. package/dist/{cursor-iojRXHF9.js → cursor-CdjaQTBW.js} +3 -3
  71. package/dist/{cursor-iojRXHF9.js.map → cursor-CdjaQTBW.js.map} +1 -1
  72. package/dist/daemon-cli.d.ts +1 -1
  73. package/dist/daemon-cli.js +3 -2
  74. package/dist/daemon-cli.js.map +1 -1
  75. package/dist/daytona-DG3omipZ.js +513 -0
  76. package/dist/daytona-DG3omipZ.js.map +1 -0
  77. package/dist/defaults-Cx3zvDOV.js +23 -0
  78. package/dist/defaults-Cx3zvDOV.js.map +1 -0
  79. package/dist/dispatcher-BMlu2BmZ.js +4941 -0
  80. package/dist/dispatcher-BMlu2BmZ.js.map +1 -0
  81. package/dist/docker-BZhMMJUp.js +727 -0
  82. package/dist/docker-BZhMMJUp.js.map +1 -0
  83. package/dist/e2b-CEVOdRib.js +480 -0
  84. package/dist/e2b-CEVOdRib.js.map +1 -0
  85. package/dist/edit-utils-EGosADZq.js +234 -0
  86. package/dist/edit-utils-EGosADZq.js.map +1 -0
  87. package/dist/environment-8wsKE2Ln.js +214 -0
  88. package/dist/environment-8wsKE2Ln.js.map +1 -0
  89. package/dist/eval-BNcvle_J.js +1595 -0
  90. package/dist/eval-BNcvle_J.js.map +1 -0
  91. package/dist/eval.d.ts +2 -2
  92. package/dist/eval.js +1 -1489
  93. package/dist/executor-Cx5-8yqi.js +621 -0
  94. package/dist/executor-Cx5-8yqi.js.map +1 -0
  95. package/dist/extension-authoring-DyGGmq9V.js +385 -0
  96. package/dist/extension-authoring-DyGGmq9V.js.map +1 -0
  97. package/dist/extensions.d.ts +2 -2
  98. package/dist/extensions.js +3 -3
  99. package/dist/{fetch-url-DX75gpL0.js → fetch-url-Ct2TjDWt.js} +4 -4
  100. package/dist/{fetch-url-DX75gpL0.js.map → fetch-url-Ct2TjDWt.js.map} +1 -1
  101. package/dist/{format-BF745pLL.js → format-C2NQEVUH.js} +39 -2
  102. package/dist/format-C2NQEVUH.js.map +1 -0
  103. package/dist/format-bytes-C7tfc3Zm.js +55 -0
  104. package/dist/format-bytes-C7tfc3Zm.js.map +1 -0
  105. package/dist/headless-C_rNUuYC.js +960 -0
  106. package/dist/headless-C_rNUuYC.js.map +1 -0
  107. package/dist/headless.d.ts +1 -1
  108. package/dist/headless.js +1 -413
  109. package/dist/{history-search-C4bk1Wz_.js → history-search-CcYzF8-0.js} +7 -3
  110. package/dist/history-search-CcYzF8-0.js.map +1 -0
  111. package/dist/history-search-worker.d.ts +1 -1
  112. package/dist/history-search-worker.js +1 -1
  113. package/dist/host-D7NrI03V.js +378 -0
  114. package/dist/host-D7NrI03V.js.map +1 -0
  115. package/dist/host-signals-BcP5QKUD.js +118 -0
  116. package/dist/host-signals-BcP5QKUD.js.map +1 -0
  117. package/dist/identity-CwfO8K-t.js +26 -0
  118. package/dist/identity-CwfO8K-t.js.map +1 -0
  119. package/dist/{index-BgN1T3lk.d.ts → index-BGe1fQT7.d.ts} +10987 -9320
  120. package/dist/index-BGe1fQT7.d.ts.map +1 -0
  121. package/dist/index-BVQsIKGn.d.ts +448 -0
  122. package/dist/index-BVQsIKGn.d.ts.map +1 -0
  123. package/dist/{index-C6NEd5y_.d.ts → index-CHABJd-L.d.ts} +3386 -502
  124. package/dist/index-CHABJd-L.d.ts.map +1 -0
  125. package/dist/{index-C37_ppWv.d.ts → index-D6a7qXpg.d.ts} +2 -3
  126. package/dist/index-D6a7qXpg.d.ts.map +1 -0
  127. package/dist/index.d.ts +6 -6
  128. package/dist/index.js +27 -23
  129. package/dist/{completion-core-BRo86R_L.js → inline-ghost-BVRQeGoL.js} +50 -2
  130. package/dist/inline-ghost-BVRQeGoL.js.map +1 -0
  131. package/dist/ipc/remote.d.ts +313 -0
  132. package/dist/ipc/remote.d.ts.map +1 -0
  133. package/dist/ipc/remote.js +4 -0
  134. package/dist/ipc.d.ts +6 -964
  135. package/dist/ipc.js +403 -4
  136. package/dist/ipc.js.map +1 -0
  137. package/dist/{isolation-Kfoof0MM.d.ts → isolation-D9L650W5.d.ts} +100 -15
  138. package/dist/isolation-D9L650W5.d.ts.map +1 -0
  139. package/dist/json-rpc--HXQti1E.js +350 -0
  140. package/dist/json-rpc--HXQti1E.js.map +1 -0
  141. package/dist/links-j8rED1sR.js +603 -0
  142. package/dist/links-j8rED1sR.js.map +1 -0
  143. package/dist/local-terminal-D8QThwo_.js +252 -0
  144. package/dist/local-terminal-D8QThwo_.js.map +1 -0
  145. package/dist/{logger--C1VQjwJ.js → logger-Gtqlayx8.js} +199 -173
  146. package/dist/logger-Gtqlayx8.js.map +1 -0
  147. package/dist/logs-view-CWG8aUAG.js.map +1 -1
  148. package/dist/{mcp-CgNd-f3F.js → mcp-_wecH9Qe.js} +608 -562
  149. package/dist/mcp-_wecH9Qe.js.map +1 -0
  150. package/dist/mcp.d.ts +2 -2
  151. package/dist/mcp.js +2 -2
  152. package/dist/media-sniff-UKijEaiQ.js.map +1 -1
  153. package/dist/{messages-2jcOA_5R.js → messages-BqiwcD2Q.js} +1017 -1014
  154. package/dist/messages-BqiwcD2Q.js.map +1 -0
  155. package/dist/{oauth-DxHO17JE.js → oauth-Px_pBB9W.js} +5 -5
  156. package/dist/{oauth-DxHO17JE.js.map → oauth-Px_pBB9W.js.map} +1 -1
  157. package/dist/{openai-compat-Dy56weK8.js → openai-compat-CFrpH7uq.js} +432 -589
  158. package/dist/openai-compat-CFrpH7uq.js.map +1 -0
  159. package/dist/orchestration-BCGmB8Ps.js +511 -0
  160. package/dist/orchestration-BCGmB8Ps.js.map +1 -0
  161. package/dist/output/stream-json.d.ts +1 -2
  162. package/dist/output/stream-json.d.ts.map +1 -1
  163. package/dist/output/stream-json.js +1 -1
  164. package/dist/output/terminal.d.ts +1 -2
  165. package/dist/output/terminal.d.ts.map +1 -1
  166. package/dist/output/terminal.js +13 -7
  167. package/dist/output/terminal.js.map +1 -1
  168. package/dist/{plugins-gbI7SBA6.js → plugins-DS7mahzZ.js} +20 -101
  169. package/dist/plugins-DS7mahzZ.js.map +1 -0
  170. package/dist/plugins.d.ts +1 -1
  171. package/dist/plugins.js +1 -2
  172. package/dist/prelude-Cxo4t74B.js +517 -0
  173. package/dist/prelude-Cxo4t74B.js.map +1 -0
  174. package/dist/presets.d.ts +1 -1
  175. package/dist/presets.js +1 -2
  176. package/dist/project-root-B8zADr4o.js +167 -0
  177. package/dist/project-root-B8zADr4o.js.map +1 -0
  178. package/dist/project-user-paths-BSgtrRxl.js +58 -0
  179. package/dist/project-user-paths-BSgtrRxl.js.map +1 -0
  180. package/dist/{prompts-Pqc3JdW_.d.ts → prompts-DYcvqShf.d.ts} +14 -11
  181. package/dist/prompts-DYcvqShf.d.ts.map +1 -0
  182. package/dist/protocol-DnhqqSmE.js +13 -0
  183. package/dist/protocol-DnhqqSmE.js.map +1 -0
  184. package/dist/providers/anthropic.d.ts +2 -2
  185. package/dist/providers/anthropic.js +1 -2
  186. package/dist/providers/arcee.d.ts +1 -1
  187. package/dist/providers/arcee.js +1 -1
  188. package/dist/providers/baseten.d.ts +1 -1
  189. package/dist/providers/baseten.js +1 -1
  190. package/dist/providers/cerebras.d.ts +1 -1
  191. package/dist/providers/cerebras.js +3 -2
  192. package/dist/providers/cerebras.js.map +1 -1
  193. package/dist/providers/local.d.ts +1 -1
  194. package/dist/providers/local.js +1 -1
  195. package/dist/providers/openai-compat.d.ts +1 -1
  196. package/dist/providers/openai-compat.js +1 -2
  197. package/dist/providers/openai.d.ts +1 -1
  198. package/dist/providers/openai.js +15 -12
  199. package/dist/providers/openai.js.map +1 -1
  200. package/dist/providers/openrouter.d.ts +1 -1
  201. package/dist/providers/openrouter.js +3 -2
  202. package/dist/providers/openrouter.js.map +1 -1
  203. package/dist/providers/xai.d.ts +1 -1
  204. package/dist/providers/xai.js +3 -3
  205. package/dist/{providers-BVhnFyiY.js → providers-XUR7F4Hi.js} +4 -4
  206. package/dist/{providers-BVhnFyiY.js.map → providers-XUR7F4Hi.js.map} +1 -1
  207. package/dist/providers.d.ts +1 -1
  208. package/dist/providers.js +4 -4
  209. package/dist/registry-D6ShBWJU.js +5234 -0
  210. package/dist/registry-D6ShBWJU.js.map +1 -0
  211. package/dist/remote-hosts-BtA_9ZGp.js +97 -0
  212. package/dist/remote-hosts-BtA_9ZGp.js.map +1 -0
  213. package/dist/restate.d.ts +2 -3
  214. package/dist/restate.d.ts.map +1 -1
  215. package/dist/restate.js +631 -572
  216. package/dist/restate.js.map +1 -1
  217. package/dist/run-summary-CklLiZ4y.js +288 -0
  218. package/dist/run-summary-CklLiZ4y.js.map +1 -0
  219. package/dist/{sandbox-BH0WQkXn.d.ts → sandbox-C5dR5DwQ.d.ts} +6 -3
  220. package/dist/sandbox-C5dR5DwQ.d.ts.map +1 -0
  221. package/dist/session/sqlite.d.ts +1 -2
  222. package/dist/session/sqlite.d.ts.map +1 -1
  223. package/dist/session/sqlite.js +1 -616
  224. package/dist/session-factory-BUGY5P40.js +1576 -0
  225. package/dist/session-factory-BUGY5P40.js.map +1 -0
  226. package/dist/{session-BqRP1hS7.js → session-xh9JXu6i.js} +94 -52
  227. package/dist/session-xh9JXu6i.js.map +1 -0
  228. package/dist/session.d.ts +1 -1
  229. package/dist/session.js +2 -2
  230. package/dist/settings-keys-Ckf29NeM.js +677 -0
  231. package/dist/settings-keys-Ckf29NeM.js.map +1 -0
  232. package/dist/shell-path-Dqaoow2J.js +412 -0
  233. package/dist/shell-path-Dqaoow2J.js.map +1 -0
  234. package/dist/{skills-BC3dodWp.js → skills-DKqZJm4X.js} +430 -470
  235. package/dist/skills-DKqZJm4X.js.map +1 -0
  236. package/dist/skills.d.ts +2 -2
  237. package/dist/skills.js +1 -2
  238. package/dist/source-mPExaUm8.js +381 -0
  239. package/dist/source-mPExaUm8.js.map +1 -0
  240. package/dist/sqlite-DV1DQzN2.js +698 -0
  241. package/dist/sqlite-DV1DQzN2.js.map +1 -0
  242. package/dist/{src-CkNQKFKr.js → src-BRMYRJib.js} +903 -1121
  243. package/dist/src-BRMYRJib.js.map +1 -0
  244. package/dist/{stdio-DPpBDPx5.js → stdio-C6YyoW3I.js} +4 -4
  245. package/dist/{stdio-DPpBDPx5.js.map → stdio-C6YyoW3I.js.map} +1 -1
  246. package/dist/{stdio-loader-Bbx19dMZ.js → stdio-loader-C830Vg-Z.js} +4 -4
  247. package/dist/{stdio-loader-Bbx19dMZ.js.map → stdio-loader-C830Vg-Z.js.map} +1 -1
  248. package/dist/system-prompt-MNVHatSD.d.ts.map +1 -1
  249. package/dist/timeout-DEaqslLO.d.ts.map +1 -1
  250. package/dist/todo-edits-Dn251YUP.js +1790 -0
  251. package/dist/todo-edits-Dn251YUP.js.map +1 -0
  252. package/dist/{turn-operations-DrbJlpq9.js → tool-formatters-IQFDrdrs.js} +1456 -2120
  253. package/dist/tool-formatters-IQFDrdrs.js.map +1 -0
  254. package/dist/tool-state-Dku6YqAB.js +989 -0
  255. package/dist/tool-state-Dku6YqAB.js.map +1 -0
  256. package/dist/tools/fetch-url.d.ts +1 -2
  257. package/dist/tools/fetch-url.d.ts.map +1 -1
  258. package/dist/tools/fetch-url.js +1 -2
  259. package/dist/tools/prompts.d.ts +1 -1
  260. package/dist/tools/prompts.js +7 -7
  261. package/dist/tools/prompts.js.map +1 -1
  262. package/dist/tools/web-search.d.ts +1 -2
  263. package/dist/tools/web-search.d.ts.map +1 -1
  264. package/dist/tools/web-search.js +2 -2
  265. package/dist/tools/web-search.js.map +1 -1
  266. package/dist/tools-_DnT3AGM.js +578 -0
  267. package/dist/tools-_DnT3AGM.js.map +1 -0
  268. package/dist/tools.d.ts +3 -3
  269. package/dist/tools.js +4 -2
  270. package/dist/tui.d.ts +534 -517
  271. package/dist/tui.d.ts.map +1 -1
  272. package/dist/tui.js +34933 -27243
  273. package/dist/tui.js.map +1 -1
  274. package/dist/turn-operations-B8sokwi5.js +333 -0
  275. package/dist/turn-operations-B8sokwi5.js.map +1 -0
  276. package/dist/turn-operations-DQX5ZZni.d.ts +3089 -0
  277. package/dist/turn-operations-DQX5ZZni.d.ts.map +1 -0
  278. package/dist/types-C7ZFOztE.d.ts +132 -0
  279. package/dist/types-C7ZFOztE.d.ts.map +1 -0
  280. package/dist/types-CH5XpIHw.js +114 -0
  281. package/dist/types-CH5XpIHw.js.map +1 -0
  282. package/dist/{types-CWgENVLY.js → types-Cu0ZGDu9.js} +10 -6
  283. package/dist/types-Cu0ZGDu9.js.map +1 -0
  284. package/dist/types-IqegbJEE.d.ts +624 -0
  285. package/dist/types-IqegbJEE.d.ts.map +1 -0
  286. package/dist/types.d.ts +4 -4
  287. package/dist/types.js +1 -1
  288. package/dist/url-D8HCl65b.js +1259 -0
  289. package/dist/url-D8HCl65b.js.map +1 -0
  290. package/dist/wasm-BVT-j5cl.js +33 -0
  291. package/dist/wasm-BVT-j5cl.js.map +1 -0
  292. package/dist/workspace-exec-stream-SV2R9d5S.js +355 -0
  293. package/dist/workspace-exec-stream-SV2R9d5S.js.map +1 -0
  294. package/dist/ws-CIHMvlSh.js +952 -0
  295. package/dist/ws-CIHMvlSh.js.map +1 -0
  296. package/dist/ws-client-N7NxzvQp.js +230 -0
  297. package/dist/ws-client-N7NxzvQp.js.map +1 -0
  298. package/dist/{xai-CazjStOK.js → xai-CvJTeLrI.js} +165 -5
  299. package/dist/xai-CvJTeLrI.js.map +1 -0
  300. package/dist/xdg-zweed0-t.js +117 -0
  301. package/dist/xdg-zweed0-t.js.map +1 -0
  302. package/docs/ACP.md +52 -0
  303. package/docs/ARCHITECTURE.md +103 -6
  304. package/docs/ARTIFACTS.md +8 -4
  305. package/docs/CHAT.md +165 -14
  306. package/docs/EXECUTION_CONTEXT.md +8 -0
  307. package/docs/EXTENSIONS.md +123 -2
  308. package/docs/HEADLESS.md +2 -0
  309. package/docs/IPC.md +775 -64
  310. package/docs/MACHINES.md +456 -0
  311. package/docs/PANELS.md +3 -0
  312. package/docs/README.md +4 -0
  313. package/docs/RESTATE.md +3 -2
  314. package/docs/RUN_IN_BACKGROUND.md +10 -2
  315. package/docs/SERVER.md +1085 -0
  316. package/docs/SKILL.md +6 -1
  317. package/docs/TERMINAL.md +75 -0
  318. package/docs/TUI.md +65 -14
  319. package/package.json +58 -26
  320. package/scripts/eval.ts +4 -0
  321. package/dist/acp-D-tVyUjw.js.map +0 -1
  322. package/dist/agent-D0zZnGbt.js.map +0 -1
  323. package/dist/anthropic-C2aole0G.js.map +0 -1
  324. package/dist/approvals-DKaJZYlm.js.map +0 -1
  325. package/dist/atomic-write-B7zr1ddF.js.map +0 -1
  326. package/dist/auth-BG2rVKSt.js.map +0 -1
  327. package/dist/completion-core-BRo86R_L.js.map +0 -1
  328. package/dist/contexts/daytona.js.map +0 -1
  329. package/dist/contexts/docker.js.map +0 -1
  330. package/dist/contexts/e2b.js.map +0 -1
  331. package/dist/contexts-CRE9pw07.js.map +0 -1
  332. package/dist/eval.js.map +0 -1
  333. package/dist/format-BF745pLL.js.map +0 -1
  334. package/dist/glob-DluQFSw1.js +0 -270
  335. package/dist/glob-DluQFSw1.js.map +0 -1
  336. package/dist/headless.js.map +0 -1
  337. package/dist/history-search-C4bk1Wz_.js.map +0 -1
  338. package/dist/index-BgN1T3lk.d.ts.map +0 -1
  339. package/dist/index-C37_ppWv.d.ts.map +0 -1
  340. package/dist/index-C6NEd5y_.d.ts.map +0 -1
  341. package/dist/ipc-BKVIIOqH.js +0 -331
  342. package/dist/ipc-BKVIIOqH.js.map +0 -1
  343. package/dist/ipc.d.ts.map +0 -1
  344. package/dist/isolation-Kfoof0MM.d.ts.map +0 -1
  345. package/dist/json-rpc-rSWMUh9b.js +0 -192
  346. package/dist/json-rpc-rSWMUh9b.js.map +0 -1
  347. package/dist/logger--C1VQjwJ.js.map +0 -1
  348. package/dist/mcp-CgNd-f3F.js.map +0 -1
  349. package/dist/messages-2jcOA_5R.js.map +0 -1
  350. package/dist/openai-compat-Dy56weK8.js.map +0 -1
  351. package/dist/output-cap-C9zukcCj.js +0 -100
  352. package/dist/output-cap-C9zukcCj.js.map +0 -1
  353. package/dist/panels-B6gRk4bf.js +0 -789
  354. package/dist/panels-B6gRk4bf.js.map +0 -1
  355. package/dist/plugins-gbI7SBA6.js.map +0 -1
  356. package/dist/prompts-Pqc3JdW_.d.ts.map +0 -1
  357. package/dist/run-summary-BU_NgwUT.js +0 -269
  358. package/dist/run-summary-BU_NgwUT.js.map +0 -1
  359. package/dist/sandbox-BH0WQkXn.d.ts.map +0 -1
  360. package/dist/session/sqlite.js.map +0 -1
  361. package/dist/session-BqRP1hS7.js.map +0 -1
  362. package/dist/shell-path-DfdXrGg9.js +0 -1549
  363. package/dist/shell-path-DfdXrGg9.js.map +0 -1
  364. package/dist/skills-BC3dodWp.js.map +0 -1
  365. package/dist/src-CkNQKFKr.js.map +0 -1
  366. package/dist/todo-edits-UOwgxGKu.js +0 -528
  367. package/dist/todo-edits-UOwgxGKu.js.map +0 -1
  368. package/dist/tool-formatters-DoRLnfuD.d.ts +0 -759
  369. package/dist/tool-formatters-DoRLnfuD.d.ts.map +0 -1
  370. package/dist/transcript-anchors-BeudMbXx.d.ts +0 -1762
  371. package/dist/transcript-anchors-BeudMbXx.d.ts.map +0 -1
  372. package/dist/turn-operations-C8bMBDl_.d.ts +0 -1258
  373. package/dist/turn-operations-C8bMBDl_.d.ts.map +0 -1
  374. package/dist/turn-operations-DrbJlpq9.js.map +0 -1
  375. package/dist/types-CWgENVLY.js.map +0 -1
  376. package/dist/xai-CazjStOK.js.map +0 -1
package/docs/SERVER.md ADDED
@@ -0,0 +1,1085 @@
1
+ # zidane-server
2
+
3
+ zidane-server runs a zidane chat host on another machine (a VPS, a home
4
+ server, a container). Sessions, settings, provider credentials and the
5
+ tools' working directories live on that machine; TUI windows and the
6
+ desktop app on your laptop attach to it over a WebSocket with a device
7
+ token. Runs keep going when the window closes, and any window you attach
8
+ later picks them up.
9
+
10
+ It is the same host the local daemon runs (see [IPC.md](./IPC.md)), served
11
+ over the network instead of a unix socket. Internals:
12
+ [IPC.md § Remote server](./IPC.md#remote-server-zidane-server).
13
+
14
+ - [Opt-in and kill switch](#opt-in-and-kill-switch)
15
+ - [Install](#install): [Docker image](#docker-image), [release binaries](#release-binaries), [from source](#from-source)
16
+ - [Deploy on Coolify](#deploy-on-coolify)
17
+ - [Configuration](#configuration)
18
+ - [TLS or a reverse proxy](#tls-or-a-reverse-proxy)
19
+ - [Tokens](#tokens)
20
+ - [Connecting](#connecting)
21
+ - [Security model](#security-model)
22
+ - [Settings sync](#settings-sync)
23
+ - [Operations](#operations)
24
+ - [Limitations](#limitations)
25
+
26
+ ## Opt-in and kill switch
27
+
28
+ Nothing connects to a remote host unless you say so: either an explicit
29
+ `--host` (or `ZIDANE_HOST_URL`) at launch, or Settings → Remote hosts
30
+ (experimental, off by default) and a server saved on Settings → Hosts.
31
+ `ZIDANE_NO_REMOTE=1` (or `zidane --no-remote`, which sets it for the
32
+ process) on the client wins over everything: the CLI refuses an explicit
33
+ `--host` with a message naming the switch, ignores a `ZIDANE_HOST_URL` from
34
+ the environment (one warning on stderr, then the window runs locally), and
35
+ the window behaves as if Remote hosts were off (no Hosts tab, saved hosts not
36
+ even read).
37
+
38
+ The server listens on loopback by default and refuses to bind any other
39
+ interface without TLS or an explicit `--behind-proxy`.
40
+
41
+ ## Install
42
+
43
+ Three ways, all the same server:
44
+
45
+ - [Docker image](#docker-image) from GitHub Container Registry
46
+ (recommended; plain Docker, Compose, Coolify),
47
+ - [release binaries](#release-binaries) for darwin and linux (arm64, x64),
48
+ - [from source](#from-source) (a checkout of this repository).
49
+
50
+ Every release (`vX.Y.Z` tag) publishes both, from the same commit and only
51
+ after the release workflow's gates passed (typecheck, lint, tests, binary
52
+ and image smoke tests).
53
+
54
+ ### Docker image
55
+
56
+ `ghcr.io/tahul/zidane-server`, for `linux/amd64` and `linux/arm64`:
57
+
58
+ ```bash
59
+ docker run -d --name zidane --restart unless-stopped \
60
+ -v zidane-data:/data -p 127.0.0.1:4280:4280 \
61
+ ghcr.io/tahul/zidane-server:7.2
62
+ # Create a token for each device (printed once):
63
+ docker exec zidane zidane-server token create --name laptop
64
+ ```
65
+
66
+ | Tag | What it points at |
67
+ |---|---|
68
+ | `7.2.5` | Exactly that release. Never moves. |
69
+ | `7.2` | The latest `7.2.x` patch release. **Recommended**: fixes without surprises. |
70
+ | `7` | The latest `7.x` release. |
71
+ | `latest` | The latest release. Fine to try things; pin `X.Y` or an exact version for anything you keep. |
72
+
73
+ A pre-release (`v8.0.0-beta.1`) is published under its own version only
74
+ (`8.0.0-beta.1`), never `X.Y`, `X` or `latest`. The image's
75
+ `org.opencontainers.image.version` label is the release version.
76
+
77
+ Each image carries a build provenance attestation (Sigstore, from the
78
+ release workflow) plus buildx's SLSA provenance and SBOM. Check that an
79
+ image was built by this repository's release workflow:
80
+
81
+ ```bash
82
+ gh attestation verify oci://ghcr.io/tahul/zidane-server:7.2.5 --owner tahul
83
+ ```
84
+
85
+ #### With Docker Compose
86
+
87
+ `server/docker-compose.selfhost.yml` runs the image with a named volume,
88
+ loopback-only port and an optional Caddy. From a checkout (or just that
89
+ file):
90
+
91
+ ```bash
92
+ # Server only, published on 127.0.0.1:4280 for a proxy you already run:
93
+ ZIDANE_SERVER_VERSION=7.2 docker compose -f server/docker-compose.selfhost.yml up -d
94
+
95
+ # Or with the bundled Caddy (automatic HTTPS; the domain must point at this machine):
96
+ ZIDANE_SERVER_VERSION=7.2 ZIDANE_DOMAIN=zidane.example.com docker compose -f server/docker-compose.selfhost.yml --profile caddy up -d
97
+
98
+ # Create a token for each device (printed once):
99
+ docker compose -f server/docker-compose.selfhost.yml exec zidane-server zidane-server token create --name laptop
100
+ ```
101
+
102
+ `ZIDANE_SERVER_VERSION` is the image tag (default `latest`); put it in a
103
+ `.env` file next to the command so every `docker compose` call agrees.
104
+
105
+ Then, on the laptop (save the token in a 0600 file first):
106
+
107
+ ```bash
108
+ zidane --host wss://zidane.example.com --token-file ~/.config/zidane/box.token
109
+ ```
110
+
111
+ #### What is in the image
112
+
113
+ The image (`server/Dockerfile`, one file for plain Docker, Compose and
114
+ Coolify) is a slim Debian (base images pinned by tag and digest) with
115
+ `git`, `openssh-client` (git over ssh, keys in `/data/.ssh`), `ripgrep`,
116
+ `bash`, `ca-certificates` and `tini`, a non-root `zidane` user (uid 1000),
117
+ the compiled server binary, and `/data` as the storage volume. Projects go
118
+ in `/data/workspaces`, the image's workspace root
119
+ (`ZIDANE_SERVER_WORKSPACE_ROOTS`): mount them there
120
+ (`- /home/me/code:/data/workspaces`) or clone them there from a session.
121
+ Its command is `start`, and its listening defaults are environment
122
+ variables (so a platform's UI can change them; a flag would beat them):
123
+ `ZIDANE_SERVER_HOST=0.0.0.0`, `ZIDANE_SERVER_BEHIND_PROXY=1` (for a
124
+ TLS-terminating proxy only) and `ZIDANE_SERVER_TRUSTED_PROXIES=private`
125
+ (see [TLS or a reverse proxy](#tls-or-a-reverse-proxy)). Its `HEALTHCHECK`
126
+ runs `zidane-server health --http` (`GET /healthz` on the server's port,
127
+ every 10 s after a 30 s start period); `STOPSIGNAL` is SIGTERM, and the
128
+ drain it starts is bounded by `ZIDANE_SERVER_DRAIN_TIMEOUT_MS` (default
129
+ 8 s, under Docker's default 10 s stop timeout; both Compose files set
130
+ `stop_grace_period: 30s`). Raise the drain only together with the stop
131
+ timeout (`docker stop -t`, `docker run --stop-timeout`,
132
+ `stop_grace_period`), or Docker kills the server mid-drain.
133
+
134
+ > **Migrating from an earlier image.** The image now defaults to
135
+ > `ZIDANE_SERVER_BEHIND_PROXY=1` and `ZIDANE_SERVER_TRUSTED_PROXIES=private`.
136
+ > A deployment that serves TLS from the server itself (`ZIDANE_SERVER_TLS_CERT`
137
+ > / `_TLS_KEY`, or `--tls-cert` / `--tls-key` in a custom command) must set
138
+ > `ZIDANE_SERVER_BEHIND_PROXY=0` and an empty `ZIDANE_SERVER_TRUSTED_PROXIES=`:
139
+ > TLS together with behind-proxy is a boot error ("TLS on the server and
140
+ > behind-proxy are exclusive"), since every direct TLS client would otherwise
141
+ > get a 403 for its missing `X-Forwarded-Proto`. The generic `PORT` variable
142
+ > is no longer read: use `ZIDANE_SERVER_PORT`.
143
+
144
+ The image and Compose run `--behind-proxy`: every upgrade must come
145
+ through the proxy with `X-Forwarded-Proto: https`, and a connection to the
146
+ published `127.0.0.1:4280` reaches the container from the Docker bridge
147
+ gateway, not from loopback. So connect through the proxy
148
+ (`wss://zidane.example.com`), use `docker exec <container> zidane-server
149
+ health` / `token …` for local operations (the unix socket, no token), or
150
+ run a separate loopback-only server without `--behind-proxy` for local
151
+ use. `GET /healthz` answers on the published port either way.
152
+
153
+ ### Release binaries
154
+
155
+ Each [GitHub release](https://github.com/Tahul/zidane/releases) carries
156
+ `zidane-server-v<version>-<os>-<arch>.tar.gz` for `darwin-arm64`,
157
+ `darwin-x64`, `linux-x64` and `linux-arm64` (the binary, `LICENSE`, this
158
+ directory's `README.md`, in a `zidane-server-v<version>-<os>-<arch>/`
159
+ directory) and a `SHA256SUMS` file. One self-contained executable, no
160
+ runtime to install. The linux builds need glibc (Debian, Ubuntu, Fedora…;
161
+ not Alpine/musl). The darwin builds are ad-hoc signed, not notarized:
162
+ downloaded with a browser, clear the quarantine flag first
163
+ (`xattr -d com.apple.quarantine zidane-server`); `curl` sets none.
164
+
165
+ ```bash
166
+ V=7.2.5 T=linux-x64 # darwin-arm64, darwin-x64, linux-x64, linux-arm64
167
+ curl -fLO "https://github.com/Tahul/zidane/releases/download/v$V/zidane-server-v$V-$T.tar.gz"
168
+ curl -fLO "https://github.com/Tahul/zidane/releases/download/v$V/SHA256SUMS"
169
+ grep " zidane-server-v$V-$T.tar.gz\$" SHA256SUMS | shasum -a 256 -c # or sha256sum -c
170
+ gh attestation verify "zidane-server-v$V-$T.tar.gz" --owner tahul # built by the release workflow
171
+ tar -xzf "zidane-server-v$V-$T.tar.gz"
172
+ sudo install -m 0755 "zidane-server-v$V-$T/zidane-server" /usr/local/bin/
173
+ zidane-server start # 127.0.0.1:4280
174
+ zidane-server token create --name laptop
175
+ ```
176
+
177
+ Run this way (loopback, no `--behind-proxy`), `zidane --host
178
+ ws://127.0.0.1:4280 --token-file …` works on the server itself without TLS
179
+ (`ws://` is accepted for loopback only). For other machines, use an SSH
180
+ tunnel (`ssh -L 4280:127.0.0.1:4280 box`, then the same command) or set up
181
+ TLS / a proxy below.
182
+
183
+ Any machine with `zidane` installed already has the server too:
184
+ `zidane server start` is `zidane-server start`.
185
+
186
+ ### From source
187
+
188
+ From a checkout of the repository:
189
+
190
+ ```bash
191
+ # The image, built locally (BuildKit; cross-compiles for another platform with --platform):
192
+ docker build -f server/Dockerfile -t zidane-server .
193
+ docker build --platform linux/amd64 -f server/Dockerfile -t zidane-server:amd64 .
194
+ # Both platforms at once need a docker-container builder (or the containerd image store):
195
+ docker buildx create --name multiarch --driver docker-container
196
+ docker buildx build --builder multiarch --platform linux/amd64,linux/arm64 -f server/Dockerfile .
197
+
198
+ # Compose, building the image from the checkout instead of pulling it:
199
+ docker compose -f server/docker-compose.selfhost.yml -f server/docker-compose.build.yml up -d --build
200
+
201
+ # The binary:
202
+ bun run server:build # → server/dist/zidane-server (this machine)
203
+ bun run scripts/build-server.ts linux-x64 # → server/dist/linux-x64/zidane-server
204
+ ./server/dist/zidane-server start # 127.0.0.1:4280
205
+ ./server/dist/zidane-server token create --name laptop
206
+
207
+ # The release archives, as the release workflow makes them (→ server/dist/release/):
208
+ bun run server:release:package
209
+ ```
210
+
211
+ ### Distribution
212
+
213
+ - **Docker image**: `ghcr.io/tahul/zidane-server`, built and pushed by
214
+ the release workflow's `server-image` job (`.github/workflows/release.yml`)
215
+ for `linux/amd64` and `linux/arm64` after the `release` job (every gate,
216
+ npm packages, GitHub release) succeeded. The Dockerfile's build stage
217
+ runs on the builder's platform and cross-compiles the binary for the
218
+ target one (Bun's `--compile --target`), so only the runtime stage's
219
+ `apt-get` runs under emulation. The `linux/amd64` image is loaded and
220
+ smoke-tested before anything is pushed. A rerun of the job pushes the
221
+ same tags again (an idempotent overwrite of that version).
222
+ - **Binary**: `scripts/build-server.ts` compiles `src/server/main.ts`
223
+ with `bun build --compile` for darwin and linux (arm64, x64). It checks
224
+ the import graph first and fails if the TUI, OpenTUI, a React renderer
225
+ or tree-sitter reaches it. `scripts/package-server-release.ts`
226
+ (`bun run server:release:package`) builds the four targets and writes
227
+ the release archives and `SHA256SUMS` (reproducible: fixed order, mtime
228
+ and owner). The release job (macOS, so darwin binaries get their ad-hoc
229
+ signature) builds, smoke-tests the darwin-arm64 binary and checks the
230
+ sums before the GitHub release exists, then attests and uploads them
231
+ right after it is created, before any npm publish.
232
+ - **npm**: `server/` is the `zidane-server` workspace package. Its
233
+ `zidane-server` launcher runs the lean binary when one is built next to
234
+ it (`server/dist/`) and reports the package's version (a stale build is
235
+ skipped with a warning), else `zidane server …` from `zidane-tui`'s
236
+ platform binary (same code, checked to be the same version). The server
237
+ runs in its own process group: one Ctrl-C or `systemctl stop` reaches
238
+ it once and drains it (a second signal at least a second later skips the
239
+ drain); if the launcher dies, the server notices (a pipe on its fd 3
240
+ closes) and shuts down gracefully. It is `private` for now: no separate
241
+ per-platform npm packages until there is demand.
242
+
243
+ **Maintainers, once**: GHCR creates the package private on its first push.
244
+ After the first release, open the package (github.com/Tahul → Packages →
245
+ `zidane-server` → Package settings): set *Change visibility* to
246
+ **Public**, and check it is linked to the repository (the image's
247
+ `org.opencontainers.image.source` label links it; otherwise *Connect
248
+ repository*). Until then, pulls without a login fail.
249
+
250
+ ## Deploy on Coolify
251
+
252
+ [Coolify](https://coolify.io) pulls the released image
253
+ (`ghcr.io/tahul/zidane-server`; or builds it from this repository), runs
254
+ it on its `coolify` Docker network and puts its Traefik in front: Traefik
255
+ terminates TLS (WebSockets included), sets `X-Forwarded-Proto` /
256
+ `X-Forwarded-For`, and proxies to the container's port. The image's
257
+ defaults are made for that: behind a proxy, proxies believed from any
258
+ private address (`private`; Traefik's address on the network is not fixed),
259
+ configuration by environment only, a health check that needs no `curl`.
260
+
261
+ **The assumption behind `private`**: the server's port is reachable only
262
+ through Traefik and the private Docker network. Leave Coolify's *Ports
263
+ Mappings* empty (a published host port bypasses Traefik, and any peer on a
264
+ private network could then claim `https`). To tighten it, set
265
+ `ZIDANE_SERVER_TRUSTED_PROXIES` to the `coolify` network's subnet
266
+ (`docker network inspect coolify -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'`
267
+ on the server). See [the caveats of `private`](#caveats-of-private).
268
+
269
+ **Without *Persistent Storage* on `/data`, every redeploy starts from an
270
+ empty data dir**: sessions, settings, provider keys, device tokens and the
271
+ audit log are gone. The image declares `/data` a `VOLUME`, but without a
272
+ mount Docker makes an anonymous volume per container, and Coolify creates
273
+ a new container on each deploy. Add the volume (step 4) before you store
274
+ anything. The server warns at start (`the data dir is not on a persistent
275
+ volume; it will be lost when the container is recreated`) when it runs in
276
+ a container and finds no named volume or host directory mounted over its
277
+ data dir.
278
+
279
+ ### 1. Create the application (Docker Image, recommended)
280
+
281
+ 1. *+ New* → **Docker Image**. *Image Name*:
282
+ **`ghcr.io/tahul/zidane-server:7.2`** (Coolify moves the tag into its
283
+ *Tag* field). Pin `X.Y` (patch releases only) or an exact version
284
+ (`7.2.5`); an empty tag means `latest`. See [the tags](#docker-image).
285
+ *Save*.
286
+ 2. *Configuration* → *General*: *Ports Exposes* **`4280`** (a Docker
287
+ Image application starts with `80`: replace it). *Ports Mappings*: empty.
288
+ 3. *Domains*: `https://zidane.example.com` (or a generated one). Traefik
289
+ handles the certificate.
290
+ 4. **Required:** *Persistent Storage* → *Add* → *Volume Mount*: name
291
+ `zidane-data`, *Destination Path* **`/data`**. Without it, every
292
+ redeploy loses all data (above). A *Directory Mount* works too, but the
293
+ host directory must be writable by uid 1000 (`chown 1000:1000 <dir>`).
294
+ 5. *Health check*: **leave Coolify's dashboard health check disabled**;
295
+ the image's `HEALTHCHECK` (`zidane-server health --http`) is used
296
+ (Coolify ignores the dashboard check while the image has one, and the
297
+ dashboard's HTTP check would need `curl` or `wget`, which the image does
298
+ not ship). To test by hand, in the application's *Terminal*:
299
+ `zidane-server health --http` (or `zidane-server health`, over the unix
300
+ socket).
301
+ *Stop timeout*: the server drains for up to 8 s
302
+ (`ZIDANE_SERVER_DRAIN_TIMEOUT_MS`), under the 10 s Docker gives a
303
+ container by default; if you raise the drain, raise the application's
304
+ stop grace period with it (where your Coolify version offers it; the
305
+ Compose build pack below sets `stop_grace_period: 30s`).
306
+ 6. *Environment Variables* (below). Mark secrets (the bootstrap token,
307
+ provider keys) as runtime-only (not *Available at Buildtime*; with the
308
+ Docker Image nothing is built, and with the Dockerfile build pack the
309
+ Dockerfile reads no build argument but `BUN_IMAGE` / `RUNTIME_IMAGE` /
310
+ `ZIDANE_VERSION`: secrets have no business in a build).
311
+ 7. *Deploy*. The log shows the image pulled, then `zidane server listening
312
+ on ws://0.0.0.0:4280` and `Stored the bootstrap token …`; the status
313
+ turns healthy within ~30 s; `https://zidane.example.com/healthz` answers
314
+ `ok`.
315
+
316
+ ### From source: Dockerfile build pack (alternative)
317
+
318
+ To deploy `main` (or your fork) rather than a release, let Coolify build
319
+ `server/Dockerfile`:
320
+
321
+ 1. *+ New* → *Public Repository* (or your fork through a GitHub App /
322
+ deploy key) → this repository, branch `main`.
323
+ 2. *Build Pack*: **Dockerfile**. *Base Directory*: **`/`**. *Dockerfile
324
+ Location*: **`/server/Dockerfile`** (the build context is the repository
325
+ root, filtered by the root `.dockerignore`).
326
+ 3. Then steps 2 to 7 above (*Ports Exposes* `4280`, domain, the `/data`
327
+ volume, health check, variables, deploy).
328
+
329
+ ### Environment variables
330
+
331
+ Everything has a working default; set what you need.
332
+
333
+ | Variable | Value |
334
+ |---|---|
335
+ | `ZIDANE_SERVER_BOOTSTRAP_TOKEN` | A first client token, so you can connect before you ever open a shell: run `zidane-server token generate` (or `zidane server token generate`, any machine with zidane; `docker run --rm <image> token generate`), paste the `zds_…` line. It stays visible in the container's configuration (Coolify's UI, `docker inspect`, terminal shells): revoke `bootstrap` once you have device tokens. See [Tokens](#tokens). |
336
+ | `ZIDANE_SERVER_BOOTSTRAP_TOKEN_FILE` | Instead of the above (not both): the path of a file holding the token, e.g. a secret file mounted with Coolify's *Persistent Storage* → *File Mount* (`/run/secrets/zidane-bootstrap`). Keeps the token out of the environment. |
337
+ | `ZIDANE_SERVER_NAME` | The name windows show (default: the container's hostname, which Docker makes the short container id unless the platform sets one; the Compose files set `zidane-server`). |
338
+ | `ANTHROPIC_API_KEY`, `OPENAI_CODEX_API_KEY`, `OPENROUTER_API_KEY`, … | Provider keys the server's agents use ([the list](#provider-keys)). Or set them later from a window. |
339
+ | `ZIDANE_SERVER_TRUSTED_PROXIES` | Default `private`; the `coolify` network's subnet to tighten. |
340
+ | `ZIDANE_SERVER_ALLOWED_ORIGINS` | Browser origins allowed to connect (default none; the TUI and the desktop app send none). |
341
+ | `ZIDANE_SERVER_PORT` | Only if you change *Ports Exposes* (then both must agree). The generic `PORT` is not read. |
342
+ | `ZIDANE_SERVER_DRAIN_TIMEOUT_MS` | How long SIGTERM waits for running turns (default `8000`, under Docker's 10 s stop timeout). |
343
+ | any other `ZIDANE_SERVER_*` | [Configuration](#configuration). |
344
+
345
+ ### Docker Compose build pack (alternative)
346
+
347
+ `server/docker-compose.yml` is the same deployment as a Compose
348
+ file, with Coolify's conventions: *Build Pack* **Docker Compose**, *Base
349
+ Directory* **`/`**, *Docker Compose Location*
350
+ **`/server/docker-compose.yml`**. It runs the released image,
351
+ `ghcr.io/tahul/zidane-server:${ZIDANE_SERVER_VERSION:-latest}` (set
352
+ `ZIDANE_SERVER_VERSION` to `7.2` or an exact version in its variables), and
353
+ declares `SERVICE_URL_ZIDANE_4280` (Coolify generates a URL from your wildcard
354
+ domain and routes it to port 4280; or enter your own domain on the
355
+ `zidane` service as `https://zidane.example.com:4280`), a named volume on
356
+ `/data`, the variables above as editable `${VAR}` / `${VAR:-default}`
357
+ entries, and the health check spelled out. No `ports:` and no Caddy.
358
+
359
+ ### 2. Connect
360
+
361
+ Save the bootstrap token in a 0600 file on your laptop, then:
362
+
363
+ ```bash
364
+ zidane --host wss://zidane.example.com --token-file ~/.config/zidane/coolify.token
365
+ ```
366
+
367
+ Or add it as a saved host: TUI Settings → Remote hosts on, Settings → Hosts
368
+ → `+ add a server`; desktop app Settings › General › Experimental › Remote
369
+ hosts, then Settings › Hosts › Add a server. URL
370
+ `wss://zidane.example.com`, the token.
371
+
372
+ ### 3. Per-device tokens, then retire the bootstrap one
373
+
374
+ In the application's *Terminal* (it opens as the `zidane` user):
375
+
376
+ ```bash
377
+ zidane-server token create --name laptop # printed once: store it on the laptop
378
+ zidane-server token create --name desktop
379
+ zidane-server token revoke bootstrap # drops its connections at once
380
+ ```
381
+
382
+ Then delete `ZIDANE_SERVER_BOOTSTRAP_TOKEN` in Coolify (or leave it: a
383
+ revoked value is never brought back; the server warns at start). Removing
384
+ the variable alone does not revoke the stored token.
385
+
386
+ ### Upgrading, backups
387
+
388
+ - **Upgrade**: with the Docker Image, bump the tag (*Configuration* →
389
+ *General* → *Docker Registry* → *Docker Image Tag*, e.g. `7.2` → `7.3`)
390
+ and *Redeploy*; on a moving tag (`7.2`, `7`, `latest`) a *Redeploy*
391
+ alone pulls the newest image under it (so does a *Restart*: pin an exact
392
+ version if you want neither to change it). Compose build pack: change
393
+ `ZIDANE_SERVER_VERSION`, *Redeploy*. Dockerfile build pack: *Redeploy*
394
+ (or automatic deployments on push) builds the branch again. The volume
395
+ carries over (only if you added one: step 4); the old container gets
396
+ SIGTERM, drains its runs for up to 8 s and exits 0; windows reconnect.
397
+ - **Backups**: the `/data` volume holds everything (sessions, settings,
398
+ credentials, tokens, audit log, workspaces). Use Coolify's storage
399
+ backups for the volume if your version offers them, or stop the
400
+ application and archive the volume on the server:
401
+ `docker run --rm -v <volume>:/data -v "$PWD":/backup debian:trixie-slim tar czf /backup/zidane-data.tgz -C /data .`
402
+ (`docker volume ls` shows the name Coolify gave it). Copying
403
+ `sessions.db` while the server runs can catch the WAL mid-write.
404
+
405
+ ## Configuration
406
+
407
+ Sources, highest first: flags, `ZIDANE_SERVER_*` environment variables,
408
+ `<data dir>/server.json`, defaults. The generic `PORT` variable is never
409
+ read (a `PORT` exported for something else must not move the server). Every value is validated at boot; an
410
+ error names the flag, variable or file key. An empty variable counts as
411
+ unset (`ZIDANE_SERVER_TRUSTED_PROXIES=` clears the image's default);
412
+ `ZIDANE_SERVER_BEHIND_PROXY=0` turns off what the image turns on.
413
+
414
+ | Setting | Flag | Environment | `server.json` | Default |
415
+ |---|---|---|---|---|
416
+ | Interface | `--host` | `ZIDANE_SERVER_HOST` | `host` | `127.0.0.1` |
417
+ | Port | `--port` | `ZIDANE_SERVER_PORT` | `port` | `4280` (`0`: any free port) |
418
+ | TLS certificate / key (PEM; not with behind-proxy: a boot error) | `--tls-cert` / `--tls-key` | `ZIDANE_SERVER_TLS_CERT` / `_TLS_KEY` | `tlsCert` / `tlsKey` (relative to the data dir) | none |
419
+ | Interface (image) | | | | `0.0.0.0` (the image sets `ZIDANE_SERVER_HOST`) |
420
+ | Behind a TLS proxy | `--behind-proxy` | `ZIDANE_SERVER_BEHIND_PROXY=1` (`0` off) | `behindProxy` | `false` (the image: on) |
421
+ | Trusted proxies (IPs / CIDRs / `private`; needs behind-proxy) | `--trusted-proxies <list>` | `ZIDANE_SERVER_TRUSTED_PROXIES` (comma-separated) | `trustedProxies` | none: any peer (a warning at start); the image: `private`; Compose: its network |
422
+ | Name shown to clients | `--name` | `ZIDANE_SERVER_NAME` | `name` | the machine's hostname; in a plain `docker run` or Coolify's Dockerfile build pack, the container's hostname (Docker's default: the short container id; set a name); both Compose files set `zidane-server` |
423
+ | Unix socket | `--socket <path\|off>` | `ZIDANE_SERVER_SOCKET` | `socket` | `<data dir>/server.sock` |
424
+ | Heartbeat (ms, `0` off) | | `ZIDANE_SERVER_HEARTBEAT_MS` | `heartbeatMs` | `20000` |
425
+ | Max message size (bytes): a bigger incoming frame closes the connection, a bigger reply is answered with an error (`ResponseTooLarge`; clients page what can be big) | | `ZIDANE_SERVER_MAX_MESSAGE_BYTES` | `maxMessageBytes` | 32 MiB |
426
+ | Rate limit per connection | | `ZIDANE_SERVER_RATE_LIMIT` / `_RATE_BURST` | `rateLimit: { perSecond, burst }` | 50/s, burst 200 (`0` off) |
427
+ | Browser origins allowed | | `ZIDANE_SERVER_ALLOWED_ORIGINS` (comma-separated) | `allowedOrigins` | none |
428
+ | Upgrades per client address | | `ZIDANE_SERVER_UPGRADE_RATE_LIMIT` / `_UPGRADE_RATE_BURST` | `upgradeRateLimit: { perSecond, burst }` | 5/s, burst 30 (`0` off) |
429
+ | Open connections per token | | `ZIDANE_SERVER_MAX_CONNECTIONS_PER_TOKEN` | `maxConnectionsPerToken` | 16 |
430
+ | Open connections per client address | | `ZIDANE_SERVER_MAX_CONNECTIONS_PER_ADDRESS` | `maxConnectionsPerAddress` | 32 |
431
+ | Workspace roots: where session folders may be (the first is the default) | `--workspace-root <dir>` (repeatable) | `ZIDANE_SERVER_WORKSPACE_ROOTS` (path-delimited, absolute) | `workspaceRoots` (relative to the data dir) | `<data dir>/workspaces` (`/data/workspaces` in the image) |
432
+ | Drain bound at shutdown (ms) | | `ZIDANE_SERVER_DRAIN_TIMEOUT_MS` | `drainTimeoutMs` | `8000` (fits Docker's 10 s stop timeout) |
433
+ | Lend this server as a machine to its clients' computers (`local@<server>`, [MACHINES.md § Reverse direction](./MACHINES.md#reverse-direction)) | `--allow-as-machine` | `ZIDANE_SERVER_ALLOW_AS_MACHINE=1` | `allowAsMachine` | off |
434
+ | Installs, updates and removals of skills, extensions and MCP servers from clients ([Installs from a client](#installs-from-a-client)) | `--no-remote-installs` (off) | `ZIDANE_SERVER_ALLOW_REMOTE_INSTALLS=0` | `allowRemoteInstalls` | on |
435
+ | Storage root | `--data-dir` | `ZIDANE_SERVER_DATA_DIR` | | zidane's usual user dir |
436
+ | Bootstrap client token ([Tokens](#tokens)) | | `ZIDANE_SERVER_BOOTSTRAP_TOKEN`, or `ZIDANE_SERVER_BOOTSTRAP_TOKEN_FILE` (a file holding it; not both) | | none |
437
+ | Log connections to stderr | `--verbose` | | | off |
438
+
439
+ `private` stands for every private address a proxy can have: `10.0.0.0/8`,
440
+ `172.16.0.0/12`, `192.168.0.0/16`, `127.0.0.0/8`, `::1`, `fc00::/7`,
441
+ `fe80::/10` (not CGNAT `100.64.0.0/10`: add it next to `private` if your
442
+ proxy is there). It mixes with addresses and CIDRs (`private,100.64.0.0/10`).
443
+
444
+ #### Caveats of `private`
445
+
446
+ `private` trusts every private address, so it is only as safe as the
447
+ network in front of the port:
448
+
449
+ - **Never publish the container's port without a `127.0.0.1:` binding**
450
+ while `private` is set (`-p 127.0.0.1:4280:4280`, not `-p 4280:4280`;
451
+ on Coolify, no *Ports Mappings* at all). Published on every interface,
452
+ any machine on your LAN, VPN or cloud VPC has a private address.
453
+ - **Rootless Docker, Docker Desktop and other setups that publish ports
454
+ through a userland proxy or source NAT** make every client, the internet
455
+ included, arrive from a private address (the proxy's or the bridge
456
+ gateway's). With them, `private` trusts everyone: set the proxy's exact
457
+ address instead, or keep the port off any network others reach.
458
+ - **What a peer wrongly trusted can do**: send its own
459
+ `X-Forwarded-Proto: https` and `X-Forwarded-For`, so the per-address
460
+ upgrade and connection limits can be dodged (a fresh forged address per
461
+ attempt) and the audit log records whatever address it claims. It still
462
+ needs a valid token to connect. It is not a token leak either: the
463
+ clients refuse `ws://` to anything but loopback, so a token never
464
+ travels in the clear to a non-loopback host because of this setting.
465
+
466
+ ### Provider keys
467
+
468
+ The server's agents read provider keys from its environment exactly as the
469
+ local daemon does (a key stored in the data dir's `credentials.json`, set
470
+ from a window or the server's own TUI, wins over the variable):
471
+ `ANTHROPIC_API_KEY`, `OPENAI_CODEX_API_KEY` (OpenAI; `OPENAI_API_KEY`
472
+ alone does not mark it configured), `OPENROUTER_API_KEY`,
473
+ `CEREBRAS_API_KEY`, `XAI_API_KEY`, `ARCEE_API_KEY`, `BASETEN_API_KEY`,
474
+ `MISTRAL_API_KEY`, `ZAI_API_KEY`, and for a local OpenAI-compatible
475
+ endpoint `LOCAL_LLM_BASE_URL` (+ `LOCAL_LLM_API_KEY`,
476
+ `LOCAL_LLM_DEFAULT_MODEL`, `LOCAL_LLM_CONTEXT_WINDOW`). The variables stay
477
+ in the agents' environment (their tools see them, as on a laptop).
478
+
479
+ `--data-dir /data` keeps everything under `/data/.zidane` (as
480
+ `ZIDANE_STORAGE_DIR` does). The image sets `ZIDANE_SERVER_DATA_DIR=/data`.
481
+
482
+ Commands:
483
+
484
+ ```
485
+ zidane-server start [flags] Serve the host (zidane server start).
486
+ zidane-server token create --name <name> Create a device token, printed once on stdout.
487
+ zidane-server token list List tokens (never their secrets).
488
+ zidane-server token revoke <id|name> Revoke a token; its live connections drop.
489
+ zidane-server token generate Print a fresh token, stored nowhere (for ZIDANE_SERVER_BOOTSTRAP_TOKEN).
490
+ zidane-server health [--timeout <ms>] daemon.info over the unix socket; exit 0 when healthy.
491
+ zidane-server health --http GET /healthz on the configured port (loopback for 0.0.0.0); exit 0 on 200.
492
+ zidane-server --version
493
+ ```
494
+
495
+ ## TLS or a reverse proxy
496
+
497
+ Tokens and transcripts must not cross a network in the clear, so a
498
+ non-loopback `--host` needs one of:
499
+
500
+ 1. **TLS in the server**: `--tls-cert` / `--tls-key`. Clients use
501
+ `wss://host:4280`. In Docker, mount the files and set
502
+ `ZIDANE_SERVER_TLS_CERT` / `_TLS_KEY`, `ZIDANE_SERVER_BEHIND_PROXY=0` and
503
+ an empty `ZIDANE_SERVER_TRUSTED_PROXIES` (see the commented block in
504
+ `server/docker-compose.selfhost.yml`). TLS and `--behind-proxy` together are a
505
+ boot error: the image turns behind-proxy on, so override it (also when
506
+ you pass `--tls-cert` / `--tls-key` in your own command).
507
+ 2. **A TLS-terminating reverse proxy** and `--behind-proxy`. The proxy
508
+ must upgrade WebSockets, set `X-Forwarded-Proto: https`, and not time
509
+ out idle streams (runs can be quiet for minutes; the 20 s heartbeat keeps
510
+ the connection busy, but give it room). Every upgrade must carry
511
+ `X-Forwarded-Proto` and every entry in it must be `https`; anything else
512
+ is a 403, from loopback too (a same-host proxy that forgot the header
513
+ would otherwise pass plain-HTTP clients through). Local tools use the
514
+ unix socket. Publish the server's port on loopback only
515
+ (`127.0.0.1:4280:4280`), so the proxy is the only way in.
516
+
517
+ Set `--trusted-proxies` (`trustedProxies`) to the proxy's addresses or
518
+ networks: upgrades from any other peer are refused (403), and only those
519
+ peers' forwarded headers are believed. Without it, every peer's
520
+ `X-Forwarded-*` headers are believed: anyone who reaches the port
521
+ directly can claim `https` and any client address (audit log,
522
+ per-address limits). The server says so at start (on stderr, after the
523
+ listening line, and as `warnings` in the audit log's `server.start`
524
+ entry), and so does `zidane server start`.
525
+
526
+ - **Caddy or nginx on the same host**, server run as a binary:
527
+ `--trusted-proxies 127.0.0.1,::1`.
528
+ - **The image behind a proxy on the host**: the proxy reaches the
529
+ published `127.0.0.1:4280` through the container network's gateway;
530
+ trust that network's subnet (`docker network inspect <network>`).
531
+ - **Compose** (`server/docker-compose.selfhost.yml`): its `zidane` network has a
532
+ fixed subnet, `172.30.80.0/24` (`ZIDANE_NETWORK_SUBNET` to change it),
533
+ and the server trusts exactly that network
534
+ (`ZIDANE_SERVER_TRUSTED_PROXIES` defaults to it): the bundled `caddy`
535
+ service, and a proxy on the host through the gateway. Any process on
536
+ the host that can reach `127.0.0.1:4280` comes in through that gateway
537
+ too, so it is trusted as a proxy: keep the published port on loopback.
538
+ - The image's own default (`docker run`, Coolify) is `private`: any
539
+ peer with a private address is believed, which holds when the port is
540
+ reachable only from the proxy's private network (published on
541
+ loopback, or not published at all as on Coolify; read
542
+ [the caveats](#caveats-of-private)). Tighten it with
543
+ `-e ZIDANE_SERVER_TRUSTED_PROXIES=<subnet>`; an empty value trusts
544
+ every peer (and warns).
545
+
546
+ The client address (audit log, per-address limits) is read from
547
+ `X-Forwarded-For`, from the right: the entry your proxy added, skipping
548
+ trusted proxies of a chain. Entries further left are whatever the client
549
+ sent. Have the proxy set the header to the peer it saw rather than
550
+ append to the client's (nginx below: `$remote_addr`, not
551
+ `$proxy_add_x_forwarded_for`); Caddy and Cloudflare already do the
552
+ right thing.
553
+
554
+ Caddy (handles upgrades, headers and timeouts on its own):
555
+
556
+ ```
557
+ zidane.example.com {
558
+ reverse_proxy 127.0.0.1:4280
559
+ }
560
+ ```
561
+
562
+ nginx:
563
+
564
+ ```nginx
565
+ location / {
566
+ proxy_pass http://127.0.0.1:4280;
567
+ proxy_http_version 1.1;
568
+ proxy_set_header Upgrade $http_upgrade;
569
+ proxy_set_header Connection "upgrade";
570
+ proxy_set_header Host $host;
571
+ proxy_set_header X-Forwarded-Proto $scheme;
572
+ proxy_set_header X-Forwarded-For $remote_addr;
573
+ proxy_read_timeout 1h;
574
+ proxy_send_timeout 1h;
575
+ proxy_buffering off;
576
+ }
577
+ ```
578
+
579
+ Cloudflare Tunnel (`cloudflared`, WebSockets are on by default; it sets
580
+ `X-Forwarded-Proto: https`):
581
+
582
+ ```yaml
583
+ ingress:
584
+ - hostname: zidane.example.com
585
+ service: http://127.0.0.1:4280
586
+ - service: http_status:404
587
+ ```
588
+
589
+ Cloudflare's proxy closes WebSockets idle for 100 s; the heartbeat
590
+ (every 20 s) keeps them alive, so leave `heartbeatMs` on.
591
+
592
+ ## Tokens
593
+
594
+ - `token create --name <device>` prints a `zds_…` token once, on stdout
595
+ (the message goes to stderr). Store it; it cannot be shown again.
596
+ - The server keeps only a SHA-256 of each token, in
597
+ `<data dir>/server-tokens.json` (mode 0600). `token list` shows id, name,
598
+ scope, created, last used and revoked, never secrets.
599
+ - Scope: `client` (open windows, the default) or `machine`
600
+ (`--scope machine`): a computer lending this server its folders and shell
601
+ (`zidane machine share`, see [MACHINES.md](./MACHINES.md)). A machine
602
+ token can only register a machine, never act as a window; revoking it
603
+ drops the machine (its sessions wait, then stop). Sharing a machine gives
604
+ this server a shell on it as the sharing user, inside the folders it
605
+ declared for file operations only.
606
+ - **`tokens.createMachineToken { name }`**: a connected client mints a
607
+ `machine` token here, so a window's "share this computer" is one step
608
+ (the TUI's and the desktop app's Hosts pages). Any client token may: in
609
+ the single-user model a client token is already an admin's (it can run
610
+ commands here through terminals and `sessions.workspaceExec`). The name
611
+ gets `-2`, `-3`, … when a live token holds it; audited as `token.create`
612
+ with `via: "tokens.createMachineToken"` and `createdBy` (the asking
613
+ token), refusals included. A client token may hold 16 live machine tokens
614
+ it minted (`createdBy` in `token list --json`); revoke some to mint more.
615
+ A machine id is bound to the token it first registered with: another
616
+ machine token can't take it over until that one is revoked or the
617
+ machine forgotten ([MACHINES.md § Security model](./MACHINES.md#security-model)).
618
+ The token is returned once, to
619
+ that client, and stored by its local host in its credentials
620
+ (`machine:<server id>`).
621
+ - `token revoke <id|name>` works while the server runs (the server watches
622
+ the file and re-reads it every 2 s); every connection the token opened is
623
+ dropped within seconds, and one whose upgrade was accepted just before is
624
+ refused when it opens.
625
+ - Last use is kept apart, in `server-tokens.usage.json`: the server never
626
+ rewrites the token file, so it can't undo a revocation the CLI just wrote.
627
+ - A token file that exists but can't be read or parsed is not "no tokens":
628
+ the running server keeps the last tokens it read, says so on stderr,
629
+ and picks the file up again once it is fixed; `token create` / `revoke`
630
+ refuse to write over it. A readable file without a token means that token
631
+ is revoked.
632
+ - Rotation: create a new token for the device, switch the device to it,
633
+ revoke the old one. One token per device makes revoking a lost laptop
634
+ painless.
635
+ - `token generate` prints a fresh token and stores nothing.
636
+ - **Bootstrap token** (`ZIDANE_SERVER_BOOTSTRAP_TOKEN`, for a platform
637
+ with no shell before the first deploy): at start the value must be a
638
+ well-formed token (`zds_` + 43 base64url characters; anything else is a
639
+ boot error that does not echo it). If no token with that value exists,
640
+ its hash is stored as the live `client` token named `bootstrap` (audited
641
+ as `token.create`); if a different `bootstrap` token is live, it is
642
+ revoked first (`token.revoke`; its connections drop), so changing the
643
+ variable rotates it. A value that was revoked is never brought back: the
644
+ server warns and ignores it. The server never prints it, and removes the
645
+ variable from its own environment at start, so processes it spawns from
646
+ then on (agents' tools, terminals) do not inherit it. **That does not make
647
+ it secret**: the value stays in the container's configuration, so
648
+ `docker inspect`, shells opened with `docker exec` or Coolify's
649
+ *Terminal* (they get the container's environment), the platform's UI, and
650
+ `/proc/1/environ` (readable by the server's own user, so by its agents'
651
+ commands too) all still show it. **Revoke it once device tokens exist**
652
+ (`token revoke bootstrap`); deleting the variable alone keeps the stored
653
+ token.
654
+
655
+ `ZIDANE_SERVER_BOOTSTRAP_TOKEN_FILE` is the alternative that keeps the
656
+ value out of the environment: the path of a file holding the token
657
+ (surrounding whitespace ignored), read at start with the same validation
658
+ and the same rotation rules. Mount it as a secret (Coolify: *Persistent
659
+ Storage* → *File Mount*, or Docker / Compose secrets under
660
+ `/run/secrets/`). Setting both variables is a boot error; an unreadable
661
+ file is one too. On Coolify, either works: the plain variable (simplest)
662
+ or a mounted secret file.
663
+
664
+ ## Connecting
665
+
666
+ ```bash
667
+ zidane --host wss://zidane.example.com --token-file ~/.config/zidane/box.token # the TUI, every session on the server
668
+ pass show zidane/box | zidane attach --host wss://… --token - # the thin client, token on stdin
669
+ ZIDANE_HOST_URL=wss://… ZIDANE_HOST_TOKEN=zds_… zidane # from the environment
670
+ ```
671
+
672
+ Prefer `--token-file <path>` (or `-` for stdin) or `ZIDANE_HOST_TOKEN`:
673
+ `--token zds_…` still works but puts the token in the process list and
674
+ your shell history, and the CLI says so on stderr. `--token -` reads stdin
675
+ up to end of file: from a terminal, paste the token and press Ctrl-D; piping
676
+ it suits `zidane attach` and scripts, not the TUI (which needs the terminal
677
+ on stdin; use `--token-file <(pass show …)` there).
678
+
679
+ `wss://` is required, except `ws://` to loopback (an SSH tunnel). The CLI
680
+ keeps the URL and token in memory and removes `ZIDANE_HOST_URL` and
681
+ `ZIDANE_HOST_TOKEN` from its environment, so tools it spawns inherit
682
+ neither. The footer shows `host: <name>`. `zidane run` (the TUI with a
683
+ prompt) resolves the host the same way; headless runs (`zidane run --json`,
684
+ `-p … --json-schema`) always run on this machine: they refuse `--host`, and
685
+ ignore (and scrub) `ZIDANE_HOST_URL` with a warning.
686
+
687
+ ### From the TUI's Hosts page
688
+
689
+ Turn on Settings → Remote hosts, then Settings → Hosts → `+ add a server`:
690
+ the URL (`wss://…`, or `ws://` to loopback), then the token (masked). The
691
+ window keeps a connection to every saved server next to This computer:
692
+ the sessions list groups sessions by host, opening one runs it on its
693
+ host, and a new session asks which host (and, on a server, which of its
694
+ folders) first. While a server session is open, Settings edits that
695
+ server's settings. Details: [TUI.md § Several hosts](./TUI.md#several-hosts-remote-hosts).
696
+
697
+ Saved hosts: `<user dir>/hosts.json` on the client (0600; names, URLs,
698
+ the server's id and version), with each token in the client's
699
+ `credentials.json` under `hosts:<id>`, never in `hosts.json`. Adding a host
700
+ connects first and saves nothing unless the server answers with the token.
701
+ `--host` / `ZIDANE_HOST_URL` stay ad hoc and are never saved; with Remote
702
+ hosts on, such a server is the window's default host next to This
703
+ computer and the saved ones.
704
+
705
+ ### From the desktop app's Hosts page
706
+
707
+ Same opt-in (Settings › General › Experimental › Remote hosts), same saved
708
+ hosts (the GUI and the TUI share `hosts.json` and the tokens), then
709
+ Settings › Hosts › Add a server: URL, token (masked), Test, Add. The app
710
+ keeps a connection to every saved server next to This computer: the
711
+ sidebar groups sessions by host, a new chat picks its host (and a server
712
+ folder) first, a server session's chat header says which server it runs
713
+ on, and while one is open Settings edits that server's settings. A session
714
+ is known by its host and its id together, so a server that shares this
715
+ computer's data dir lists the same session ids as two sessions, each
716
+ opened and edited on its own host. Details:
717
+ [IPC.md § GUI several hosts](./IPC.md#gui-several-hosts).
718
+
719
+ A refused token fails the connect with `token refused (401)` or `forbidden
720
+ (403): …`, not an opaque WebSocket error: the client asks the server why
721
+ over plain HTTP. The messages are fixed texts, never the response body (a
722
+ proxy may echo the request, the token included); a 403 says why only when
723
+ the server labels it (`X-Zidane-Refusal`: `origin`, `insecure-transport`,
724
+ `untrusted-proxy`, `scope`), else it is a generic "refused by the server
725
+ or a proxy in front of it". That request never follows a redirect: a 3xx
726
+ fails the connect ("the server redirects"), and the token goes to the URL's
727
+ own origin only. Connecting gives up after 15 s.
728
+
729
+ Host URLs carry no credentials: a URL with `user:pass@`, a query string or
730
+ a fragment is refused ("put the token in the token field"), and messages
731
+ name a host by scheme, host and path only. A `hosts.json` entry saved
732
+ before this check is read without them (it then fails auth: add it again).
733
+
734
+ A saved server is known by the id it answered with when it was added. If
735
+ another server answers at its URL later (reinstalled, or the name now
736
+ points elsewhere), the window closes that connection at once, shows the
737
+ host as `different server` ("A different server answers at <url>") and
738
+ does not retry; nothing that server says is recorded under the saved
739
+ host. Remove the host and add it again if the server was reinstalled.
740
+
741
+ ## Security model
742
+
743
+ - **Transport**: TLS (in the server or the proxy). Plain WebSocket only on
744
+ loopback.
745
+ - **Auth**: a live `client` token on every upgrade (`Authorization: Bearer`,
746
+ or a subprotocol entry for clients that can't set headers). Missing,
747
+ unknown and revoked tokens get the same 401, before any JSON-RPC. The
748
+ subprotocol fallback puts the token in `Sec-WebSocket-Protocol`, which
749
+ proxies may log: prefer clients that send `Authorization` (the CLI does,
750
+ under Bun), or have the proxy keep that header out of its logs.
751
+ Tokens are hashed at rest and compared in constant time; logs and the
752
+ audit log name tokens by id and name only.
753
+ - **Origins**: browsers send `Origin`, CLI clients don't. An upgrade with
754
+ an origin not in `allowedOrigins` (empty by default) is a 403, so a web
755
+ page can't reach the server with a stolen token.
756
+ - **Limits**: frames over `maxMessageBytes` close the connection (1009);
757
+ a reply over it is answered with a `ResponseTooLarge` error instead (the
758
+ connection stays); a client that stops reading is dropped once more than
759
+ 8 MiB of pushed signals wait for it (it reconnects and resumes);
760
+ a token bucket per connection rate-limits requests (JSON-RPC error
761
+ -32012, the connection survives; a large over-budget message is refused
762
+ without being parsed). Upgrades are rate-limited per client address
763
+ (429), and open connections capped per token and per client address
764
+ (429).
765
+ - **Audit log**: `<data dir>/server-audit.jsonl` (0600, rotated past 5 MB):
766
+ connects and disconnects (token id and name, address, client version,
767
+ reason), token create and revoke, server start and stop, settings sync
768
+ (`sync.claim` / `push` / `reset` / `release`: device, category, counts,
769
+ never a value; [Settings sync](#settings-sync)), and every
770
+ install, update or removal from a client (`catalog.install` /
771
+ `catalog.remove` / `catalog.update` / `mcp.upsert` / `mcp.remove`: kind,
772
+ spec or name, `ok`, the error or `refused`, token id and name; never an
773
+ MCP server's secrets). Refused upgrades
774
+ (address, reason, the forwarded client behind a proxy) go to
775
+ `server-audit.failures.jsonl`, so a flood can't rotate the entries above
776
+ away: the first refusal per address and minute in full, the rest of the
777
+ minute as one `auth.failure.summary` (count by reason). Writes are
778
+ buffered and asynchronous, flushed at shutdown.
779
+ - **What crosses the wire**: requests and signals of the chat host
780
+ protocol: prompts, transcripts and streamed output, tool calls and their
781
+ results, settings, approvals. The server's agents use the server's
782
+ provider credentials (its environment, e.g. `ANTHROPIC_API_KEY`, or the
783
+ credentials in its data dir); the window's own credentials and
784
+ keybindings stay on the window's machine. A client can set and remove
785
+ the server's provider API keys (`auth.*`): keys go in, never come back
786
+ out through `auth.*`, and are not logged. OAuth sign-ins run on the
787
+ server with the browser on the client ([Sign-in from a
788
+ client](#sign-in-from-a-client)): tokens never cross the wire. MCP
789
+ servers' secrets are redacted from `catalog.mcps` (a best effort over
790
+ free-form arguments: [IPC.md](./IPC.md)).
791
+ - **A token is shell access to the server.** Whoever holds a client token
792
+ can run commands there as the server's user (the agent's shell tool, `!`
793
+ commands, terminals), so they can read anything that user can read,
794
+ provider keys and the token hashes included. The workspace file methods
795
+ (`sessions.workspaceReadFile` / `WriteFile` / `ListFiles` / `FileSize`,
796
+ the files panel) and `sessions.workspaceExec`'s working directory are
797
+ confined to the workspace roots (real paths, symlinks followed) and never
798
+ reach the data dir (`credentials.json`, tokens, audit log), even when a
799
+ root contains it; a worktree the server made for the session is allowed.
800
+ That keeps credentials out of casual reach, not out of a determined
801
+ client's: commands the agent or the user runs are not confined. Treat a
802
+ token like an SSH key, and run the server as a user whose files you are
803
+ ready to expose.
804
+ - **`allowAsMachine` does not widen that.** With it on, any client token
805
+ can also ask this server to be a machine of the client's computer
806
+ (`machine.attach`, then `machine.call` for each command or file
807
+ operation), served by the same confined executor a shared machine runs:
808
+ paths and `cwd` real-path inside the workspace roots, never the data
809
+ dir nor zidane's other folders or the credential folders the sandbox
810
+ hides (even when an operator root contains them), its commands under
811
+ this server's own `sandbox*` settings. Each executor is keyed by the
812
+ asking token and its host id (another token never reaches one), at most
813
+ 4 per token. That is
814
+ what terminals and `sessions.workspaceExec` already let the same token
815
+ do. Off by default all the same: a server you only use as a host has no
816
+ reason to answer it. Every `machine.*` method is refused while it is off
817
+ (`allowAsMachine is off`), and clients see it in `ipc.hello`
818
+ (`machineServe: true`).
819
+ - **Unix socket**: `<data dir>/server.sock`, mode 0600, unauthenticated,
820
+ like the local daemon's. In the image it lives on `/data` and only the
821
+ `zidane` user (and `docker exec`) can reach it; `zidane-server health`
822
+ uses it. `--socket off` removes it (the image's healthcheck, over HTTP,
823
+ keeps working).
824
+ - **`GET /healthz`** (and `HEAD`) on the server's port answers `200 ok`
825
+ to anyone, before every check above: no token, no proxy headers, no rate
826
+ limit, no audit line, no host details (no name, no version); `503` while
827
+ shutting down, `405` for other methods. It says the listener is up.
828
+ - **Where tools run**: on the server, as the server's user, in the
829
+ session's working directory, which must be inside a workspace root. The
830
+ host checks the roots at every entry point that takes a folder:
831
+ `sessions.create`, folder changes, orchestration workers (the
832
+ `orchestration.createWorker` request and the model's `sessions` tool,
833
+ verb `create`), `environments.list`, `commands.list` / `completeArgs`,
834
+ `catalog.*`, `models.list`; a launch-override folder outside them is
835
+ dropped (with `ZIDANE_DEBUG` set, a line names the session and the
836
+ path), folder listings stay inside them, symlinks resolved and paths
837
+ compared in their canonical spelling (case-insensitive filesystems). A session
838
+ without a folder (the orchestration main) works in the first root. That
839
+ guards against mistakes, not a hostile client (see above).
840
+
841
+ ## Settings sync
842
+
843
+ A window can copy this computer's settings to a saved server, one way:
844
+ nothing comes back. Per saved server: TUI Settings → Hosts → the server →
845
+ `ctrl+s`; desktop app Settings › Hosts → the server's card → Sync. Only with
846
+ Remote hosts on: off, none of it loads and no file is watched.
847
+
848
+ - **Modes**: *Off* (the default), *Import once* (push the selected
849
+ categories now), *Keep in sync* (push them as they change: watchers on the
850
+ files below, plus the window's own settings changes).
851
+ - **Categories**, ticked per server:
852
+
853
+ | Category | What goes | How it lands on the server |
854
+ |---|---|---|
855
+ | Agent settings | Host-scoped settings (safe mode, run limits, compaction, interaction mode, subagent models…), never the window's own (`isWindowOnlySetting`), nor what describes this computer: the sandbox (every `sandbox*` key: syncing would let a client turn the server's sandbox off), `useDaemon`, `remoteHosts`, `environments`, nor the enabled lists (`isSyncableSetting`; the server skips them too) | `settings.set` |
856
+ | Safe-mode allowlists | Each project's safelist (`projects.json`) | For the projects the server has: the same path inside a workspace root, else the one folder of that name directly in a root; others are skipped. The server's own entries stay; entries this computer dropped go |
857
+ | `SYSTEM.md` and user skills | The user-scope `SYSTEM.md`, the user-scope skills (`~/.agents/skills`, `~/.zidane/skills`), as files | Written in the server user's `~/.zidane/` (skills replaced whole, staged then swapped). A skill dropped here is removed there |
858
+ | MCP servers | The user-scope `mcps.json` servers, their `env` and `headers` included | `mcp.upsert` / `mcp.remove` (behind `allowRemoteInstalls`). A stdio server whose command is not on the server's PATH comes back with a warning: it may not run there |
859
+ | Extensions | Those installed from npm or GitHub (their `.install.json`); local ones are listed as not syncable | `catalog.install` (behind `allowRemoteInstalls`, audited). Installed once; never updated or removed by sync |
860
+ | Provider API keys | Off by default. Each key is opted in on its own, after a warning: it copies a secret to another machine | `auth.setApiKey`, only for providers the device opted in (the server checks) |
861
+
862
+ MCP OAuth tokens never sync (they are bound to the device): sign in on
863
+ the server from the window ([Sign-in from a client](#sign-in-from-a-client)).
864
+ - **One source per server.** The server keeps a record (`sync.json`, 0600):
865
+ the device that syncs to it (id and name) and the token it claimed with,
866
+ its categories and opted-in keys, the fields it set, the overrides.
867
+ Another device sees "synced from <device>" and pushes nothing until it
868
+ takes over (asks first); a takeover drops the previous device's marks,
869
+ not the values. The source is the device id AND that token: a device id
870
+ is the client's word, so the same id over another token is another
871
+ source and must take over too. This is coordination between one user's
872
+ devices, not a boundary between users (any client token can take over;
873
+ it can already run commands here).
874
+ - **Synced fields are marked.** The server's settings, in any window, say
875
+ "synced from <device>" next to a synced setting.
876
+ - **Overrides.** A synced field edited on the server (a window's
877
+ `settings.set`, `mcp.*`, `catalog.remove`, `auth.*`; or on disk: its value
878
+ no longer matches what sync wrote) is an override: pushes skip it. The
879
+ setting says so ("edited here, not synced"); reset it there (TUI `ctrl+r`
880
+ on the row, desktop app *Sync again*) or on the Sync page, and the next
881
+ push (sent at once by the source) writes it again.
882
+ - **No sync back** in this version.
883
+ - **Offline.** Changes wait on this computer (`hosts-sync.json`: the
884
+ categories still to push, at most one entry each, the newest value
885
+ winning since there is one source) and go when the server reconnects, also
886
+ after a restart.
887
+ - **Off, or removing the server**, keeps what the server has; the server
888
+ stops naming this device as its source.
889
+
890
+ On this computer: `<user dir>/device.json` (0600: a random device id made
891
+ once, the host name) and `<user dir>/hosts-sync.json` (0600: per saved
892
+ server, mode, categories, opted-in providers, what waits, a hash of what
893
+ was last pushed, the last outcome; never a key).
894
+
895
+ On the server: pushes are bounded (a skill: 200 files, 1 MiB a file, 4 MiB
896
+ in all; 16 MiB of files a push; `SYSTEM.md` 256 KiB; settings 256 KiB),
897
+ skill names are plain names and file paths plain relative paths (no `..`,
898
+ no absolute path, written inside the skill's folder only; a skill folder
899
+ that is a link is left alone). Every `sync.claim`, `sync.push`, `sync.reset`
900
+ and `sync.release` is in the audit log with its token (category, device,
901
+ counts; never a value or a key). A token can already run commands on the
902
+ server, so sync adds no privilege; extension and MCP installs still follow
903
+ `allowRemoteInstalls`.
904
+
905
+ ## Sign-in from a client
906
+
907
+ OAuth accounts (providers such as Claude Pro/Max, ChatGPT, xAI, Cursor, and
908
+ MCP servers that use OAuth) are created on the server from a window: the
909
+ browser opens on the client, the tokens land in the server's
910
+ `credentials.json` / `mcp-credentials.json`. TUI: Settings › Authentication,
911
+ `o` on a provider; MCP servers tab, `o` on an HTTP server. Desktop app:
912
+ Settings › Authentication › Sign in, and Sign in on an MCP server row.
913
+
914
+ - **Loopback flows** (Anthropic, OpenAI Codex, xAI, MCP servers): the server
915
+ starts the login (`auth.oauth.begin`) and keeps the PKCE verifier and the
916
+ `state`; the authorization URL sends the browser back to a loopback
917
+ address on the CLIENT, which listens there and forwards the code
918
+ (`auth.oauth.complete`); the server checks the `state` and exchanges the
919
+ code. A provider's redirect is fixed (Anthropic `localhost:53692`, Codex
920
+ `localhost:1455`, xAI `127.0.0.1:56121`): when that port is busy on the
921
+ client, the window says so and takes the address the browser lands on,
922
+ pasted. An MCP server's redirect is any free loopback port the client
923
+ picks (registered by the server through dynamic client registration).
924
+ The server binds no callback port of its own.
925
+ - **Paste-back** (Anthropic's code page, any flow whose browser can't reach
926
+ the loopback): the code (or the whole address) is pasted in the window
927
+ and sent the same way.
928
+ - **Device-code and polling flows** (Cursor, third-party providers): the
929
+ window shows the code and URL; the server polls.
930
+ - **A provider's questions** (a choice between flows, a free-text prompt)
931
+ are the window's to answer, never picked by the server: `begin` /
932
+ `complete` answer with a `step`, the window shows it and answers with
933
+ `auth.oauth.respond`. A window that can't show one (today's TUI and
934
+ desktop app for a choice; a prompt goes to their paste box) ends the
935
+ sign-in saying so.
936
+ - **Providers that would listen on the server**: a sign-in whose provider
937
+ doesn't honor `callbackServer: false` (pi-ai's stock flows, a third
938
+ party's loopback flow) would bind a port on the server that the client's
939
+ browser can't reach: it is refused ("sign in on the server machine or
940
+ use an API key"). zidane's Anthropic, Codex and xAI flows honor it;
941
+ paste, device and polling flows never listen.
942
+ - **Security**: a flow has a random id, belongs to the connection that
943
+ began it (another connection's `complete` / `cancel` is refused), takes
944
+ one code, ends on a `state` mismatch, and expires after 10 minutes; a
945
+ dropped connection abandons its flows. Codes and tokens are never logged
946
+ and never sent to the client: `complete` answers with the provider's
947
+ status (or the MCP server's name). The server never opens a browser.
948
+
949
+ ## Installs from a client
950
+
951
+ > **On by default, deliberately.** A client token can already run any
952
+ > command on the server (terminals, `sessions.workspaceExec`), so installs
953
+ > add no privilege; they make the server's catalog manageable from a
954
+ > window. Turn it off when windows should not change the server user's
955
+ > skills, extensions and MCP servers: `--no-remote-installs`,
956
+ > `ZIDANE_SERVER_ALLOW_REMOTE_INSTALLS=0`, or `"allowRemoteInstalls": false`
957
+ > in `server.json`.
958
+
959
+ With `allowRemoteInstalls` on (the default), a window installs skills and
960
+ extensions (from GitHub or npm), updates extensions, removes them, and adds
961
+ or removes MCP servers on the server, with the same forms as for This
962
+ computer (TUI: `i` / ctrl+u / ctrl+x / `a` on the server's tabs; desktop
963
+ app: the server's Skills / Extensions / MCP servers panels). Every window
964
+ attached to the server re-reads the catalog (`catalog:changed`), and the
965
+ server rebuilds its agents.
966
+
967
+ - **User scope only**: what is installed goes to the server user's
968
+ `~/.agents/*` / `~/.zidane/*` (`HOME`, `/data` in the image). A project's
969
+ skills, extensions and MCP servers are files in its folder (inside the
970
+ workspace roots), which the agent and the files panel already edit;
971
+ only user-scope entries are removable from a client.
972
+ - **Specs**: GitHub (`owner/repo[#ref][:dir]`, GitHub and skills.sh URLs) and
973
+ npm only; anything path-like (`./x`, `/x`, `~`, `..`, `file:`) is refused.
974
+ An install runs `git` / the package registry ON THE SERVER, bounded to 5
975
+ minutes: past it the install is stopped (its `git` / package-manager
976
+ child killed, no further step started) and rolled back (staging folder
977
+ removed, a replaced version restored) before the refusal is answered;
978
+ its progress lines come back with the result.
979
+ - **MCP servers' secrets** (`env`, `headers`) typed in a window are stored
980
+ in the server's `mcps.json` and never come back (`catalog.mcps` redacts
981
+ them; the reply names the server only).
982
+ - **Gate and audit**: a client token can already run commands on the
983
+ server, so installs add no privilege; they are still a separate switch
984
+ (`--no-remote-installs`, `ZIDANE_SERVER_ALLOW_REMOTE_INSTALLS=0`,
985
+ `"allowRemoteInstalls": false`) and every attempt, refused or failed ones
986
+ included, is in the audit log with its token. With it off, every install
987
+ method is refused and windows hide the actions (`ipc.hello` no longer says
988
+ `remoteInstalls`). Plugins are discovered from their scan paths, as on
989
+ This computer: they are turned on and off over the wire, not installed.
990
+
991
+ ## Operations
992
+
993
+ - **Data layout** (the image's volume):
994
+
995
+ ```
996
+ /data/.zidane/
997
+ sessions.db (+ -wal, -shm) sessions
998
+ settings, credentials as the host writes them
999
+ host-id the host's stable id
1000
+ server.json optional configuration
1001
+ server-tokens.json token hashes (0600)
1002
+ server-tokens.usage.json last use per token (0600)
1003
+ server-audit.jsonl (+ .1) audit log (0600)
1004
+ server-audit.failures.jsonl (+ .1) refused upgrades, aggregated (0600)
1005
+ sync.json settings sync record: source device, synced fields, overrides (0600)
1006
+ server.sock unix socket, while running
1007
+ workspaces/ default workspace root (the image uses /data/workspaces)
1008
+ ```
1009
+
1010
+ `HOME` is `/data` in the image, so git config and other dotfiles the
1011
+ agents' tools write persist on the volume too.
1012
+ - **Backups**: stop the server (`docker compose stop zidane-server`; it
1013
+ drains running turns for up to 8 s), copy the volume, start it again:
1014
+ `docker run --rm -v zidane_zidane-data:/data -v "$PWD":/backup debian:trixie-slim tar czf /backup/zidane-data.tgz -C /data .`
1015
+ Copying `sessions.db` while the server runs can catch the WAL mid-write.
1016
+ - **Upgrades**: bump the image tag (or keep a moving one: `7.2`), then
1017
+ `docker compose pull && docker compose up -d` (plain Docker: `docker
1018
+ pull`, then remove and `docker run` the container again with the same
1019
+ volume); a release binary: replace it and restart. The volume carries
1020
+ over. SIGTERM shuts down gracefully (connections
1021
+ closed, the host drained for up to `ZIDANE_SERVER_DRAIN_TIMEOUT_MS`,
1022
+ 8 s by default, then exit); windows reconnect and reload their open
1023
+ session. Keep the drain under the container's stop timeout (Docker:
1024
+ 10 s by default; the Compose files give 30 s), or Docker sends SIGKILL
1025
+ mid-drain.
1026
+ - **Version skew**: client and server must speak the same IPC protocol
1027
+ version; a mismatch is refused at the handshake with both versions and
1028
+ which side to upgrade. Within a protocol version, older and newer clients
1029
+ both attach; a newer one degrades on methods the server lacks, and a
1030
+ server too old to serve the window's sessions is refused with a message
1031
+ naming it. No client ever drains or restarts a server.
1032
+ - **Health**: `zidane-server health` exits 0 and prints
1033
+ `ok server "<name>" v<version> protocol <n> pid <pid> up <s>s`;
1034
+ `zidane-server health --http` exits 0 and prints `ok <url>` (the image's
1035
+ `HEALTHCHECK`). `curl -fsS https://zidane.example.com/healthz` from
1036
+ outside. `docker inspect -f '{{.State.Health.Status}}' <container>`.
1037
+ - **Smoke tests**: `bun run server:smoke` (the binary) and
1038
+ `bun run server:smoke:image` (the image, twice: with its defaults —
1039
+ healthcheck, socket health, token via `docker exec`, a direct upgrade
1040
+ refused, a connection through a proxy — and Coolify-like —
1041
+ `ZIDANE_SERVER_PORT=8080`, a bootstrap token from `token generate`,
1042
+ `/healthz`, a tokenless upgrade refused with 401, the bootstrap token
1043
+ through a proxy; SIGTERM exits 0 in both; then a container without a
1044
+ volume, which must warn that its data dir is not persistent;
1045
+ `ZIDANE_SMOKE_IMAGE=<ref>` or `--image <ref>` smokes an image already
1046
+ built instead of building one). CI runs both (`server` job), plus
1047
+ `bun run server:release:package` (the four release archives and their
1048
+ `SHA256SUMS`) and the image's `linux/amd64` + `linux/arm64` buildx build.
1049
+
1050
+ ## Limitations
1051
+
1052
+ What a server session does not do (yet), in the TUI and the desktop app:
1053
+
1054
+ - **Installs are user scope only** ([Installs from a
1055
+ client](#installs-from-a-client)): a project's skills, extensions and MCP
1056
+ servers are edited as files in its folder; plugins are not installed
1057
+ from a client. Editing an existing MCP server's fields from a window
1058
+ (beyond replacing it) is not offered.
1059
+ - **This window's extension UI only.** Extension popups (completion
1060
+ providers), panels and palette items are this computer's extensions; a
1061
+ server session gets none from the server's. Its `/` commands and inline
1062
+ ghost text do run on the server.
1063
+ - **No dev browser.** The browser panel drives a page on this computer, so
1064
+ it is offered for This computer's sessions only; a server's agents get no
1065
+ browser tools from a window.
1066
+ - **`!` shell commands** run on the server, in the session's folder,
1067
+ streamed live like a local `!` (`sessions.workspaceExecStream`): esc kills
1068
+ the process group there; at most 4 running per connection and 1 MiB of
1069
+ output each (then the command is stopped, as locally); a dropped
1070
+ connection kills them. Against an older server they are buffered
1071
+ (10 minutes; esc stops waiting, not the command).
1072
+ - **Session folders are on the server**, inside its workspace roots. A
1073
+ client never sends its own working directory to a server: a new session
1074
+ starts in the server's first root, or in the folder picked in the
1075
+ new-session flow (`fs.listDirs`).
1076
+ - **No durable execution.** A server restart interrupts running turns
1077
+ (a commented Restate sidecar placeholder sits in
1078
+ `server/docker-compose.selfhost.yml`).
1079
+ - **Machines** ([MACHINES.md](./MACHINES.md)): computers lend this server
1080
+ their folders and shell (`zidane machine share`, or "share this computer"
1081
+ in a window's Hosts page), and with `allowAsMachine` this server lends
1082
+ itself to its clients' computers over their own connection. What is not
1083
+ done yet: [MACHINES.md § Not in v1](./MACHINES.md#not-in-v1).
1084
+ - **Claude Code import is local**: it imports this machine's Claude data
1085
+ into this machine's store, so imported sessions don't show on a server.