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.
- package/LICENSE +21 -0
- package/README.md +34 -6
- package/dist/{acp-D-tVyUjw.js → acp-DQDhi2dj.js} +230 -77
- package/dist/acp-DQDhi2dj.js.map +1 -0
- package/dist/acp-cli.d.ts +1 -1
- package/dist/acp-cli.js +9 -66
- package/dist/acp-cli.js.map +1 -1
- package/dist/acp.d.ts +10 -590
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +3 -3
- package/dist/{agent-D0zZnGbt.js → agent-DSLNmBp0.js} +14401 -13214
- package/dist/agent-DSLNmBp0.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +3 -2
- package/dist/aliasing-lVFbm81n.js +217 -0
- package/dist/aliasing-lVFbm81n.js.map +1 -0
- package/dist/{anthropic-C2aole0G.js → anthropic-CD00LhUP.js} +1442 -1264
- package/dist/anthropic-CD00LhUP.js.map +1 -0
- package/dist/artifacts-CV2hUKnL.js.map +1 -1
- package/dist/{atomic-write-B7zr1ddF.js → atomic-write-Ijgf2Mhc.js} +13 -3
- package/dist/atomic-write-Ijgf2Mhc.js.map +1 -0
- package/dist/call-tool-dHlbGOIY.js +70 -0
- package/dist/call-tool-dHlbGOIY.js.map +1 -0
- package/dist/chat/pure.d.ts +372 -6
- package/dist/chat/pure.d.ts.map +1 -1
- package/dist/chat/pure.js +35 -244
- package/dist/chat/pure.js.map +1 -1
- package/dist/{approvals-DKaJZYlm.js → chat-JPJJ8uu5.js} +15879 -14631
- package/dist/chat-JPJJ8uu5.js.map +1 -0
- package/dist/chat.d.ts +4 -6
- package/dist/chat.js +23 -12
- package/dist/cli-shared-C-YCaglL.js +159 -0
- package/dist/cli-shared-C-YCaglL.js.map +1 -0
- package/dist/client-core-BQSyDALT.d.ts +2115 -0
- package/dist/client-core-BQSyDALT.d.ts.map +1 -0
- package/dist/codemode-worker.d.ts +1 -0
- package/dist/codemode-worker.js +155 -0
- package/dist/codemode-worker.js.map +1 -0
- package/dist/codemode.d.ts +352 -0
- package/dist/codemode.d.ts.map +1 -0
- package/dist/codemode.js +5 -0
- package/dist/connection-Dr-Js4yZ.d.ts +28 -0
- package/dist/connection-Dr-Js4yZ.d.ts.map +1 -0
- package/dist/connection-loop-gdh22LTI.js +112 -0
- package/dist/connection-loop-gdh22LTI.js.map +1 -0
- package/dist/context-segments-BHKlagL6.js +1016 -0
- package/dist/context-segments-BHKlagL6.js.map +1 -0
- package/dist/{context-transport-T_6AYIvy.js → context-transport-DSMTUsK1.js} +3 -3
- package/dist/{context-transport-T_6AYIvy.js.map → context-transport-DSMTUsK1.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +19 -8
- package/dist/contexts/daytona.d.ts.map +1 -1
- package/dist/contexts/daytona.js +1 -440
- package/dist/contexts/docker.d.ts +36 -7
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +1 -456
- package/dist/contexts/e2b.d.ts +68 -13
- package/dist/contexts/e2b.d.ts.map +1 -1
- package/dist/contexts/e2b.js +1 -382
- package/dist/contexts/sandbox.d.ts +1 -1
- package/dist/contexts/sandbox.js +5 -0
- package/dist/contexts/sandbox.js.map +1 -1
- package/dist/{contexts-CRE9pw07.js → contexts-d7UolYot.js} +1059 -524
- package/dist/contexts-d7UolYot.js.map +1 -0
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/cred-keys-GpBg71cP.js +105 -0
- package/dist/cred-keys-GpBg71cP.js.map +1 -0
- package/dist/{auth-BG2rVKSt.js → credentials-JttEuRON.js} +55 -206
- package/dist/credentials-JttEuRON.js.map +1 -0
- package/dist/{cursor-iojRXHF9.js → cursor-CdjaQTBW.js} +3 -3
- package/dist/{cursor-iojRXHF9.js.map → cursor-CdjaQTBW.js.map} +1 -1
- package/dist/daemon-cli.d.ts +1 -1
- package/dist/daemon-cli.js +3 -2
- package/dist/daemon-cli.js.map +1 -1
- package/dist/daytona-DG3omipZ.js +513 -0
- package/dist/daytona-DG3omipZ.js.map +1 -0
- package/dist/defaults-Cx3zvDOV.js +23 -0
- package/dist/defaults-Cx3zvDOV.js.map +1 -0
- package/dist/dispatcher-BMlu2BmZ.js +4941 -0
- package/dist/dispatcher-BMlu2BmZ.js.map +1 -0
- package/dist/docker-BZhMMJUp.js +727 -0
- package/dist/docker-BZhMMJUp.js.map +1 -0
- package/dist/e2b-CEVOdRib.js +480 -0
- package/dist/e2b-CEVOdRib.js.map +1 -0
- package/dist/edit-utils-EGosADZq.js +234 -0
- package/dist/edit-utils-EGosADZq.js.map +1 -0
- package/dist/environment-8wsKE2Ln.js +214 -0
- package/dist/environment-8wsKE2Ln.js.map +1 -0
- package/dist/eval-BNcvle_J.js +1595 -0
- package/dist/eval-BNcvle_J.js.map +1 -0
- package/dist/eval.d.ts +2 -2
- package/dist/eval.js +1 -1489
- package/dist/executor-Cx5-8yqi.js +621 -0
- package/dist/executor-Cx5-8yqi.js.map +1 -0
- package/dist/extension-authoring-DyGGmq9V.js +385 -0
- package/dist/extension-authoring-DyGGmq9V.js.map +1 -0
- package/dist/extensions.d.ts +2 -2
- package/dist/extensions.js +3 -3
- package/dist/{fetch-url-DX75gpL0.js → fetch-url-Ct2TjDWt.js} +4 -4
- package/dist/{fetch-url-DX75gpL0.js.map → fetch-url-Ct2TjDWt.js.map} +1 -1
- package/dist/{format-BF745pLL.js → format-C2NQEVUH.js} +39 -2
- package/dist/format-C2NQEVUH.js.map +1 -0
- package/dist/format-bytes-C7tfc3Zm.js +55 -0
- package/dist/format-bytes-C7tfc3Zm.js.map +1 -0
- package/dist/headless-C_rNUuYC.js +960 -0
- package/dist/headless-C_rNUuYC.js.map +1 -0
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -413
- package/dist/{history-search-C4bk1Wz_.js → history-search-CcYzF8-0.js} +7 -3
- package/dist/history-search-CcYzF8-0.js.map +1 -0
- package/dist/history-search-worker.d.ts +1 -1
- package/dist/history-search-worker.js +1 -1
- package/dist/host-D7NrI03V.js +378 -0
- package/dist/host-D7NrI03V.js.map +1 -0
- package/dist/host-signals-BcP5QKUD.js +118 -0
- package/dist/host-signals-BcP5QKUD.js.map +1 -0
- package/dist/identity-CwfO8K-t.js +26 -0
- package/dist/identity-CwfO8K-t.js.map +1 -0
- package/dist/{index-BgN1T3lk.d.ts → index-BGe1fQT7.d.ts} +10987 -9320
- package/dist/index-BGe1fQT7.d.ts.map +1 -0
- package/dist/index-BVQsIKGn.d.ts +448 -0
- package/dist/index-BVQsIKGn.d.ts.map +1 -0
- package/dist/{index-C6NEd5y_.d.ts → index-CHABJd-L.d.ts} +3386 -502
- package/dist/index-CHABJd-L.d.ts.map +1 -0
- package/dist/{index-C37_ppWv.d.ts → index-D6a7qXpg.d.ts} +2 -3
- package/dist/index-D6a7qXpg.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +27 -23
- package/dist/{completion-core-BRo86R_L.js → inline-ghost-BVRQeGoL.js} +50 -2
- package/dist/inline-ghost-BVRQeGoL.js.map +1 -0
- package/dist/ipc/remote.d.ts +313 -0
- package/dist/ipc/remote.d.ts.map +1 -0
- package/dist/ipc/remote.js +4 -0
- package/dist/ipc.d.ts +6 -964
- package/dist/ipc.js +403 -4
- package/dist/ipc.js.map +1 -0
- package/dist/{isolation-Kfoof0MM.d.ts → isolation-D9L650W5.d.ts} +100 -15
- package/dist/isolation-D9L650W5.d.ts.map +1 -0
- package/dist/json-rpc--HXQti1E.js +350 -0
- package/dist/json-rpc--HXQti1E.js.map +1 -0
- package/dist/links-j8rED1sR.js +603 -0
- package/dist/links-j8rED1sR.js.map +1 -0
- package/dist/local-terminal-D8QThwo_.js +252 -0
- package/dist/local-terminal-D8QThwo_.js.map +1 -0
- package/dist/{logger--C1VQjwJ.js → logger-Gtqlayx8.js} +199 -173
- package/dist/logger-Gtqlayx8.js.map +1 -0
- package/dist/logs-view-CWG8aUAG.js.map +1 -1
- package/dist/{mcp-CgNd-f3F.js → mcp-_wecH9Qe.js} +608 -562
- package/dist/mcp-_wecH9Qe.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/media-sniff-UKijEaiQ.js.map +1 -1
- package/dist/{messages-2jcOA_5R.js → messages-BqiwcD2Q.js} +1017 -1014
- package/dist/messages-BqiwcD2Q.js.map +1 -0
- package/dist/{oauth-DxHO17JE.js → oauth-Px_pBB9W.js} +5 -5
- package/dist/{oauth-DxHO17JE.js.map → oauth-Px_pBB9W.js.map} +1 -1
- package/dist/{openai-compat-Dy56weK8.js → openai-compat-CFrpH7uq.js} +432 -589
- package/dist/openai-compat-CFrpH7uq.js.map +1 -0
- package/dist/orchestration-BCGmB8Ps.js +511 -0
- package/dist/orchestration-BCGmB8Ps.js.map +1 -0
- package/dist/output/stream-json.d.ts +1 -2
- package/dist/output/stream-json.d.ts.map +1 -1
- package/dist/output/stream-json.js +1 -1
- package/dist/output/terminal.d.ts +1 -2
- package/dist/output/terminal.d.ts.map +1 -1
- package/dist/output/terminal.js +13 -7
- package/dist/output/terminal.js.map +1 -1
- package/dist/{plugins-gbI7SBA6.js → plugins-DS7mahzZ.js} +20 -101
- package/dist/plugins-DS7mahzZ.js.map +1 -0
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +1 -2
- package/dist/prelude-Cxo4t74B.js +517 -0
- package/dist/prelude-Cxo4t74B.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -2
- package/dist/project-root-B8zADr4o.js +167 -0
- package/dist/project-root-B8zADr4o.js.map +1 -0
- package/dist/project-user-paths-BSgtrRxl.js +58 -0
- package/dist/project-user-paths-BSgtrRxl.js.map +1 -0
- package/dist/{prompts-Pqc3JdW_.d.ts → prompts-DYcvqShf.d.ts} +14 -11
- package/dist/prompts-DYcvqShf.d.ts.map +1 -0
- package/dist/protocol-DnhqqSmE.js +13 -0
- package/dist/protocol-DnhqqSmE.js.map +1 -0
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +1 -2
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +3 -2
- package/dist/providers/cerebras.js.map +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -2
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +15 -12
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +3 -2
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +3 -3
- package/dist/{providers-BVhnFyiY.js → providers-XUR7F4Hi.js} +4 -4
- package/dist/{providers-BVhnFyiY.js.map → providers-XUR7F4Hi.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +4 -4
- package/dist/registry-D6ShBWJU.js +5234 -0
- package/dist/registry-D6ShBWJU.js.map +1 -0
- package/dist/remote-hosts-BtA_9ZGp.js +97 -0
- package/dist/remote-hosts-BtA_9ZGp.js.map +1 -0
- package/dist/restate.d.ts +2 -3
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js +631 -572
- package/dist/restate.js.map +1 -1
- package/dist/run-summary-CklLiZ4y.js +288 -0
- package/dist/run-summary-CklLiZ4y.js.map +1 -0
- package/dist/{sandbox-BH0WQkXn.d.ts → sandbox-C5dR5DwQ.d.ts} +6 -3
- package/dist/sandbox-C5dR5DwQ.d.ts.map +1 -0
- package/dist/session/sqlite.d.ts +1 -2
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -616
- package/dist/session-factory-BUGY5P40.js +1576 -0
- package/dist/session-factory-BUGY5P40.js.map +1 -0
- package/dist/{session-BqRP1hS7.js → session-xh9JXu6i.js} +94 -52
- package/dist/session-xh9JXu6i.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/settings-keys-Ckf29NeM.js +677 -0
- package/dist/settings-keys-Ckf29NeM.js.map +1 -0
- package/dist/shell-path-Dqaoow2J.js +412 -0
- package/dist/shell-path-Dqaoow2J.js.map +1 -0
- package/dist/{skills-BC3dodWp.js → skills-DKqZJm4X.js} +430 -470
- package/dist/skills-DKqZJm4X.js.map +1 -0
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -2
- package/dist/source-mPExaUm8.js +381 -0
- package/dist/source-mPExaUm8.js.map +1 -0
- package/dist/sqlite-DV1DQzN2.js +698 -0
- package/dist/sqlite-DV1DQzN2.js.map +1 -0
- package/dist/{src-CkNQKFKr.js → src-BRMYRJib.js} +903 -1121
- package/dist/src-BRMYRJib.js.map +1 -0
- package/dist/{stdio-DPpBDPx5.js → stdio-C6YyoW3I.js} +4 -4
- package/dist/{stdio-DPpBDPx5.js.map → stdio-C6YyoW3I.js.map} +1 -1
- package/dist/{stdio-loader-Bbx19dMZ.js → stdio-loader-C830Vg-Z.js} +4 -4
- package/dist/{stdio-loader-Bbx19dMZ.js.map → stdio-loader-C830Vg-Z.js.map} +1 -1
- package/dist/system-prompt-MNVHatSD.d.ts.map +1 -1
- package/dist/timeout-DEaqslLO.d.ts.map +1 -1
- package/dist/todo-edits-Dn251YUP.js +1790 -0
- package/dist/todo-edits-Dn251YUP.js.map +1 -0
- package/dist/{turn-operations-DrbJlpq9.js → tool-formatters-IQFDrdrs.js} +1456 -2120
- package/dist/tool-formatters-IQFDrdrs.js.map +1 -0
- package/dist/tool-state-Dku6YqAB.js +989 -0
- package/dist/tool-state-Dku6YqAB.js.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -2
- package/dist/tools/fetch-url.d.ts.map +1 -1
- package/dist/tools/fetch-url.js +1 -2
- package/dist/tools/prompts.d.ts +1 -1
- package/dist/tools/prompts.js +7 -7
- package/dist/tools/prompts.js.map +1 -1
- package/dist/tools/web-search.d.ts +1 -2
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +2 -2
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools-_DnT3AGM.js +578 -0
- package/dist/tools-_DnT3AGM.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +4 -2
- package/dist/tui.d.ts +534 -517
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +34933 -27243
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-B8sokwi5.js +333 -0
- package/dist/turn-operations-B8sokwi5.js.map +1 -0
- package/dist/turn-operations-DQX5ZZni.d.ts +3089 -0
- package/dist/turn-operations-DQX5ZZni.d.ts.map +1 -0
- package/dist/types-C7ZFOztE.d.ts +132 -0
- package/dist/types-C7ZFOztE.d.ts.map +1 -0
- package/dist/types-CH5XpIHw.js +114 -0
- package/dist/types-CH5XpIHw.js.map +1 -0
- package/dist/{types-CWgENVLY.js → types-Cu0ZGDu9.js} +10 -6
- package/dist/types-Cu0ZGDu9.js.map +1 -0
- package/dist/types-IqegbJEE.d.ts +624 -0
- package/dist/types-IqegbJEE.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +1 -1
- package/dist/url-D8HCl65b.js +1259 -0
- package/dist/url-D8HCl65b.js.map +1 -0
- package/dist/wasm-BVT-j5cl.js +33 -0
- package/dist/wasm-BVT-j5cl.js.map +1 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js +355 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js.map +1 -0
- package/dist/ws-CIHMvlSh.js +952 -0
- package/dist/ws-CIHMvlSh.js.map +1 -0
- package/dist/ws-client-N7NxzvQp.js +230 -0
- package/dist/ws-client-N7NxzvQp.js.map +1 -0
- package/dist/{xai-CazjStOK.js → xai-CvJTeLrI.js} +165 -5
- package/dist/xai-CvJTeLrI.js.map +1 -0
- package/dist/xdg-zweed0-t.js +117 -0
- package/dist/xdg-zweed0-t.js.map +1 -0
- package/docs/ACP.md +52 -0
- package/docs/ARCHITECTURE.md +103 -6
- package/docs/ARTIFACTS.md +8 -4
- package/docs/CHAT.md +165 -14
- package/docs/EXECUTION_CONTEXT.md +8 -0
- package/docs/EXTENSIONS.md +123 -2
- package/docs/HEADLESS.md +2 -0
- package/docs/IPC.md +775 -64
- package/docs/MACHINES.md +456 -0
- package/docs/PANELS.md +3 -0
- package/docs/README.md +4 -0
- package/docs/RESTATE.md +3 -2
- package/docs/RUN_IN_BACKGROUND.md +10 -2
- package/docs/SERVER.md +1085 -0
- package/docs/SKILL.md +6 -1
- package/docs/TERMINAL.md +75 -0
- package/docs/TUI.md +65 -14
- package/package.json +58 -26
- package/scripts/eval.ts +4 -0
- package/dist/acp-D-tVyUjw.js.map +0 -1
- package/dist/agent-D0zZnGbt.js.map +0 -1
- package/dist/anthropic-C2aole0G.js.map +0 -1
- package/dist/approvals-DKaJZYlm.js.map +0 -1
- package/dist/atomic-write-B7zr1ddF.js.map +0 -1
- package/dist/auth-BG2rVKSt.js.map +0 -1
- package/dist/completion-core-BRo86R_L.js.map +0 -1
- package/dist/contexts/daytona.js.map +0 -1
- package/dist/contexts/docker.js.map +0 -1
- package/dist/contexts/e2b.js.map +0 -1
- package/dist/contexts-CRE9pw07.js.map +0 -1
- package/dist/eval.js.map +0 -1
- package/dist/format-BF745pLL.js.map +0 -1
- package/dist/glob-DluQFSw1.js +0 -270
- package/dist/glob-DluQFSw1.js.map +0 -1
- package/dist/headless.js.map +0 -1
- package/dist/history-search-C4bk1Wz_.js.map +0 -1
- package/dist/index-BgN1T3lk.d.ts.map +0 -1
- package/dist/index-C37_ppWv.d.ts.map +0 -1
- package/dist/index-C6NEd5y_.d.ts.map +0 -1
- package/dist/ipc-BKVIIOqH.js +0 -331
- package/dist/ipc-BKVIIOqH.js.map +0 -1
- package/dist/ipc.d.ts.map +0 -1
- package/dist/isolation-Kfoof0MM.d.ts.map +0 -1
- package/dist/json-rpc-rSWMUh9b.js +0 -192
- package/dist/json-rpc-rSWMUh9b.js.map +0 -1
- package/dist/logger--C1VQjwJ.js.map +0 -1
- package/dist/mcp-CgNd-f3F.js.map +0 -1
- package/dist/messages-2jcOA_5R.js.map +0 -1
- package/dist/openai-compat-Dy56weK8.js.map +0 -1
- package/dist/output-cap-C9zukcCj.js +0 -100
- package/dist/output-cap-C9zukcCj.js.map +0 -1
- package/dist/panels-B6gRk4bf.js +0 -789
- package/dist/panels-B6gRk4bf.js.map +0 -1
- package/dist/plugins-gbI7SBA6.js.map +0 -1
- package/dist/prompts-Pqc3JdW_.d.ts.map +0 -1
- package/dist/run-summary-BU_NgwUT.js +0 -269
- package/dist/run-summary-BU_NgwUT.js.map +0 -1
- package/dist/sandbox-BH0WQkXn.d.ts.map +0 -1
- package/dist/session/sqlite.js.map +0 -1
- package/dist/session-BqRP1hS7.js.map +0 -1
- package/dist/shell-path-DfdXrGg9.js +0 -1549
- package/dist/shell-path-DfdXrGg9.js.map +0 -1
- package/dist/skills-BC3dodWp.js.map +0 -1
- package/dist/src-CkNQKFKr.js.map +0 -1
- package/dist/todo-edits-UOwgxGKu.js +0 -528
- package/dist/todo-edits-UOwgxGKu.js.map +0 -1
- package/dist/tool-formatters-DoRLnfuD.d.ts +0 -759
- package/dist/tool-formatters-DoRLnfuD.d.ts.map +0 -1
- package/dist/transcript-anchors-BeudMbXx.d.ts +0 -1762
- package/dist/transcript-anchors-BeudMbXx.d.ts.map +0 -1
- package/dist/turn-operations-C8bMBDl_.d.ts +0 -1258
- package/dist/turn-operations-C8bMBDl_.d.ts.map +0 -1
- package/dist/turn-operations-DrbJlpq9.js.map +0 -1
- package/dist/types-CWgENVLY.js.map +0 -1
- 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.
|