@turmind/halo 1.6.3 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +58 -67
  2. package/admin-out/404.html +1 -1
  3. package/admin-out/_next/static/chunks/989-5d7743ff56d00dd7.js +1 -0
  4. package/admin-out/_next/static/chunks/app/page-8881b4f87a3d5ae8.js +1 -0
  5. package/admin-out/index.html +1 -1
  6. package/admin-out/index.txt +4 -4
  7. package/bin/halo.js +4 -0
  8. package/bundled-docs/design/architecture.md +1 -1
  9. package/bundled-docs/design/canvas-extensions.md +2 -2
  10. package/bundled-docs/design/observability.md +7 -5
  11. package/bundled-docs/design/storage.md +8 -5
  12. package/bundled-docs/design/wechat.md +1 -1
  13. package/bundled-docs/dev/add-model-provider.md +2 -1
  14. package/bundled-docs/dev/api.md +2 -1
  15. package/bundled-docs/dev/deploy.md +114 -26
  16. package/bundled-docs/dev/desktop-packaging.md +13 -1
  17. package/bundled-docs/dev/env.md +3 -3
  18. package/bundled-docs/dev/logging.md +13 -2
  19. package/bundled-docs/dev/tools.md +2 -2
  20. package/bundled-docs/guide/agents.md +2 -2
  21. package/bundled-docs/guide/cli.md +20 -8
  22. package/bundled-docs/guide/getting-started.md +13 -5
  23. package/bundled-docs/guide/workspace.md +2 -0
  24. package/bundled-docs/requirements/agent.md +1 -1
  25. package/bundled-docs/requirements/explorer.md +14 -2
  26. package/bundled-docs/requirements/settings.md +2 -2
  27. package/dist/index.js +2565 -1449
  28. package/package.json +3 -2
  29. package/templates/models/gemini.yaml +93 -0
  30. package/admin-out/_next/static/chunks/989-ddba0eb72566da23.js +0 -1
  31. package/admin-out/_next/static/chunks/app/page-7ffcbddb49dc56c4.js +0 -1
  32. /package/admin-out/_next/static/{k5zuJrivSdbaPifBatwdr → kfp1cMc36J6hjZ06L9mm-}/_buildManifest.js +0 -0
  33. /package/admin-out/_next/static/{k5zuJrivSdbaPifBatwdr → kfp1cMc36J6hjZ06L9mm-}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- <!DOCTYPE html><!--k5zuJrivSdbaPifBatwdr--><html lang="en" class="dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/7b736202f0b1dbfd.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/3b4dcd0ac6b8eae8.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/659eccb5db697b76.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-74f577c1b094f942.js"/><script src="/_next/static/chunks/1b86ba38-c29611e086b2ce25.js" async=""></script><script src="/_next/static/chunks/698-19872069fed27164.js" async=""></script><script src="/_next/static/chunks/main-app-340a5b58ed229c4d.js" async=""></script><script src="/_next/static/chunks/989-ddba0eb72566da23.js" async=""></script><script src="/_next/static/chunks/app/layout-ea161287d934ba96.js" async=""></script><script src="/_next/static/chunks/1d2d5650-d8d130c85727fdbb.js" async=""></script><script src="/_next/static/chunks/147-d4200c70445aabb6.js" async=""></script><script src="/_next/static/chunks/app/page-7ffcbddb49dc56c4.js" async=""></script><meta name="next-size-adjust" content=""/><title>Halo - Multi-Agent Workspace</title><meta name="description" content="Multi-agent collaboration workspace for complex project delivery"/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/><link rel="icon" href="/icon.svg?3a6863548daca964" type="image/svg+xml" sizes="any"/><script>try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__variable_d8ebd1 font-sans antialiased"><div hidden=""><!--$--><!--/$--></div><div class="flex h-screen w-screen items-center justify-center bg-[var(--background)]"><div class="text-sm text-[var(--muted-foreground)]">Loading...</div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-74f577c1b094f942.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[58321,[\"989\",\"static/chunks/989-ddba0eb72566da23.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-ddba0eb72566da23.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[41506,[\"560\",\"static/chunks/1d2d5650-d8d130c85727fdbb.js\",\"147\",\"static/chunks/147-d4200c70445aabb6.js\",\"989\",\"static/chunks/989-ddba0eb72566da23.js\",\"974\",\"static/chunks/app/page-7ffcbddb49dc56c4.js\"],\"default\"]\na:I[23632,[],\"OutletBoundary\"]\nc:I[86889,[],\"AsyncMetadataOutlet\"]\ne:I[23632,[],\"ViewportBoundary\"]\n10:I[23632,[],\"MetadataBoundary\"]\n11:\"$Sreact.suspense\"\n13:I[1935,[],\"\"]\n:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/7b736202f0b1dbfd.css\",\"style\"]\n:HL[\"/_next/static/css/3b4dcd0ac6b8eae8.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"k5zuJrivSdbaPifBatwdr\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/7b736202f0b1dbfd.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/3b4dcd0ac6b8eae8.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"dark\",\"suppressHydrationWarning\":true,\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}\"}}]}],[\"$\",\"body\",null,{\"className\":\"__variable_d8ebd1 font-sans antialiased\",\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L6\",null,{\"Component\":\"$7\",\"searchParams\":{},\"params\":{},\"promises\":[\"$@8\",\"$@9\"]}],[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/659eccb5db697b76.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"$La\",null,{\"children\":[\"$Lb\",[\"$\",\"$Lc\",null,{\"promise\":\"$@d\"}]]}]]}],{},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[[\"$\",\"$Le\",null,{\"children\":\"$Lf\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$L10\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$11\",null,{\"fallback\":null,\"children\":\"$L12\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$13\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"8:{}\n9:\"$0:f:0:1:2:children:1:props:children:0:props:params\"\n"])</script><script>self.__next_f.push([1,"f:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\nb:null\n"])</script><script>self.__next_f.push([1,"14:I[88809,[],\"IconMark\"]\nd:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Halo - Multi-Agent Workspace\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Multi-agent collaboration workspace for complex project delivery\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"link\",\"3\",{\"rel\":\"icon\",\"href\":\"/icon.svg?3a6863548daca964\",\"type\":\"image/svg+xml\",\"sizes\":\"any\"}],[\"$\",\"$L14\",\"4\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"12:\"$d:metadata\"\n"])</script></body></html>
1
+ <!DOCTYPE html><!--kfp1cMc36J6hjZ06L9mm_--><html lang="en" class="dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/7b736202f0b1dbfd.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/3b4dcd0ac6b8eae8.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/659eccb5db697b76.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-74f577c1b094f942.js"/><script src="/_next/static/chunks/1b86ba38-c29611e086b2ce25.js" async=""></script><script src="/_next/static/chunks/698-19872069fed27164.js" async=""></script><script src="/_next/static/chunks/main-app-340a5b58ed229c4d.js" async=""></script><script src="/_next/static/chunks/989-5d7743ff56d00dd7.js" async=""></script><script src="/_next/static/chunks/app/layout-ea161287d934ba96.js" async=""></script><script src="/_next/static/chunks/1d2d5650-d8d130c85727fdbb.js" async=""></script><script src="/_next/static/chunks/147-d4200c70445aabb6.js" async=""></script><script src="/_next/static/chunks/app/page-8881b4f87a3d5ae8.js" async=""></script><meta name="next-size-adjust" content=""/><title>Halo - Multi-Agent Workspace</title><meta name="description" content="Multi-agent collaboration workspace for complex project delivery"/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/><link rel="icon" href="/icon.svg?3a6863548daca964" type="image/svg+xml" sizes="any"/><script>try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__variable_d8ebd1 font-sans antialiased"><div hidden=""><!--$--><!--/$--></div><div class="flex h-screen w-screen items-center justify-center bg-[var(--background)]"><div class="text-sm text-[var(--muted-foreground)]">Loading...</div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-74f577c1b094f942.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[58321,[\"989\",\"static/chunks/989-5d7743ff56d00dd7.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-5d7743ff56d00dd7.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[88625,[\"560\",\"static/chunks/1d2d5650-d8d130c85727fdbb.js\",\"147\",\"static/chunks/147-d4200c70445aabb6.js\",\"989\",\"static/chunks/989-5d7743ff56d00dd7.js\",\"974\",\"static/chunks/app/page-8881b4f87a3d5ae8.js\"],\"default\"]\na:I[23632,[],\"OutletBoundary\"]\nc:I[86889,[],\"AsyncMetadataOutlet\"]\ne:I[23632,[],\"ViewportBoundary\"]\n10:I[23632,[],\"MetadataBoundary\"]\n11:\"$Sreact.suspense\"\n13:I[1935,[],\"\"]\n:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/7b736202f0b1dbfd.css\",\"style\"]\n:HL[\"/_next/static/css/3b4dcd0ac6b8eae8.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"kfp1cMc36J6hjZ06L9mm-\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/7b736202f0b1dbfd.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/3b4dcd0ac6b8eae8.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"dark\",\"suppressHydrationWarning\":true,\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}\"}}]}],[\"$\",\"body\",null,{\"className\":\"__variable_d8ebd1 font-sans antialiased\",\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L6\",null,{\"Component\":\"$7\",\"searchParams\":{},\"params\":{},\"promises\":[\"$@8\",\"$@9\"]}],[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/659eccb5db697b76.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"$La\",null,{\"children\":[\"$Lb\",[\"$\",\"$Lc\",null,{\"promise\":\"$@d\"}]]}]]}],{},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[[\"$\",\"$Le\",null,{\"children\":\"$Lf\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$L10\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$11\",null,{\"fallback\":null,\"children\":\"$L12\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$13\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"8:{}\n9:\"$0:f:0:1:2:children:1:props:children:0:props:params\"\n"])</script><script>self.__next_f.push([1,"f:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\nb:null\n"])</script><script>self.__next_f.push([1,"14:I[88809,[],\"IconMark\"]\nd:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Halo - Multi-Agent Workspace\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Multi-agent collaboration workspace for complex project delivery\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"link\",\"3\",{\"rel\":\"icon\",\"href\":\"/icon.svg?3a6863548daca964\",\"type\":\"image/svg+xml\",\"sizes\":\"any\"}],[\"$\",\"$L14\",\"4\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"12:\"$d:metadata\"\n"])</script></body></html>
@@ -1,10 +1,10 @@
1
1
  1:"$Sreact.fragment"
2
- 2:I[58321,["989","static/chunks/989-ddba0eb72566da23.js","177","static/chunks/app/layout-ea161287d934ba96.js"],"ThemeProvider"]
3
- 3:I[24554,["989","static/chunks/989-ddba0eb72566da23.js","177","static/chunks/app/layout-ea161287d934ba96.js"],"AppI18nProvider"]
2
+ 2:I[58321,["989","static/chunks/989-5d7743ff56d00dd7.js","177","static/chunks/app/layout-ea161287d934ba96.js"],"ThemeProvider"]
3
+ 3:I[24554,["989","static/chunks/989-5d7743ff56d00dd7.js","177","static/chunks/app/layout-ea161287d934ba96.js"],"AppI18nProvider"]
4
4
  4:I[63693,[],""]
5
5
  5:I[74713,[],""]
6
6
  6:I[5444,[],"ClientPageRoot"]
7
- 7:I[41506,["560","static/chunks/1d2d5650-d8d130c85727fdbb.js","147","static/chunks/147-d4200c70445aabb6.js","989","static/chunks/989-ddba0eb72566da23.js","974","static/chunks/app/page-7ffcbddb49dc56c4.js"],"default"]
7
+ 7:I[88625,["560","static/chunks/1d2d5650-d8d130c85727fdbb.js","147","static/chunks/147-d4200c70445aabb6.js","989","static/chunks/989-5d7743ff56d00dd7.js","974","static/chunks/app/page-8881b4f87a3d5ae8.js"],"default"]
8
8
  a:I[23632,[],"OutletBoundary"]
9
9
  c:I[86889,[],"AsyncMetadataOutlet"]
10
10
  e:I[23632,[],"ViewportBoundary"]
@@ -15,7 +15,7 @@ e:I[23632,[],"ViewportBoundary"]
15
15
  :HL["/_next/static/css/7b736202f0b1dbfd.css","style"]
16
16
  :HL["/_next/static/css/3b4dcd0ac6b8eae8.css","style"]
17
17
  :HL["/_next/static/css/659eccb5db697b76.css","style"]
18
- 0:{"P":null,"b":"k5zuJrivSdbaPifBatwdr","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/7b736202f0b1dbfd.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/3b4dcd0ac6b8eae8.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"dark","suppressHydrationWarning":true,"children":[["$","head",null,{"children":["$","script",null,{"dangerouslySetInnerHTML":{"__html":"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}"}}]}],["$","body",null,{"className":"__variable_d8ebd1 font-sans antialiased","children":["$","$L2",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L6",null,{"Component":"$7","searchParams":{},"params":{},"promises":["$@8","$@9"]}],[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/659eccb5db697b76.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","$La",null,{"children":["$Lb",["$","$Lc",null,{"promise":"$@d"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Le",null,{"children":"$Lf"}],["$","meta",null,{"name":"next-size-adjust","content":""}]],["$","$L10",null,{"children":["$","div",null,{"hidden":true,"children":["$","$11",null,{"fallback":null,"children":"$L12"}]}]}]]}],false]],"m":"$undefined","G":["$13",[]],"s":false,"S":true}
18
+ 0:{"P":null,"b":"kfp1cMc36J6hjZ06L9mm-","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/7b736202f0b1dbfd.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/3b4dcd0ac6b8eae8.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"dark","suppressHydrationWarning":true,"children":[["$","head",null,{"children":["$","script",null,{"dangerouslySetInnerHTML":{"__html":"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}"}}]}],["$","body",null,{"className":"__variable_d8ebd1 font-sans antialiased","children":["$","$L2",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L6",null,{"Component":"$7","searchParams":{},"params":{},"promises":["$@8","$@9"]}],[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/659eccb5db697b76.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","$La",null,{"children":["$Lb",["$","$Lc",null,{"promise":"$@d"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Le",null,{"children":"$Lf"}],["$","meta",null,{"name":"next-size-adjust","content":""}]],["$","$L10",null,{"children":["$","div",null,{"hidden":true,"children":["$","$11",null,{"fallback":null,"children":"$L12"}]}]}]]}],false]],"m":"$undefined","G":["$13",[]],"s":false,"S":true}
19
19
  8:{}
20
20
  9:"$0:f:0:1:2:children:1:props:children:0:props:params"
21
21
  f:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
package/bin/halo.js CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ if (Number(process.versions.node.split('.')[0]) < 22) {
3
+ process.stderr.write('halo requires Node.js 22 or newer (found ' + process.version + '). Upgrade: https://nodejs.org/ (or `nvm install 22`)\n')
4
+ process.exit(1)
5
+ }
2
6
  const emit = process.emitWarning.bind(process)
3
7
  process.emitWarning = (warning, ...args) => {
4
8
  const o = args[0]
@@ -179,7 +179,7 @@ File: `tools/workspace-tools.ts`. 9 tools: file_read / view_image / file_write /
179
179
  File: `ws/handler.ts` (`cleanupConnection`) + `bufferDetachedNotification` in `ws/event-processor.ts`. After WS disconnect, an inline handler takes over the session tree's events and buffers structural ones for replay on reconnect. `/session new` only opens a draft tab — the previous session stays subscribed — see [design/background-dispatch.md](background-dispatch.md).
180
180
 
181
181
  ### WorkspaceWatcher — file watching
182
- File: `ws/file-watcher.ts`. `@parcel/watcher` recursive native subscription (one per workspace root, shared across connections via `ws/watcher-pool.ts`) → 300ms debounce + per-path Map dedup → callback. Ignores node_modules / .git / .next / dist etc. (`IGNORED_SEGMENTS`) — passed to parcel as the bare names (top-level `ignorePaths`, string compare) plus **one** `**/{a,b,…}/**` brace glob for nested occurrences. Not one glob per segment: parcel runs every ignore regex against every inode during the initial walk, and 52 separate regexes made subscribe on a ~26k-inode repo take ~13s (one brace regex: ~0.8s, same watch set). `.halo/sessions/` and `.halo/logs/` are **not** ignored — the dedup keeps volume low, and the front-end drops `change` events for files not open in the editor, so Explorer can reflect session deletions/creations while chat streaming stays cheap.
182
+ File: `ws/file-watcher.ts`. `@parcel/watcher` recursive native subscription (one per workspace root, shared across connections via `ws/watcher-pool.ts`) → 300ms debounce + per-path Map dedup → callback. Ignores node_modules / .git / .next / dist etc. (`IGNORED_SEGMENTS`, which also sets folder download's default exclusions — `routes/folder-zip.ts`) — passed to parcel as the bare names (top-level `ignorePaths`, string compare) plus **one** `**/{a,b,…}/**` brace glob for nested occurrences. Not one glob per segment: parcel runs every ignore regex against every inode during the initial walk, and 52 separate regexes made subscribe on a ~26k-inode repo take ~13s (one brace regex: ~0.8s, same watch set). `.halo/sessions/` and `.halo/logs/` are **not** ignored — the dedup keeps volume low, and the front-end drops `change` events for files not open in the editor, so Explorer can reflect session deletions/creations while chat streaming stays cheap.
183
183
 
184
184
  ### TerminalManager — PTY management
185
185
  File: `ws/terminal-manager.ts`. Spawns a shell via node-pty. On disconnect, detaches with a 50KB ring buffer; replays on reconnect.
@@ -229,7 +229,7 @@ The ipynb extension (2.0.0+) runs cells on **real kernels on the Halo machine**.
229
229
  - **Trust level = the built-in terminal**: kernels run as the Halo service user, unsandboxed; admin cookie only (the HTTP routes sit behind `authMiddleware`, the WS handshake checks the cookie like `/ws`; web-channel / IM / A2A tokens can't reach it); not mounted in AgentCore mode; the child env is `cleanChildEnv()`. No manifest capability gates it — `init.jupyter` is the host's signal (a new capability value would make ipynb 2.0.0 uninstallable on a 1.6.0 registry, same reasoning as `init.export`). The iframe is same-origin with the login cookie, so the extension fetches / opens the WS to `/api/jupyter/*` itself.
230
230
  - **Settings** (`general.jupyter.*`, globalOnly, read at each spawn — no restart): `enabled` (default `true`) and `path` (default empty = `jupyter` on PATH, else the full path of the executable).
231
231
  - **Status** (`GET /api/jupyter/status`) never starts anything: disabled → `{enabled:false, available:false}`; executable missing → `available:false` + `error` + `hint: 'pip install jupyter_server ipykernel'`; else `<jupyter> server --version` is run once per resolved path and cached (a failing probe isn't cached, so a later `pip install` is picked up).
232
- - **Server**: one per Halo process, started by the first `kernelspecs` / kernel start (concurrent callers share one start), restarted lazily after it exits. Args: `server --no-browser --ServerApp.ip=127.0.0.1 --ServerApp.port=<free port> --ServerApp.port_retries=0 --ServerApp.root_dir=<filesystem root> --ServerApp.terminals_enabled=False --ServerApp.shutdown_no_activity_timeout=600 --MappingKernelManager.cull_idle_timeout=3600 --MappingKernelManager.cull_interval=300 --MappingKernelManager.cull_connected=True` (+ `allow_root` when Halo itself runs as root). The random upstream token goes in `JUPYTER_TOKEN` (env, not argv — `ps` would show it) and every upstream call sends `Authorization: token …`. Ready = `GET /api/status` answers, within 30 s, else 503 with the stderr tail. When the process exits, the kernel Map is cleared and every client socket closes 1011.
232
+ - **Server**: one per Halo process, started by the first `kernelspecs` / kernel start (concurrent callers share one start), restarted lazily after it exits. Args: `server --no-browser --ServerApp.ip=127.0.0.1 --ServerApp.port=<free port> --ServerApp.port_retries=0 --ServerApp.root_dir=<filesystem root> --ServerApp.terminals_enabled=False --ServerApp.shutdown_no_activity_timeout=600 --MappingKernelManager.cull_idle_timeout=3600 --MappingKernelManager.cull_interval=300 --MappingKernelManager.cull_connected=True` (+ `allow_root` when Halo itself runs as root). Both spawns (the `--version` probe and the server) pass `cwd: os.homedir()` — inheriting the server's cwd left a live jupyter holding the desktop install dir open, blocking the installer; `root_dir` is unaffected. The random upstream token goes in `JUPYTER_TOKEN` (env, not argv — `ps` would show it) and every upstream call sends `Authorization: token …`. Ready = `GET /api/status` answers, within 30 s, else 503 with the stderr tail. When the process exits, the kernel Map is cleared and every client socket closes 1011.
233
233
  - **Kernel start** (`POST /api/jupyter/kernels {projectId, path, name}`): `resolveProjectPath` + `validatePath` (symlinks followed; outside → 400), then the **real path of the notebook's directory** is sent as the upstream kernel `path`. jupyter_server 2.x `cwd_for_path` splits that on `/`, joins the parts onto `root_dir` and normalizes — with `root_dir` = `/` that is the absolute path itself; on Windows the native `C:\…` path is a single part and joining an absolute drive path onto the root replaces it, so any drive works (forward slashes would give a drive-relative `D:x`). jupyter silently walks up to `root_dir` when the directory doesn't exist, so Halo checks it first (400).
234
234
  - **Kernel ops / channels** only for ids in Halo's in-process Map (unknown → 404, also when jupyter itself started it). Upstream 404 (culled) drops the id. A kernel jupyter shuts down on its own (idle cull) does **not** close its channels sockets — the only sign is an iopub `shutdown_reply` with `content.restart: false` (a restart sends the same frame with `restart: true`, and the socket keeps working across it); the proxy watches for that frame, drops the id and closes the client sockets 1000. A kernel that dies in a cell (`exit()`, a crash) is auto-restarted by jupyter: `status: restarting` → `starting`, same id, socket stays. Channels: WS `/api/jupyter/channels?kernel=<id>[&session_id=<s>]` → `ws://127.0.0.1:<port>/api/kernels/<id>/channels[?session_id=<s>]` (`session_id` `^[\w-]{1,128}$`, else 400; forwarded since 1.6.3 so jupyter's per-session buffering works, see Lifecycle), frames piped both ways as-is (text and binary), frames sent before the upstream handshake are queued. No subprotocol is offered upstream or accepted from the client, so jupyter speaks the **legacy JSON protocol** (every message a text frame carrying `channel`, `header`, `parent_header`, `metadata`, `content`, `buffers` go as binary frames). The proxy pings the client every 20 s (keepalive through reverse proxies; 2 missed pongs = dead).
235
235
  - **Lifecycle**: a kernel with no client socket for **60 s** is DELETEd upstream (tab closed; the grace rides out a reload / reconnect, which cancels it; a kernel nobody ever connects to gets the same grace). **Reload**: since ipynb 2.0.1 the extension sends no `DELETE` on `pagehide` (it can't tell a reload from a close; 2.0.0's unload `DELETE` killed the kernel on every reload) — it closes the socket and stores `{id, name, session, run: {msgId, cellId, index, source, count, outputs}}` in `sessionStorage` (`halo-ipynb-kernel:<projectId>:<path>`, per browser tab, claimed once by the next page of that notebook). The next page reconnects with the same id and `session_id`; a handshake that fails (id gone after the grace, server restarted) falls back to a fresh kernel on the next run. jupyter_server (verified 2.21.1) buffers a disconnected session's messages and replays them to a socket that reconnects with the same `session_id` — output printed during the reload and the cell's `execute_reply` — and the proxy only reaches that buffer by forwarding `session_id` (1.6.2 doesn't: output in the gap is lost, live output still streams, and the shell `kernel_info_request` the extension sends on reattach, queued behind the running cell, still marks it done). jupyter sends no `status` to a new socket while the kernel is busy (its connect nudge skips busy kernels), so the extension sets the badge from what it stored (no route reads a kernel's state). The extension still `DELETE`s the old kernel when the user picks another kernelspec. jupyter culls kernels idle for 1 h even while connected, and exits itself after 10 min with no kernels. Halo's graceful shutdown POSTs upstream `/api/shutdown` (stops every kernel, then exits; works on Windows too) and SIGKILLs after 5 s; a `process.on('exit')` hook kills the child on other exits. If Halo dies hard, the orphaned server goes on its own: its kernels are culled after 1 h idle, then it exits 10 min later.
@@ -259,7 +259,7 @@ Built-in skill `templates/skills/extension/` (in `BUILTIN_SKILL_IDS`; `TEMPLATE_
259
259
  - An API error or "no release" on an identified platform stops with a hint (set the token, or download the zip from the releases page and `install <path>`). **Never a git fallback**: a git checkout of a build-step extension lacks its build output and would install broken.
260
260
  - Anything else (ssh, `ssh://`, `file://`, a local path, an https host that answered neither probe) → git mode: the highest `<id>-v<x.y.z>` tag by semver (prereleases skipped), shallow clone, `extensions/<id>/` minus the build inputs `pack.mjs` leaves out of a zip (`node_modules/`, `src/`, `package*.json`). Refused ("needs a build step") when the package has `fetch-deps.sh` / `build.sh` — a presence check, and all four current halo-hub extensions carry one, so a plain-git mirror of halo-hub installs none of them by id today.
261
261
 
262
- The hub repo keeps one directory per extension (`glb` / `ipynb` commit their vendored deps, `fetch-deps.sh` only refreshes them; `drawio` / `excalidraw` gitignore their build output and commit only the inputs), `scripts/pack.mjs extensions/<id>` → `dist/<id>-<version>.zip` with contents at the zip root (maintenance scripts and build inputs excluded), and one release per tag `<id>-v<version>` carrying exactly one zip (a second one would be picked by accident). Skills and workspaces in the same repo are tagged `skill-<id>-v<ver>` / `ws-<id>-v<ver>`; nothing installs them yet. Catalog today (only `glb-v1.0.0` is released so far):
262
+ The hub repo keeps one directory per extension (`glb` / `ipynb` commit their vendored deps, `fetch-deps.sh` only refreshes them; `drawio` / `excalidraw` gitignore their build output and commit only the inputs), `scripts/pack.mjs extensions/<id>` → `dist/<id>-<version>.zip` with contents at the zip root (maintenance scripts and build inputs excluded; `pack.mjs` hand-mirrors the `registry.ts` manifest rules, and `packages/server/test/extension-manifest-rules-drift.test.ts` pins a hash of those rules and fails with a pointer to the hub copy when they change), and one release per tag `<id>-v<version>` carrying exactly one zip (a second one would be picked by accident). Skills and workspaces in the same repo are tagged `skill-<id>-v<ver>` / `ws-<id>-v<ver>`; nothing installs them yet. Catalog today (only `glb-v1.0.0` is released so far):
263
263
 
264
264
  - `glb` 1.1.0 — `.glb .gltf .obj .stl`, `default`, read-only (`<model-viewer>` 4.3.1 offline bundle with Draco/Basis decoders; `.obj` / `.stl` converted in memory to GLB via three.js; `.gltf` must be self-contained — external `.bin` / textures → `error` frame; no `.mtl`).
265
265
  - `ipynb` 2.0.0 — `.ipynb`, `default`, `save`: notebook editor (markdown, KaTeX math, highlighted code, outputs incl. images / HTML tables; no widgets / JS outputs) that runs cells through the Jupyter proxy above when the host sends `init.jupyter` (Halo ≥ 1.6.1); older hosts edit and save without run controls.
@@ -8,12 +8,12 @@ A vendor-neutral observability layer built on the official OpenTelemetry JS SDK.
8
8
 
9
9
  | key | type | default | description |
10
10
  |---|---|---|---|
11
- | `general.observability.endpoint` | string | `''` | OTLP base URL of an OpenTelemetry collector (e.g. `http://localhost:4318`). Traces, metrics and logs all exported over OTLP http/protobuf — `http://` or `https://`, on the collector's OTLP/HTTP port (4318 by default); gRPC (4317) not supported. Empty = off. Restart required. |
12
- | `general.observability.service_name` | string | `halo` | OTel resource `service.name`. Restart required. |
13
- | `general.observability.headers` | string, **secret** | `''` | Extra headers for every OTLP request, comma-separated `k=v` (e.g. `authorization=Bearer …`). Restart required. |
14
- | `general.observability.capture_content` | boolean | `false` | Put prompt / completion / tool argument+result text on spans (`gen_ai.*.messages` etc.). Off = metadata only (model, tokens, latency, tool names). Restart required. |
11
+ | `general.observability.endpoint` | string | `''` | OTLP base URL of an OpenTelemetry collector (e.g. `http://localhost:4318`). Traces, metrics and logs all exported over OTLP http/protobuf — `http://` or `https://`, on the collector's OTLP/HTTP port (4318 by default); gRPC (4317) not supported. Empty = off. Server: restart required; cron / evolution runs: next run. |
12
+ | `general.observability.service_name` | string | `halo` | OTel resource `service.name`. Server: restart required; cron / evolution runs: next run. |
13
+ | `general.observability.headers` | string, **secret** | `''` | Extra headers for every OTLP request, comma-separated `k=v` (e.g. `authorization=Bearer …`). Server: restart required; cron / evolution runs: next run. |
14
+ | `general.observability.capture_content` | boolean | `false` | Put prompt / completion / tool argument+result text on spans (`gen_ai.*.messages` etc.). Off = metadata only (model, tokens, latency, tool names). Server: restart required; cron / evolution runs: next run. |
15
15
 
16
- All four keys are `globalOnly` and take effect on server restart — `otel.ts` reads them once at boot, before anything else touches the OTel API. A `halo cli` / `halo tui` process reads them the same way at its own start, so a change reaches the next run.
16
+ All four keys are `globalOnly`. The server's own exporter switches only on restart — `otel.ts` reads them once at boot, before anything else touches the OTel API. A `halo cli` / `halo tui` process reads them the same way at its own start, so a change reaches the next run — including the cron and evolution children the server spawns, which don't inherit the server's boot-time values (see the child-env rule below). A changed or cleared endpoint therefore reaches their next run without a server restart.
17
17
 
18
18
  **Which process exported.** Every process shares `service.name`, so the resource also carries `halo.process` = `server` \| `cli` \| `tui` (`HaloProcess` in `otel.ts`; `initObservability()` defaults to `server`). Cron and evolution children are `halo cli` runs, so they report `cli`; their `session.id` (`cron-<jobId>`, or the `__evo_agent__`-style agent name) tells them apart from a hand-run cli. The startup log line names it: `[Observability] OTLP export → … (service.name=…, halo.process=cli, …)`.
19
19
 
@@ -26,6 +26,8 @@ All four keys are `globalOnly` and take effect on server restart — `otel.ts` r
26
26
 
27
27
  No `OTEL_EXPORTER_OTLP_ENDPOINT` after the mapping → `enabled=false`, `initObservability()` returns without registering anything.
28
28
 
29
+ **Child env.** Every key the mapping actually set (one that was unset before) is recorded in `settingsOtelEnvKeys` (`child-env.ts`), and `cleanChildEnv()` strips those keys from every child it builds. Inherited, the server's boot-time values would win a cron / evolution `halo cli` child's own `??=`, so a changed endpoint would never arrive and a cleared one would keep exporting until a server restart. Operator-exported `OTEL_*` keys aren't recorded and still pass through ("operator wins" holds in children too). Side effect: terminals, sandbox shells and the Jupyter server — also built from `cleanChildEnv()` — no longer inherit Halo's settings-derived collector config.
30
+
29
31
  **External-SDK detection.** If an external SDK is already registered (e.g. via `node --require` of a vendor distro) — detected by comparing `ProxyTracerProvider.getDelegate()` against a fresh proxy's `NoopTracerProvider` delegate — halo logs `[Observability] external OpenTelemetry SDK already registered — using it, skipping built-in exporters` and skips its own exporters. The `??=` env mapping above still applies first, so the external SDK picks up halo's settings unless it already set its own.
30
32
 
31
33
  **TLS / HTTPS.** `https://` endpoints work out of the box against CA-signed certs — the exporter picks `node:http` vs `node:https` by URL scheme. Self-signed certs / mTLS go through the standard exporter env vars `OTEL_EXPORTER_OTLP_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` (read by the exporter itself, not by halo code). Verified: a self-signed sink is rejected with `DEPTH_ZERO_SELF_SIGNED_CERT` unless the cert is trusted — no silent bypass.
@@ -39,9 +39,12 @@ Defines the persisted-data format for every Halo surface. Format changes must re
39
39
  │ ├── settings.yaml # Global settings (API keys, credentials)
40
40
  │ ├── config.yaml # System config (admin password, server settings)
41
41
  │ ├── a2a-remotes.yaml # A2A remote list (server-wide, full-only; see design/a2a.md)
42
+ │ ├── server.env # Env snapshot for the systemd user unit (600), rewritten by `halo server start -d` / `restart`
42
43
  │ └── channels/
43
44
  │ └── channels.db # Channel accounts DB
44
45
 
46
+ ~/.config/systemd/user/halo.service # Outside ~/.halo: the user unit `halo server start -d` writes on Linux with systemd
47
+
45
48
  <workspace>/.halo/ # Workspace-level (overrides same-name global configs)
46
49
  ├── USER.md # Optional, overrides global
47
50
  ├── INSTRUCTIONS.md # Project-level instructions (overrides global INSTRUCTIONS.md)
@@ -413,16 +416,16 @@ A value of the form `<<ENV_NAME>>` is replaced at read time with `process.env.EN
413
416
  | `general.logging.level` | warn | `debug` / `info` / `warn` / `error`. Restart required |
414
417
  | `general.a2a.public_url` | `''` | Global-only. Origin peers use to reach this server for A2A (no trailing slash): the agent card's interface URL and the default push-webhook base for `a2a_send`. Empty = derived from the request host (no outbound dispatch without it or a remote's `push_base`). Env `HALO_A2A_PUBLIC_URL` overrides. See [a2a.md](a2a.md) |
415
418
  | `general.a2a.url_allowlist` | `100.64.0.0/10,*.ts.net` | Global-only. CIDRs / host patterns A2A egress may reach besides public https (push webhooks, remote cards and RPC, inbound image URLs). Private / loopback only when listed; plain http only to listed hosts / ranges; link-local always refused. See [a2a.md](a2a.md#push-delivery-and-url-policy) |
416
- | `general.observability.endpoint` | `''` | OTLP base URL of an OpenTelemetry collector (e.g. `http://localhost:4318`); empty = off. Restart required. See [observability.md](observability.md) |
417
- | `general.observability.service_name` | `halo` | OTel resource `service.name`. Restart required |
418
- | `general.observability.headers` | `''` | **Secret.** Extra OTLP request headers, comma-separated `k=v`. Restart required |
419
- | `general.observability.capture_content` | `false` | Put prompt/completion/tool text on spans. Restart required |
419
+ | `general.observability.endpoint` | `''` | OTLP base URL of an OpenTelemetry collector (e.g. `http://localhost:4318`); empty = off. Restart required (server); cron / evolution runs use a new value on their next run. See [observability.md](observability.md) |
420
+ | `general.observability.service_name` | `halo` | OTel resource `service.name`. Restart required (server); cron / evolution: next run |
421
+ | `general.observability.headers` | `''` | **Secret.** Extra OTLP request headers, comma-separated `k=v`. Restart required (server); cron / evolution: next run |
422
+ | `general.observability.capture_content` | `false` | Put prompt/completion/tool text on spans. Restart required (server); cron / evolution: next run |
420
423
 
421
424
  Schema source: [packages/server/src/settings-schema.ts](../../../packages/server/src/settings-schema.ts) `generalSection()`.
422
425
 
423
426
  ### Caching
424
427
 
425
- `config.ts` reads settings.yaml lazily with mtime-watching: every read stats the file and reparses if mtime changed. UI saves bump mtime, so the server picks up new values on the next read **without a restart** — except the keys marked "Restart required" above plus `general.agent.max_retries`, which `config.ts` reads once into object literals at boot (observability: `initObservability()`); their schema fields carry `restartRequired: true` and the Settings page shows a restart notice after saving one.
428
+ `config.ts` reads settings.yaml lazily with mtime-watching: every read stats the file and reparses if mtime changed. UI saves bump mtime, so the server picks up new values on the next read **without a restart** — except the keys marked "Restart required" above plus `general.agent.max_retries`, which `config.ts` reads once into object literals at boot (observability: `initObservability()`, mapped onto `OTEL_*` env; the cron / evolution `halo cli` children don't inherit those settings-derived vars — `cleanChildEnv()` strips them — so each re-reads settings at its own start); their schema fields carry `restartRequired: true` and the Settings page shows a restart notice after saving one.
426
429
 
427
430
  ### Orphans
428
431
 
@@ -96,7 +96,7 @@ loop {
96
96
  }
97
97
  ```
98
98
 
99
- > **单实例依赖**:长轮询循环跟 HTTP server 解耦 —— 即便 `:9527` 没绑上,只要进程活着,`runAccountLoop` 就继续拉消息。如果有孤儿 server 进程残留,每个进程都会独立收到同一条消息并 fan out 到不同 session(每个进程内存里的 `activeOverrides` 是独立的,`findActiveSessionId` 可能返回不同结果)。因此 `src/index.ts` 在启动时要抢 `~/.halo/global/server.pid` 单实例锁,详见 [dev/deploy.md](../dev/deploy.md#4-start-the-server)。
99
+ > **单实例依赖**:长轮询循环跟 HTTP server 解耦 —— 即便 `:9527` 没绑上,只要进程活着,`runAccountLoop` 就继续拉消息。如果有孤儿 server 进程残留,每个进程都会独立收到同一条消息并 fan out 到不同 session(每个进程内存里的 `activeOverrides` 是独立的,`findActiveSessionId` 可能返回不同结果)。因此 `src/index.ts` 在启动时要抢 `~/.halo/global/server.pid` 单实例锁,详见 [dev/deploy.md](../dev/deploy.md#5-start-the-server)。
100
100
 
101
101
  `handleInbound(account, msg)` — WeChat-specific head, then the shared tail:
102
102
  1. Extract `from_user_id` / text + media items / `context_token`
@@ -18,7 +18,7 @@ Total effort: typically a few hours depending on how weird the upstream API is a
18
18
 
19
19
  ### Existing providers
20
20
 
21
- Runtime names (`runtime:` value → class): `bedrock-invoke` → `BedrockAgent`, `bedrock-mantle` → `MantleAgent` (OpenAI Responses API), `anthropic-messages` → `AnthropicAgent`, `openai-chat` → `OpenAIAgent` (Chat Completions); the vendor subclasses keep their vendor name (`kimi`, `deepseek`, `minimax`, `qwen`, `hunyuan`, `doubao`, `zhipu`). No implementation class reads the provider id — the endpoint (required in `agent.yaml`) carries everything host-specific.
21
+ Runtime names (`runtime:` value → class): `bedrock-invoke` → `BedrockAgent`, `bedrock-mantle` → `MantleAgent` (OpenAI Responses API), `anthropic-messages` → `AnthropicAgent`, `openai-chat` → `OpenAIAgent` (Chat Completions); the vendor subclasses keep their vendor name (`kimi`, `deepseek`, `minimax`, `qwen`, `hunyuan`, `doubao`, `zhipu`, `gemini`). No implementation class reads the provider id — the endpoint (required in `agent.yaml`) carries everything host-specific.
22
22
 
23
23
  | Provider ID | `runtime` | Class | File | Notes |
24
24
  |---|---|---|---|---|
@@ -30,6 +30,7 @@ Runtime names (`runtime:` value → class): `bedrock-invoke` → `BedrockAgent`,
30
30
  | `hunyuan` | `hunyuan` | `HunyuanAgent` | `hunyuan-agent.ts` | Tencent Hy3 / Hy4 preview / Hy3 preview via OpenAI-compatible (tokenhub.tencentmaas.com), streaming via `fetchChatCompletionStream` (`stream_options.include_usage` accepted; usage in its own `choices: []` chunk), reasoning_effort low/high, thinking off sends `reasoning_effort: 'none'` (an omitted field thinks by default — live-probed 2026-10-10), automatic prompt caching (cached_tokens), no vision |
31
31
  | `doubao` | `doubao` | `DoubaoAgent` | `doubao-agent.ts` | ByteDance Doubao Seed 2.1 Pro + Seed-2.0 (pro/lite/mini/code) on Volcengine Ark, OpenAI-compatible, streaming via `fetchChatCompletionStream` (usage in its own `choices: []` chunk) + `thinking:{type:'enabled'/'disabled'}` (no `auto`), automatic prompt caching. Images go out as `image_url` content parts; only 2.1 Pro is `image: true` (the 2.0 models misread a red PNG), so the registry flag gates the rest |
32
32
  | `zhipu` | `zhipu` | `ZhipuAgent` | `zhipu-agent.ts` | Zhipu GLM-5.x on open.bigmodel.cn, OpenAI-compatible, streaming via `fetchChatCompletionStream` (tool-call arguments arrive complete in one fragment; usage chunk carries `choices`). Output cap via `max_tokens` only (`max_completion_tokens` is silently ignored). Thinking branched on modelId: **glm-5.3\*** always thinks — always sends `thinking:{type:'enabled',clear_thinking:false}`, `reasoning_effort` only when low/high/max (`disabled` / other efforts 400); other ids (glm-5.2) honour `thinking:{type:'disabled'}`. Vision per model: `glm-5.3-flash` / `glm-5.3-flashx` see `image_url` data URLs; `glm-5.3` / `glm-5.2` are text-only and 400 on any image part (code 1210, classified `multimodal_4xx` so leftover history images degrade). Automatic caching, `prompt_tokens_details.cached_tokens` (included in `prompt_tokens`, subtracted). |
33
+ | `gemini` | `gemini` | `GeminiAgent` | `gemini-agent.ts` | Google Gemini 3.x on the Gemini API's OpenAI-compatible surface (`generativelanguage.googleapis.com/v1beta/openai`, Bearer key), streaming via `fetchChatCompletionStream`. **Thought signatures**: the first tool call of a reply carries `extra_content.google.thought_signature`, and a follow-up that replays a current-turn call without it 400s. The runtime takes the signatures from the raw frames (the `onChunk` hook) and re-attaches them by tool-call id. A call it never saw (rebuilt instance) gets Google's placeholder `skip_thought_signature_validator`. Tool-call replies end with `finish_reason: "stop"`, so they're folded as `tool_calls`. Thinking can't be turned off on Gemini 3. The level goes as `extra_body.google.thinking_config.thinking_level` with `include_thoughts` (this 400s next to `reasoning_effort`). The summary streams inline as `<thought>…</thought>` at the start of `content` and is split out into thinking. Levels per model: 3.8-flash low / medium / high, 3.5-flash-lite minimal…high, xhigh / max invalid everywhere. Usage: `completion_tokens` excludes thought tokens, so output = `total_tokens − prompt_tokens`. Implicit caching via `prompt_tokens_details.cached_tokens` (included in `prompt_tokens`, subtracted). Vision via `image_url` data URLs (probed 2026-10-12) |
33
34
  | `openai` | `openai-chat` | `OpenAIAgent` | `openai-agent.ts` | **Generic OpenAI-compatible**. Use for native OpenAI or any third-party OpenAI gateway (incl. Ollama / llama.cpp). Ships a short Bedrock Mantle list callable with a Bedrock API key (`openai.gpt-5.5` default, `openai.gpt-5.4`; `defaultEndpoint` Mantle us-east-1 `/openai/v1`, presets us-east-1 / us-west-2 / api.openai.com — GPT-6.x / 5.6 reject `max_tokens`, so they stay on the Responses-API providers) plus the legacy `gpt-4o` placeholder (official API only); any other endpoint + model id can be typed, and capabilities for unlisted ids are openly editable in the form. Streaming via `fetchChatCompletionStream` — always sends `stream: true` + `stream_options: { include_usage: true }` (OpenAI's own field; Ollama ≥ 0.3 / vLLM / LM Studio accept it too). A gateway that rejects unknown fields would 400 — accepted trade-off, no non-streaming fallback. Forgiving about cache field names (`cachedPromptTokens`: first value > 0 of `prompt_tokens_details.cached_tokens`, top-level `cached_tokens`, `prompt_cache_hit_tokens`, `cache_read_tokens`) and reasoning field names (`reasoning_content` for OpenAI o-series/DeepSeek, `reasoning` for Ollama/llama.cpp). |
34
35
  | `anthropic` | `anthropic-messages` | `AnthropicAgent` | `anthropic-agent.ts` | **Generic Anthropic Messages**. Use for api.anthropic.com or any Anthropic-compatible gateway. Ships a short Bedrock Mantle list callable with a Bedrock API key as `x-api-key` (`anthropic.claude-opus-5-5` default, `anthropic.claude-haiku-4-5`; `defaultEndpoint` Mantle us-east-1 `/anthropic`, presets us-east-1 / us-west-2 / api.anthropic.com) plus the legacy `claude-sonnet-4-6` placeholder (official API only); any other endpoint + model id can be typed. Streaming via `fetchAnthropicStream` (the gateway must support `stream: true` SSE — the reference Anthropic behaviour). Standard `cache_control: ephemeral` with optional `ttl: '1h'`. |
35
36
  | `mimo-token-plan-china` | `anthropic-messages` | `AnthropicAgent` | (reuses `anthropic-agent.ts`) | Xiaomi MiMo (UI name "Xiaomi MiMo"; the id keeps its historical name so existing configs don't break) — Anthropic-compatible gateway, streams like `anthropic` (gateway built for Claude Code, which always streams; not live-probed); no dedicated class, just `runtime: anthropic-messages` in its yaml (`defaultEndpoint` `token-plan-cn.xiaomimimo.com/anthropic`). The key type picks the endpoint: Token Plan `tp-`/`ttp-` keys only work on `token-plan-{cn,sgp,ams}.xiaomimimo.com/anthropic`, pay-as-you-go `sk-` keys only on `api.xiaomimimo.com/anthropic` — all four ship as `endpointPresets` (pay-as-you-go live-probed 2026-09-24; sgp / ams from the docs only). V2.6 (`mimo-v2.6-pro` / `-flash`, both vision, 1M / 131072) is current; `mimo-v2.5(-pro)` kept in the template until the official 2026-10-21 deprecation, then drop |
@@ -36,6 +36,7 @@ File: `packages/server/src/routes/files.ts`
36
36
  | GET | `/api/files?path=&projectId=` | Read file content (max 10 MB) |
37
37
  | GET | `/api/files/stat?path=&projectId=` | Lightweight mtime + size |
38
38
  | GET | `/api/files/download?path=&projectId=&inline=` | Download or inline-preview — streams the file (no full read into memory), supports `Range` (206 Partial Content) so `<video>`/`<audio>` can seek + partial-load; aborts the read if the client disconnects. Missing file → 404 `{error:'File not found'}` |
39
+ | GET | `/api/files/download-folder?path=&projectId=[&include=excluded][&estimate=1]` | Folder → one zip, built by yazl while it streams (zip64, UTF-8 names; already-compressed extensions stored, the rest deflated; one file open at a time, paced by backpressure; never staged on disk or in memory). `path` must be a directory inside the project, `''` = workspace root; it may itself be a folder symlink inside the workspace (resolved, then checked like any target); no `/tmp` exception. `Content-Disposition: attachment` with RFC 5987 `filename*` = `<folder name>.zip` (root → workspace dir name); no `Content-Length`, no Range. **Excluded by default**: any *folder* whose name is in `IGNORED_SEGMENTS` (`ws/file-watcher.ts`); `include=excluded` brings them back. **Always excluded**: `isHiddenHostPath` / `isHiddenWorkspacePath` (also against a nested `.halo/`), plus common credential stores (~/.kube, cloud CLI configs, shell histories — a zip-only list in `folder-zip.ts`, matched relative to the home dir), `.halo/settings.yaml`, `.env` / `.env.*`. Names a zip can't hold — any containing `\`, or a top-level `X:` prefix — are skipped as `unreadable`. Directory symlinks never followed; file symlinks only when the realpath stays inside the workspace and passes those checks. `estimate=1` → `{files, bytes, excluded: {<reason>: n}, ignoredNames: {<folder>: n}}` with reasons `ignored` / `blocked` / `symlink` / `unreadable`, same rules, no content read. More than 200,000 files or a walk over 10 s → **413** `{error}` (both modes, before any header). A blocked target → 403. Anything left out → `_halo-excluded.txt` at the zip root (grouped by reason, a pruned dir listed once, 1000 paths per reason + a count). Mid-stream (status already sent): a file that fails to open is skipped, a read error partway through truncates that entry — both are listed there. A client disconnect stops the walk and destroys the zip stream |
39
40
  | PUT | `/api/files` | Save file (body: `{path, content, projectId}`) |
40
41
  | PUT | `/api/files/raw?path=&projectId=[&expectMtime=][&create=1][&append=1][&root=]` | Replace an **existing** file's bytes with the raw request body (binary-safe; `PUT /api/files` is utf-8 text only). Used by canvas preview extensions' save path. 404 if the file doesn't exist (save never creates) unless `create=1`; 400 for a directory. `expectMtime` is the `modifiedAt` the client loaded from `/api/files/stat` (float ms — compared after `Math.round`): when the on-disk mtime differs → **409** `{error:'conflict', mtime, size}` so the client can offer overwrite (retry without / with the new `expectMtime`). `create=1` creates a missing file (parent dirs `mkdir -p`); `append=1` appends instead of overwriting (`expectMtime` ignored; with `create=1` a missing file is created first) — the bundle extensions' `fs` write / append. `root=<dir>` (workspace-relative; the bundle host always sends its bundle path): `<dir>` must be an existing directory and `path` strictly inside it, else **404** `{error:'Bundle root not found'}` with nothing created. Success → `{ok, path, mtime, size}` |
41
42
  | POST | `/api/files/new` | Create empty file |
@@ -46,7 +47,7 @@ File: `packages/server/src/routes/files.ts`
46
47
 
47
48
  **Inline Content-Type** for images comes from core's shared extension→MIME table (`imageMimeFromExt`, `packages/core/src/media/mime.ts`) on both `/api/files/download?inline=1` and `/api/web/file`; each route keeps only its own non-image entries (pdf / html / video / audio) and falls back to `application/octet-stream`. That table is why `.bmp` now serves as `image/bmp` on both — the two routes used to carry their own copies and `/api/web/file` returned `application/octet-stream` for it. `.svg` / `.ico` / `.avif` get a real image Content-Type when served, but are deliberately not "photos" (they're outside `IMAGE_EXTS`) so they never reach a vision model or a channel's sendPhoto.
48
49
 
49
- All file operations validate the path stays inside the project root (prevents path traversal). The check (`validatePath` in `routes/workspace-path.ts`, shared with `data-preview.ts` and `git.ts`) realpaths both sides — longest existing ancestor plus the re-appended tail — so a symlink inside the workspace pointing outside it is rejected too, not just lexical `../`. One exception: `GET /api/files/download` also accepts absolute paths under `/tmp/` so that agent-produced working files (e.g. Playwright screenshots) can be inline-previewed from chat media chips.
50
+ All file operations validate the path stays inside the project root (prevents path traversal). The check (`validatePath` in `routes/workspace-path.ts`, shared with `data-preview.ts` and `git.ts`) realpaths both sides — longest existing ancestor plus the re-appended tail — so a symlink inside the workspace pointing outside it is rejected too, not just lexical `../`. One exception: `GET /api/files/download` (not `download-folder`) also accepts absolute paths under `/tmp/` so that agent-produced working files (e.g. Playwright screenshots) can be inline-previewed from chat media chips.
50
51
 
51
52
  ### Filesystem browse (not project-scoped — for the workspace picker)
52
53
 
@@ -16,8 +16,8 @@ set -e
16
16
  version='x.y.z' # exact requested version
17
17
  curl -sfI "https://registry.npmjs.org/@turmind/halo/-/halo-$version.tgz" >/dev/null # tarball replicated? (a fresh publish can 404 for ~15 min)
18
18
  sudo sh -c 'umask 022; npm install -g --prefix /usr --no-audit --no-fund "@turmind/halo@$1"' sh "$version"
19
- installed=$(sudo -u ubuntu env HOME=/home/ubuntu /usr/bin/halo --version 2>&1)
20
- test "$installed" = "halo $version" # CLI writes to stderr; mismatch or launch failure stops before restart
19
+ installed=$(sudo -u ubuntu env HOME=/home/ubuntu /usr/bin/halo --version)
20
+ test "$installed" = "halo $version" # mismatch or launch failure stops before restart
21
21
  sudo systemctl restart halo.service
22
22
  curl --fail --retry 10 --retry-connrefused --retry-delay 1 --max-time 3 http://127.0.0.1:9527/api/health # status ok, requested version
23
23
  sudo journalctl -u halo.service --since '2 minutes ago' -n 40 --no-pager
@@ -49,9 +49,40 @@ Nginx is **not required**. Use it only for domain routing, SSL termination, or w
49
49
 
50
50
  ## Prerequisites
51
51
 
52
- - Node.js ≥ 22
53
- - pnpm 11 (pinned by the root `packageManager`; `corepack enable` picks it up)
54
- - AWS credentials configured (`~/.aws/credentials` or env vars) with Bedrock access
52
+ - Node.js ≥ 22 — Ubuntu 24.04's apt `nodejs` is 18, too old; use NodeSource (`curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs`), `brew install node@22`, or nvm / fnm. On an older Node, `halo` exits 1 at launch with the version it found and these install hints instead of crashing. On a just-booted cloud VM the first `apt-get install` can fail on the dpkg lock (`Could not get lock …`, exit 100) while cloud-init / the SSM agent is still installing packages — wait a minute and rerun
53
+ - pnpm 11 — source builds only (pinned by the root `packageManager`; `corepack enable` picks it up)
54
+ - AWS credentials — optional, only for the Bedrock providers (`~/.aws/credentials`, env vars, or an instance role)
55
+ - bubblewrap (Linux) — needed for the `workspace` / `readonly` access levels: `sudo apt install bubblewrap`; on Ubuntu 23.10+ / 24.04 also the [AppArmor profile](#bubblewrap-on-ubuntu-2310-and-2404-apparmor)
56
+
57
+ ### bubblewrap on Ubuntu 23.10 and 24.04 (AppArmor)
58
+
59
+ Ubuntu 23.10+ ships `kernel.apparmor_restrict_unprivileged_userns = 1`, and the `bubblewrap` package brings no AppArmor profile, so `apt install bubblewrap` alone isn't enough. Check with:
60
+
61
+ ```bash
62
+ bwrap --ro-bind / / --die-with-parent -- /bin/true
63
+ # bwrap: setting up uid map: Permission denied ← blocked by AppArmor
64
+ ```
65
+
66
+ Halo's boot probe sees the same failure. The server warns once at startup, `[Server] OS sandbox unavailable (bwrap cannot create namespaces: bwrap: setting up uid map: Permission denied) — workspace/readonly sessions get no shell_exec. Fix: …` with a link to this section, and then runs without an OS sandbox. `halo cli` / `tui` print the same line, tagged `[cli]`, but only for `--access workspace|readonly`; without bubblewrap installed, the fix it names is `sudo apt install bubblewrap`. Fix it with a profile that lets only `bwrap` create user namespaces:
67
+
68
+ ```bash
69
+ sudo apt-get install -y bubblewrap
70
+ sudo tee /etc/apparmor.d/bwrap >/dev/null <<'EOF'
71
+ abi <abi/4.0>,
72
+ include <tunables/global>
73
+ profile bwrap /usr/bin/bwrap flags=(unconfined) {
74
+ userns,
75
+ include if exists <local/bwrap>
76
+ }
77
+ EOF
78
+ sudo apparmor_parser -r /etc/apparmor.d/bwrap
79
+ halo server restart && curl -s --retry 15 --retry-connrefused --retry-delay 1 localhost:9527/api/health # "sandbox":"bwrap"
80
+ # a unit-run server is restarted through systemctl (Option C: may need the printed `sudo systemctl restart halo`)
81
+ ```
82
+
83
+ Alternative: `sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0` (persist it in `/etc/sysctl.d/`). That also works, but it lifts the restriction for **every** program, not just bwrap.
84
+
85
+ Check: `GET /api/health` reports `"sandbox":"bwrap"`; `"sandbox":null` means non-full sessions still have no OS sandbox (and no `shell_exec`).
55
86
 
56
87
  ## 1. Install dependencies and build
57
88
 
@@ -78,7 +109,7 @@ halo setup # interactive — picks up arrow-key UI on TTYs
78
109
 
79
110
  Re-run any time to change the password, refresh model keys, or toggle optional skills. Built-in agent / skill files are force-overwritten on every run; user-created agents and skills are left alone.
80
111
 
81
- For Docker / CI builds where stdin isn't a TTY, see the **Docker** section below.
112
+ For Docker / CI builds where stdin isn't a TTY, see [Non-interactive (Docker / CI) details](#non-interactive-docker--ci-details) below.
82
113
 
83
114
  ## 4. Environment variables (optional)
84
115
 
@@ -93,7 +124,9 @@ When `HALO_PASSWORD` is set, the password chosen via `halo setup` is ignored at
93
124
 
94
125
  ## 5. Start the server
95
126
 
96
- > **单实例锁**:server 启动时会写 `~/.halo/global/server.lock`(有 `flock` 命令时用 flock,没有就回退到 pid 探测),退出时自动清理。如果 lock 里记录的进程还活着,新 server 会拒绝启动并打印 `kill <pid>` 提示。原因是 WeChat 长轮询循环跟 HTTP server 解耦,多个进程并存会导致同一条微信消息被 fan out 到多个 session。陈旧 lock(进程已不在)会被自动识别并清除。要手动重启先 kill 旧的:`kill $(cat ~/.halo/global/server.lock)`。
127
+ > **Single-instance lock**: on start the server writes `~/.halo/global/server.lock` (flock when the `flock` command exists, otherwise a pid probe) and removes it on exit. If the process recorded in the lock is still alive, a new server refuses to start and prints a `kill <pid>` hint. The reason: the WeChat long-poll loop is decoupled from the HTTP server, so several processes side by side would fan the same WeChat message out to several sessions. A stale lock (process gone) is detected and cleared automatically. To restart by hand, kill the old one first: `kill $(cat ~/.halo/global/server.lock)`.
128
+
129
+ Options A and B below run a **source checkout**; for the npm install see [systemd for the npm install](#option-c-systemd-for-the-npm-install).
97
130
 
98
131
  ### Option A: quick start
99
132
 
@@ -128,6 +161,39 @@ sudo systemctl daemon-reload
128
161
  sudo systemctl enable --now halo
129
162
  ```
130
163
 
164
+ ### Option C: systemd for the npm install
165
+
166
+ With `@turmind/halo` installed via npm (see [§8](#8-install-via-npm-recommended)) on Linux with systemd, the simple path is `halo server start -d`. It writes and enables the user unit `~/.config/systemd/user/halo.service` (`Restart=on-failure`, `RestartSec=5`), turns on linger so it starts at boot, and saves your shell's environment to `~/.halo/secrets/server.env` for it. If linger can't be enabled it still starts, and warns `sudo loginctl enable-linger <user>`.
167
+
168
+ Write the system unit below when you want a system-level unit instead. If `start -d` already made the user unit, `systemctl --user disable --now halo` first, or both start at boot. Once a unit in `/etc/systemd/system/` runs `halo server start` as you (`User=` you, same HOME), `start -d` starts it instead of creating a user unit. The system unit runs the `halo` binary itself — no `WorkingDirectory`, and no `HALO_PASSWORD` once `halo setup` has stored the password hash (run setup as the same user):
169
+
170
+ ```bash
171
+ sudo tee /etc/systemd/system/halo.service <<'EOF'
172
+ [Unit]
173
+ Description=Halo Server
174
+ After=network.target
175
+
176
+ [Service]
177
+ Type=simple
178
+ User=YOUR_USER
179
+ ExecStart=/usr/bin/halo server start
180
+ Restart=on-failure
181
+ RestartSec=5
182
+
183
+ [Install]
184
+ WantedBy=multi-user.target
185
+ EOF
186
+
187
+ sudo systemctl daemon-reload
188
+ sudo systemctl enable --now halo
189
+ ```
190
+
191
+ Use `which halo` for the `ExecStart` path (`/usr/bin/halo` with apt / NodeSource Node). Each start prints `[Server] Halo <version> listening on port 9527 on all network interfaces — local: http://localhost:9527` and a network-exposure line to stderr, whatever the log level. For a system unit, look in the journal (`journalctl -u halo`).
192
+
193
+ Under either unit kind, `halo server stop|restart|status` find the unit that owns the server (the lock pid's cgroup) and go through `systemctl` (`--user` for the user unit). A system unit may need sudo: when polkit refuses, the command prints the exact `sudo systemctl restart|stop halo` line to run. Running that directly is equally valid. `stop` only stops the unit; it starts again at boot until you disable it (`systemctl --user disable halo`, or `sudo systemctl disable halo`). Upgrade with `halo upgrade && halo server restart`.
194
+
195
+ **Known limits**: the user unit pins the Node binary that ran `start -d` (`process.execPath`), so after an nvm Node upgrade run `halo server restart` once to rewrite it.
196
+
131
197
  ### Crash semantics — why `Restart=on-failure` is load-bearing
132
198
 
133
199
  The server **exits 1 on any uncaught exception** (`process.on('uncaughtException')`
@@ -137,18 +203,22 @@ half-taken locks and half-written files, and since the db is the source of truth
137
203
  — every daemon (cron / evo ticker / channels) reconciles from it on boot — a
138
204
  restart is a full recovery while a zombie process silently corrupts state.
139
205
 
140
- So the supervisor is what turns a crash into a ~5s blip: the unit above pairs
141
- `Restart=on-failure` (fires exactly on non-zero exit) with `RestartSec=5`.
142
- Docker deployments want an equivalent `restart:` policy. Under a bare `nohup
206
+ So the supervisor is what turns a crash into a ~5s blip: the units above — and
207
+ the user unit `halo server start -d` writes — pair `Restart=on-failure` (fires
208
+ exactly on non-zero exit) with `RestartSec=5`. Docker deployments want an
209
+ equivalent `restart:` policy. Under a bare `nohup
143
210
  node dist/index.js` / foreground `halo server start` there is still no
144
211
  supervisor — a crash is a hard stop, with the stack in
145
212
  `~/.halo/global/logs/server.log` for whoever restarts it.
146
213
  `unhandledRejection` is only logged, not fatal.
147
214
 
148
- **`halo server start -d` brings its own bounded supervisor.** The detached
149
- process `-d` spawns is not the server: it re-execs `halo server start`
150
- (foreground) with `HALO_SUPERVISE=1`, and *that* process supervises the real
151
- server as its child (`packages/cli/src/server-supervisor.ts`). Policy:
215
+ **Off systemd, `halo server start -d` brings its own bounded supervisor** —
216
+ on macOS, Windows, Linux without systemd, a HOME that isn't your passwd home,
217
+ or a user manager the shell can't reach (on Linux `-d` prints a one-line note
218
+ saying which). The detached process `-d` spawns is not the server: it re-execs
219
+ `halo server start` (foreground) with `HALO_SUPERVISE=1`, and *that* process
220
+ supervises the real server as its child (`packages/cli/src/server-supervisor.ts`).
221
+ Policy:
152
222
 
153
223
  - **Restarts only on failure** — non-zero exit, or death by any signal other
154
224
  than `SIGTERM` / `SIGINT`. `SIGKILL` (OOM killer) and `SIGSEGV` count as
@@ -159,7 +229,9 @@ server as its child (`packages/cli/src/server-supervisor.ts`). Policy:
159
229
  server start -d`.
160
230
  - **2s pause between attempts**, so an instant-crash loop can't spin the CPU.
161
231
  - Every decision lands in the daemon log as `[respawn] …` — the stdout/stderr
162
- capture `~/.halo/logs/server.log`, which `halo server logs` tails.
232
+ capture `~/.halo/logs/server.log`, which `halo server logs` tails. Only this
233
+ supervisor writes them; under the user unit a crash restart is systemd's
234
+ `RestartSec=5` (`systemctl --user status halo` shows it).
163
235
 
164
236
  `halo server stop` (and `--force`) is not mistaken for a crash: it stamps the
165
237
  server pid into a marker file `~/.halo/global/server.stop` *before* signalling,
@@ -172,9 +244,11 @@ one's marker.
172
244
 
173
245
  Pid ownership is unchanged: `~/.halo/global/server.lock` is still written and
174
246
  flock'd by the **server** process itself, so `halo server stop|restart|status`
175
- keep targeting the server exactly as before. The supervisor has no pidfile of
176
- its own; killing it directly leaves the server running as an orphan (same end
177
- state as the pre-supervisor `-d`), and `halo server stop` still stops it.
247
+ target the server's own pid (or, for a unit-run server, its unit). The
248
+ supervisor has no pidfile of its own; killing it directly leaves the server
249
+ running as an orphan (same end state as the pre-supervisor `-d`), and `halo
250
+ server stop` still stops it. On a systemd host, `halo server restart` moves a
251
+ supervised `-d` server under the user unit.
178
252
 
179
253
  ## 6. Verify
180
254
 
@@ -196,7 +270,7 @@ cd /path/to/halo
196
270
  cd packages/admin && npx next build --no-lint && node scripts/copy-monaco.mjs && cd ../..
197
271
  pnpm --filter @turmind/halo-server build
198
272
 
199
- # 用 lock 文件里的 pid 确保旧进程彻底退出(避免孤儿进程继续长轮询)
273
+ # use the pid in the lock file so the old process really exits (an orphan would keep long-polling)
200
274
  kill $(cat ~/.halo/global/server.lock) 2>/dev/null
201
275
  sleep 2
202
276
  cd packages/server && HALO_PASSWORD=your_password nohup node dist/index.js > /tmp/server.log 2>&1 &
@@ -217,8 +291,8 @@ This installs the `halo` binary on `$PATH`. Subcommands available:
217
291
  | `halo setup` | Interactive password / port / model keys / optional skills setup |
218
292
  | `halo setup --non-interactive` (alias `-y`) | Skip every prompt — seed templates only, supply password via `HALO_PASSWORD` env. Use in Dockerfiles / CI. |
219
293
  | `halo upgrade` | Bump the npm install in place. Compares the bundled version against `npm view @turmind/halo version`; no-op if already latest, otherwise runs `npm install -g @turmind/halo@latest` and prints a server-restart hint. On EACCES, suggests retrying with `sudo`. |
220
- | `halo server start` | Launch HTTP/WS server (foreground). Add `-d` for daemon. |
221
- | `halo server stop` / `restart` / `status` / `logs` | Server lifecycle |
294
+ | `halo server start` | Launch HTTP/WS server (foreground). Add `-d` for daemon — on Linux with systemd, a user unit that starts at boot ([Option C](#option-c-systemd-for-the-npm-install)). |
295
+ | `halo server stop` / `restart` / `status` / `logs` | Server lifecycle; a unit-run server goes through `systemctl` |
222
296
  | `halo tui` | Interactive TUI client |
223
297
  | `halo cli "<prompt>"` | One-shot prompt → reply, exit |
224
298
  | `halo agents` / `halo sessions` | List agents / sessions |
@@ -228,7 +302,7 @@ This installs the `halo` binary on `$PATH`. Subcommands available:
228
302
  ### Upgrade flow
229
303
 
230
304
  1. `halo upgrade` — bumps the on-disk npm package
231
- 2. `halo server restart` — server's startup check sees `~/.halo/global/.template-version` is behind the new bundled `TEMPLATE_VERSION`, runs `ensureHaloHome` automatically, then starts (`halo cli` / `halo tui` run the same check on start, so a CLI-only install refreshes without a server). Refreshes `docs/`, built-in agents, built-in skills, system prompts, and the model registry. User-owned files (USER.md, custom agents/skills, INSTRUCTIONS.md overrides) are left alone. See `init.ts` for the per-category overwrite policy.
305
+ 2. `halo server restart` (under a user or system unit alike; `sudo systemctl restart halo` also works for a system unit) — server's startup check sees `~/.halo/global/.template-version` is behind the new bundled `TEMPLATE_VERSION`, runs `ensureHaloHome` automatically, then starts (`halo cli` / `halo tui` run the same check on start, so a CLI-only install refreshes without a server). Refreshes `docs/`, built-in agents, built-in skills, system prompts, and the model registry. User-owned files (USER.md, custom agents/skills, INSTRUCTIONS.md overrides) are left alone. See `init.ts` for the per-category overwrite policy.
232
306
 
233
307
  ### Public release command reference
234
308
 
@@ -244,7 +318,9 @@ Use only for a new public release; the retained details here are not extra prepa
244
318
 
245
319
  **npm token gotcha**: `npm publish` on this package needs a granular access token created with **"Bypass 2FA"** checked — scope / permission alone yields `403 Two-factor authentication or granular access token with bypass 2fa enabled is required`. `npm whoami` and `npm token list` succeed with a non-bypass token, so neither is a valid pre-flight; check `GET https://registry.npmjs.org/-/npm/v1/tokens` (with the token as bearer) and look for `"bypass_2fa": true` on the token in use before starting a release.
246
320
 
247
- The published package contains a single bundled JS entry (~1.4 MB), all built-in templates (agents / skills / prompts / models), bundled platform docs, and the admin Web UI static export. Total install footprint ≈ 120 MB after npm dedupes shared deps.
321
+ The published package contains a single bundled JS entry (~1.4 MB), all built-in templates (agents / skills / prompts / models), bundled platform docs, and the admin Web UI static export. Total install footprint is about 280 MB on disk (`du`; mostly `node_modules`).
322
+
323
+ Two log files: `~/.halo/logs/server.log` is the stdout/stderr capture of `halo server start -d`, whether the user unit or the off-systemd supervisor runs it (what `halo server logs` tails; the supervisor's `[respawn]` lines appear only off systemd; a system unit logs to the journal instead); `~/.halo/global/logs/server.log` is the server's structured, rotated log (level `general.logging.level`, default `warn`).
248
324
 
249
325
  ### Non-interactive (Docker / CI) details
250
326
 
@@ -252,8 +328,20 @@ The published package contains a single bundled JS entry (~1.4 MB), all built-in
252
328
 
253
329
  - Seeds / refreshes `~/.halo/global/` from the bundled templates (per the per-category overwrite policy in `init.ts`)
254
330
  - Generates `server.jwt_secret` if missing
255
- - Binds the built-in agents to the provider named by `HALO_DEFAULT_PROVIDER`, when set (an unknown id only warns)
331
+ - Binds every built-in agent (default / executor / deep-executor plus the internal ones) to the provider named by `HALO_DEFAULT_PROVIDER`, when set (an unknown id only warns)
332
+ - For that provider, writes each env-backed key into `~/.halo/secrets/settings.yaml` as a `<<NAME>>` placeholder when `NAME` is set in setup's env (e.g. `DEEPSEEK_API_KEY` → `deepseek.secrets.api_key: "<<DEEPSEEK_API_KEY>>"`) — never the key itself; an unset `NAME` only warns. Bedrock is skipped: the AWS credential chain reads `AWS_*` on its own
256
333
  - **Does not** set a password — supply one via `HALO_PASSWORD` env
334
+ - **Does not** store any literal model key — `HALO_PASSWORD` is the admin password only
335
+
336
+ The placeholder is expanded on read (see [env.md](env.md)). Without `HALO_DEFAULT_PROVIDER`, or for another provider, write it yourself; e.g. for DeepSeek:
337
+
338
+ ```yaml
339
+ deepseek:
340
+ secrets:
341
+ api_key: "<<DEEPSEEK_API_KEY>>"
342
+ ```
343
+
344
+ The variable must be set wherever the file is read — the server and `halo cli` / `tui` alike; under systemd add `Environment=DEEPSEEK_API_KEY=…` or an `EnvironmentFile=` to the unit. If you create the file yourself instead of editing the one `halo setup` wrote, `chmod 600` it.
257
345
 
258
346
  Minimal Dockerfile:
259
347
 
@@ -261,10 +349,10 @@ Minimal Dockerfile:
261
349
  FROM node:22-slim
262
350
  RUN apt-get update && apt-get install -y --no-install-recommends bubblewrap && rm -rf /var/lib/apt/lists/*
263
351
  RUN npm install -g @turmind/halo
264
- ENV HALO_PASSWORD=changeme
352
+ ENV HALO_PASSWORD=changeme HALO_DEFAULT_PROVIDER=deepseek
265
353
  ENV HALO_PORT=9527
266
354
  EXPOSE 9527
267
- CMD halo setup -y && halo server start
355
+ CMD halo setup -y && halo server start # key from `docker run -e DEEPSEEK_API_KEY=…`; setup writes its placeholder
268
356
  ```
269
357
 
270
358
  ## 9. Observability (OpenTelemetry collector)
@@ -522,7 +522,19 @@ Still good practice: build server + admin **before** `pnpm dist:arm64`.
522
522
  close. Fixed by matching `Get-CimInstance Win32_Process` `.ExecutablePath`,
523
523
  which is readable from either bitness. Rule: **any PowerShell the installer
524
524
  runs is 32-bit** — never filter processes by `Get-Process .Path` /
525
- `.MainModule` there. To tell the two causes apart on a user's box, list
525
+ `.MainModule` there. The match itself was a second trap: `$INSTDIR` used to
526
+ be spliced into a single-quoted PS literal with `-like`, so a `'` in the
527
+ install path ended the string (parse error, nothing killed) and `[` / `]`
528
+ read as wildcards (could match a sibling dir). The macro now puts `$INSTDIR`
529
+ in env var `HALO_INSTDIR` (`System::Call
530
+ 'kernel32::SetEnvironmentVariable(…)'`, inherited by the `nsExec` child) and
531
+ matches `ExecutablePath` with a literal, case-insensitive
532
+ `.StartsWith($env:HALO_INSTDIR + '\')` — rule: never splice a path into the
533
+ PS command line. A live `jupyter server` (ipynb kernels) could hold the
534
+ install dir open too, since it inherited the server's cwd inside it; both
535
+ jupyter spawns now pass `cwd: os.homedir()` (see
536
+ [canvas-extensions.md](../design/canvas-extensions.md#notebook-kernels-jupyter-proxy)).
537
+ To tell the two causes apart on a user's box, list
526
538
  what still runs from the install dir (64-bit PowerShell): `Get-CimInstance
527
539
  Win32_Process | ? ExecutablePath -like "$env:LOCALAPPDATA\Programs\Halo\*"`
528
540
  — non-empty = this cause (append `| % { Stop-Process -Id $_.ProcessId