@turmind/halo 2.2.3 → 2.2.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/README.md +2 -1
- package/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/1989-9d30cdb1a86e4beb.js +1 -0
- package/admin-out/_next/static/chunks/4362.115405260299210d.js +1 -0
- package/admin-out/_next/static/chunks/{7613.fe24922a6a839656.js → 7073.4081ed209e6748bd.js} +1 -1
- package/admin-out/_next/static/chunks/{7073.9d5f9dd7a77827b7.js → 7613.9c1657bb3b52d1b5.js} +1 -1
- package/admin-out/_next/static/chunks/app/{page-10a4afac9cbc27a2.js → page-c9d174a5432b0007.js} +1 -1
- package/admin-out/_next/static/chunks/{webpack-9532ec7f1fa9a757.js → webpack-1970c5140e4019b7.js} +1 -1
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +4 -4
- package/bundled-docs/design/a2a.md +3 -1
- package/bundled-docs/design/architecture.md +1 -1
- package/bundled-docs/design/node.md +18 -39
- package/bundled-docs/design/observability.md +2 -2
- package/bundled-docs/design/relay.md +1 -1
- package/bundled-docs/design/session.md +2 -2
- package/bundled-docs/design/storage.md +4 -1
- package/bundled-docs/design/ws.md +2 -2
- package/bundled-docs/dev/api.md +4 -3
- package/bundled-docs/dev/deploy.md +1 -1
- package/bundled-docs/dev/env.md +2 -1
- package/bundled-docs/dev/tools.md +10 -6
- package/bundled-docs/guide/README.md +1 -1
- package/bundled-docs/guide/cli.md +1 -30
- package/bundled-docs/guide/node.md +57 -36
- package/bundled-docs/requirements/settings.md +38 -1
- package/dist/evo-wrapper.js +323 -247
- package/dist/index.js +1727 -1962
- package/package.json +2 -1
- package/templates/agents/default/agent.yaml +1 -1
- package/templates/models/aws-bedrock-openai.yaml +81 -7
- package/admin-out/_next/static/chunks/1989-3b2faa2731d973c4.js +0 -1
- package/admin-out/_next/static/chunks/4362.9d2ee4f9b6facc49.js +0 -1
- /package/admin-out/_next/static/{bbPEaP5uh2KvLzSHGU2CO → vf5W8US5y8B2f1E6zJJw3}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{bbPEaP5uh2KvLzSHGU2CO → vf5W8US5y8B2f1E6zJJw3}/_ssgManifest.js +0 -0
package/admin-out/_next/static/chunks/{webpack-9532ec7f1fa9a757.js → webpack-1970c5140e4019b7.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{"use strict";var e={},a={};function d(t){var f=a[t];if(void 0!==f)return f.exports;var r=a[t]={id:t,loaded:!1,exports:{}},c=!0;try{e[t].call(r.exports,r,r.exports,d),c=!1}finally{c&&delete a[t]}return r.loaded=!0,r.exports}d.m=e,(()=>{var e=[];d.O=(a,t,f,r)=>{if(t){r=r||0;for(var c=e.length;c>0&&e[c-1][2]>r;c--)e[c]=e[c-1];e[c]=[t,f,r];return}for(var b=1/0,c=0;c<e.length;c++){for(var[t,f,r]=e[c],o=!0,n=0;n<t.length;n++)(!1&r||b>=r)&&Object.keys(d.O).every(e=>d.O[e](t[n]))?t.splice(n--,1):(o=!1,r<b&&(b=r));if(o){e.splice(c--,1);var i=f();void 0!==i&&(a=i)}}return a}})(),d.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return d.d(a,{a:a}),a},(()=>{var e,a=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__;d.t=function(t,f){if(1&f&&(t=this(t)),8&f||"object"==typeof t&&t&&(4&f&&t.__esModule||16&f&&"function"==typeof t.then))return t;var r=Object.create(null);d.r(r);var c={};e=e||[null,a({}),a([]),a(a)];for(var b=2&f&&t;"object"==typeof b&&!~e.indexOf(b);b=a(b))Object.getOwnPropertyNames(b).forEach(e=>c[e]=()=>t[e]);return c.default=()=>t,d.d(r,c),r}})(),d.d=(e,a)=>{for(var t in a)d.o(a,t)&&!d.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},d.f={},d.e=e=>Promise.all(Object.keys(d.f).reduce((a,t)=>(d.f[t](e,a),a),[])),d.u=e=>"static/chunks/"+(({2427:"61527465",2926:"72d2e465",3755:"fcb912be",5103:"4ec056c8",5129:"a89fe872",5712:"43ad6dab",6819:"304eda7d",7638:"63c91ae9",9082:"bf5d185f"})[e]||e)+"."+({0:"700b96fc96b80936",92:"788f49cdb265a668",199:"fca450ee70f089fb",295:"0ef9b18be46f06e5",391:"71124432b84c926d",545:"e760052f10d36a9b",1160:"e199e2decb964814",1208:"0d86b06cbdf8a97e",1299:"9ae1e73c8ca9cc34",1336:"3eb7654fcfae2a58",1494:"56493f36b01d2bda",1522:"7d80e505c25fc865",1650:"78e7ab21cb8bbb48",1807:"764b0338b13846a5",2202:"59a95bacad0d8c25",2204:"93c869dacb19e384",2329:"2f32de914b104261",2427:"ae14a459dbfc381c",2449:"53d632fa8e412643",2477:"79f2f92780311a37",2555:"279979b7abe9d67f",2612:"427656205dd7d7cc",2926:"c6751b76f1fa1cb3",2938:"1555d68665415ace",2979:"d5a2293e776ec5de",3023:"8f2b6311aae80029",3047:"a49ae982be1f9fd4",3483:"cb8f5ba28c565e7f",3755:"a7d93c5f2dd72e30",3986:"a6181916d2c44515",4120:"c271dfe3e1bc082e",4267:"030877aaeac1bd71",4362:"
|
|
1
|
+
(()=>{"use strict";var e={},a={};function d(t){var f=a[t];if(void 0!==f)return f.exports;var r=a[t]={id:t,loaded:!1,exports:{}},c=!0;try{e[t].call(r.exports,r,r.exports,d),c=!1}finally{c&&delete a[t]}return r.loaded=!0,r.exports}d.m=e,(()=>{var e=[];d.O=(a,t,f,r)=>{if(t){r=r||0;for(var c=e.length;c>0&&e[c-1][2]>r;c--)e[c]=e[c-1];e[c]=[t,f,r];return}for(var b=1/0,c=0;c<e.length;c++){for(var[t,f,r]=e[c],o=!0,n=0;n<t.length;n++)(!1&r||b>=r)&&Object.keys(d.O).every(e=>d.O[e](t[n]))?t.splice(n--,1):(o=!1,r<b&&(b=r));if(o){e.splice(c--,1);var i=f();void 0!==i&&(a=i)}}return a}})(),d.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return d.d(a,{a:a}),a},(()=>{var e,a=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__;d.t=function(t,f){if(1&f&&(t=this(t)),8&f||"object"==typeof t&&t&&(4&f&&t.__esModule||16&f&&"function"==typeof t.then))return t;var r=Object.create(null);d.r(r);var c={};e=e||[null,a({}),a([]),a(a)];for(var b=2&f&&t;"object"==typeof b&&!~e.indexOf(b);b=a(b))Object.getOwnPropertyNames(b).forEach(e=>c[e]=()=>t[e]);return c.default=()=>t,d.d(r,c),r}})(),d.d=(e,a)=>{for(var t in a)d.o(a,t)&&!d.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},d.f={},d.e=e=>Promise.all(Object.keys(d.f).reduce((a,t)=>(d.f[t](e,a),a),[])),d.u=e=>"static/chunks/"+(({2427:"61527465",2926:"72d2e465",3755:"fcb912be",5103:"4ec056c8",5129:"a89fe872",5712:"43ad6dab",6819:"304eda7d",7638:"63c91ae9",9082:"bf5d185f"})[e]||e)+"."+({0:"700b96fc96b80936",92:"788f49cdb265a668",199:"fca450ee70f089fb",295:"0ef9b18be46f06e5",391:"71124432b84c926d",545:"e760052f10d36a9b",1160:"e199e2decb964814",1208:"0d86b06cbdf8a97e",1299:"9ae1e73c8ca9cc34",1336:"3eb7654fcfae2a58",1494:"56493f36b01d2bda",1522:"7d80e505c25fc865",1650:"78e7ab21cb8bbb48",1807:"764b0338b13846a5",2202:"59a95bacad0d8c25",2204:"93c869dacb19e384",2329:"2f32de914b104261",2427:"ae14a459dbfc381c",2449:"53d632fa8e412643",2477:"79f2f92780311a37",2555:"279979b7abe9d67f",2612:"427656205dd7d7cc",2926:"c6751b76f1fa1cb3",2938:"1555d68665415ace",2979:"d5a2293e776ec5de",3023:"8f2b6311aae80029",3047:"a49ae982be1f9fd4",3483:"cb8f5ba28c565e7f",3755:"a7d93c5f2dd72e30",3986:"a6181916d2c44515",4120:"c271dfe3e1bc082e",4267:"030877aaeac1bd71",4362:"115405260299210d",4613:"0985db499409cf22",4794:"ba746110cfcbf536",4808:"1cbe5b52fa01fc3d",5056:"a87b1e056ba592b1",5103:"fe288ac8bd38a73b",5129:"304b3efb308b10a1",5287:"7ea08918388fa804",5510:"f3889206b5768b33",5593:"d4c5f6a0862b4c6c",5700:"4189184227fcf178",5712:"4911ea318c123311",5741:"dd7360163a2374cf",5850:"b58c01f1e835d335",6066:"1571d105977d528c",6203:"672c38fcc2748c83",6390:"4de9361c493b8d29",6476:"c7ec7d60350855c8",6574:"30661d21e54bb004",6694:"91c424de10ee2a24",6819:"2c6ab1276b03c10a",6927:"7f60d259473bb56a",7073:"4081ed209e6748bd",7250:"a005173b9af23f14",7267:"09e9b83beadc4847",7423:"ce958b2d6a455a2f",7443:"ee6684ba397e49da",7482:"7f6f11637c008b77",7498:"621ff3bc139607ad",7573:"7f7f2a6fe7891e68",7613:"9c1657bb3b52d1b5",7638:"ed896f9d6d01dfd4",7796:"052a2a69f9bc0471",8148:"156aea8f2a9ce398",8300:"6c86a2236d42618f",8555:"2e8d51736bfefd63",8558:"baa0cf80a6964d20",8608:"dba2a172fa47a740",8680:"ed502a4a22357230",8698:"28ca7821a30dba63",8712:"e587efd8a6256aa9",8771:"53592f31c08edafa",8878:"330238d3d9f33faf",8892:"9df3095f6c52e734",9082:"735c9f3d05199f97",9104:"6d57df418831714a",9121:"574e833259fe9085",9142:"0aac6a6c8e06bcaa",9180:"3c809798364a0c62",9590:"3e1a4754a7e66e84",9599:"b2fd519c8a2951c9",9837:"92abff094b289f98",9839:"73433bbeec6e1a51",9868:"dcc9fd5f0041ffa8",9937:"cff3c28655b07c61"})[e]+".js",d.miniCssF=e=>{},d.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),d.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),(()=>{var e={},a="_N_E:";d.l=(t,f,r,c)=>{if(e[t])return void e[t].push(f);if(void 0!==r)for(var b,o,n=document.getElementsByTagName("script"),i=0;i<n.length;i++){var l=n[i];if(l.getAttribute("src")==t||l.getAttribute("data-webpack")==a+r){b=l;break}}b||(o=!0,(b=document.createElement("script")).charset="utf-8",b.timeout=120,d.nc&&b.setAttribute("nonce",d.nc),b.setAttribute("data-webpack",a+r),b.src=d.tu(t)),e[t]=[f];var u=(a,d)=>{b.onerror=b.onload=null,clearTimeout(s);var f=e[t];if(delete e[t],b.parentNode&&b.parentNode.removeChild(b),f&&f.forEach(e=>e(d)),a)return a(d)},s=setTimeout(u.bind(null,void 0,{type:"timeout",target:b}),12e4);b.onerror=u.bind(null,b.onerror),b.onload=u.bind(null,b.onload),o&&document.head.appendChild(b)}})(),d.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},d.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;d.tt=()=>(void 0===e&&(e={createScriptURL:e=>e},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("nextjs#bundler",e))),e)})(),d.tu=e=>d.tt().createScriptURL(e),d.p="/_next/",(()=>{d.b=document.baseURI||self.location.href;var e={8068:0,3266:0,8174:0,7772:0};d.f.j=(a,t)=>{var f=d.o(e,a)?e[a]:void 0;if(0!==f)if(f)t.push(f[2]);else if(/^(3266|7772|8068|8174)$/.test(a))e[a]=0;else{var r=new Promise((d,t)=>f=e[a]=[d,t]);t.push(f[2]=r);var c=d.p+d.u(a),b=Error();d.l(c,t=>{if(d.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var r=t&&("load"===t.type?"missing":t.type),c=t&&t.target&&t.target.src;b.message="Loading chunk "+a+" failed.\n("+r+": "+c+")",b.name="ChunkLoadError",b.type=r,b.request=c,f[1](b)}},"chunk-"+a,a)}},d.O.j=a=>0===e[a];var a=(a,t)=>{var f,r,[c,b,o]=t,n=0;if(c.some(a=>0!==e[a])){for(f in b)d.o(b,f)&&(d.m[f]=b[f]);if(o)var i=o(d)}for(a&&a(t);n<c.length;n++)r=c[n],d.o(e,r)&&e[r]&&e[r][0](),e[r]=0;return d.O(i)},t=self.webpackChunk_N_E=self.webpackChunk_N_E||[];t.forEach(a.bind(null,0)),t.push=a.bind(null,t.push.bind(t))})()})();
|
package/admin-out/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><!--
|
|
1
|
+
<!DOCTYPE html><!--vf5W8US5y8B2f1E6zJJw3--><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/87d4fc049f865389.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-1970c5140e4019b7.js"/><script src="/_next/static/chunks/1b86ba38-4193816aebc732b9.js" async=""></script><script src="/_next/static/chunks/9698-0157e7ac70c3f9fc.js" async=""></script><script src="/_next/static/chunks/main-app-5e981adb363c52e5.js" async=""></script><script src="/_next/static/chunks/1989-9d30cdb1a86e4beb.js" async=""></script><script src="/_next/static/chunks/app/layout-0ed29966e0abc26b.js" async=""></script><script src="/_next/static/chunks/1d2d5650-c677408087984c7a.js" async=""></script><script src="/_next/static/chunks/1607-bf227fc5e4db45b7.js" async=""></script><script src="/_next/static/chunks/app/page-c9d174a5432b0007.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-1970c5140e4019b7.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,[\"1989\",\"static/chunks/1989-9d30cdb1a86e4beb.js\",\"7177\",\"static/chunks/app/layout-0ed29966e0abc26b.js\"],\"ThemeProvider\"]\n3:I[24554,[\"1989\",\"static/chunks/1989-9d30cdb1a86e4beb.js\",\"7177\",\"static/chunks/app/layout-0ed29966e0abc26b.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[47333,[\"4560\",\"static/chunks/1d2d5650-c677408087984c7a.js\",\"1607\",\"static/chunks/1607-bf227fc5e4db45b7.js\",\"1989\",\"static/chunks/1989-9d30cdb1a86e4beb.js\",\"8974\",\"static/chunks/app/page-c9d174a5432b0007.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/87d4fc049f865389.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"vf5W8US5y8B2f1E6zJJw3\",\"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/87d4fc049f865389.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>
|
package/admin-out/index.txt
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
1:"$Sreact.fragment"
|
|
2
|
-
2:I[58321,["1989","static/chunks/1989-
|
|
3
|
-
3:I[24554,["1989","static/chunks/1989-
|
|
2
|
+
2:I[58321,["1989","static/chunks/1989-9d30cdb1a86e4beb.js","7177","static/chunks/app/layout-0ed29966e0abc26b.js"],"ThemeProvider"]
|
|
3
|
+
3:I[24554,["1989","static/chunks/1989-9d30cdb1a86e4beb.js","7177","static/chunks/app/layout-0ed29966e0abc26b.js"],"AppI18nProvider"]
|
|
4
4
|
4:I[63693,[],""]
|
|
5
5
|
5:I[74713,[],""]
|
|
6
6
|
6:I[5444,[],"ClientPageRoot"]
|
|
7
|
-
7:I[47333,["4560","static/chunks/1d2d5650-c677408087984c7a.js","1607","static/chunks/1607-bf227fc5e4db45b7.js","1989","static/chunks/1989-
|
|
7
|
+
7:I[47333,["4560","static/chunks/1d2d5650-c677408087984c7a.js","1607","static/chunks/1607-bf227fc5e4db45b7.js","1989","static/chunks/1989-9d30cdb1a86e4beb.js","8974","static/chunks/app/page-c9d174a5432b0007.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/87d4fc049f865389.css","style"]
|
|
17
17
|
:HL["/_next/static/css/659eccb5db697b76.css","style"]
|
|
18
|
-
0:{"P":null,"b":"
|
|
18
|
+
0:{"P":null,"b":"vf5W8US5y8B2f1E6zJJw3","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/87d4fc049f865389.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"}]]
|
|
@@ -148,7 +148,9 @@ A `: keepalive` comment every 15 s. A dropped client only unsubscribes, so the S
|
|
|
148
148
|
|
|
149
149
|
**URL policy** (`url-policy.ts`) — applied to push URLs at create **and** at send, to inbound `url` image parts, and to every outbound card / RPC URL:
|
|
150
150
|
- `http` / `https` only, no userinfo.
|
|
151
|
-
- `http.ts` sends through `node:http(s).request({ lookup: guardedLookup })`: every resolved IP is checked **inside** the lookup, so the
|
|
151
|
+
- `http.ts` sends direct requests through `node:http(s).request({ lookup: guardedLookup })`: every resolved IP is checked **inside** the lookup, so the check and the connect use the same resolution (no DNS-rebinding gap). An IP literal is checked up front.
|
|
152
|
+
- **Outbound proxy** (Settings → `general.outbound_proxy.*`, [requirements/settings.md](../requirements/settings.md#outbound-proxy)): only outbound remote card / RPC requests may go through it. That path runs `checkUrl` on the server's own resolution first, then the proxy resolves and connects the target — the two resolutions aren't guaranteed to agree, and a target only the proxy can resolve isn't supported. Turning the proxy on means trusting the proxy and its egress policy. The request's `timeoutMs` bounds the proxy's CONNECT too. A target in no_proxy takes the direct path above; push webhooks and inbound image downloads always do.
|
|
153
|
+
- Neither path follows redirects — a 3xx is just a non-2xx status.
|
|
152
154
|
- **Always refused**, even when listed: link-local (`169.254.0.0/16` incl. the metadata IP, `fe80::/10`), multicast, unspecified.
|
|
153
155
|
- Each IP must be public **or** inside a listed CIDR; private, loopback and CGNAT ranges are refused unless listed.
|
|
154
156
|
- https is fine anywhere allowed; plain http only when the host matches a listed host pattern or every IP is inside a listed CIDR.
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
│ /api/feishu/* (feishu descriptor) │
|
|
41
41
|
│ /api/wecom/* (wecom descriptor) │
|
|
42
42
|
│ /api/nodes/* → createNodeRoutes() (linked machines) │
|
|
43
|
-
│ /api/nodes/connect → NodeHub.handleUpgrade (halo
|
|
43
|
+
│ /api/nodes/connect → NodeHub.handleUpgrade (halo-node WS) │
|
|
44
44
|
│ /ws → setupWebSocketHandler({wss,registry}) │
|
|
45
45
|
│ /* → static frontend (Next.js out/) │
|
|
46
46
|
│ /ping, POST / (A2A) → a2a/agentcore.ts (agentcore-a2a mode) │
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Linked machines (`halo
|
|
1
|
+
# Linked machines (`halo-node`)
|
|
2
2
|
|
|
3
|
-
> User guide: [guide/node.md](../guide/node.md). Tool reference: [dev/tools.md → Node tools](../dev/tools.md#node-tools). API: [dev/api.md → Linked machines](../dev/api.md#linked-machines-halo-node). Code: `packages/server/src/nodes/` (wired in `index.ts`, `session-agent-builder.ts`),
|
|
3
|
+
> User guide: [guide/node.md](../guide/node.md). Tool reference: [dev/tools.md → Node tools](../dev/tools.md#node-tools). API: [dev/api.md → Linked machines](../dev/api.md#linked-machines-halo-node). Code: `packages/server/src/nodes/` (wired in `index.ts`, `session-agent-builder.ts`), machine client `packages/node-go/`, admin `features/nodes/`.
|
|
4
4
|
|
|
5
5
|
## Problem
|
|
6
6
|
|
|
7
|
-
The server's agents work on the server's filesystem. Users also want them to touch files on *their own* machine — a laptop or a box behind NAT the server can't reach. `halo
|
|
7
|
+
The server's agents work on the server's filesystem. Users also want them to touch files on *their own* machine — a laptop or a box behind NAT the server can't reach. `halo-node` is a small outbound client: it dials the server over WebSocket, and the workspace's full-access agents get `node_*` tools that run the local file tools (and, opt-in, `shell_exec`) **on that machine**. No inbound port, no model keys, no `halo setup` on the machine.
|
|
8
8
|
|
|
9
9
|
## Pieces
|
|
10
10
|
|
|
@@ -14,51 +14,28 @@ The server's agents work on the server's filesystem. Users also want them to tou
|
|
|
14
14
|
| Hub | `nodes/hub.ts` | Server side. `noServer` `WebSocketServer` on `/api/nodes/connect`; one live connection per account; request/response correlation, timeouts, cancel, heartbeat; `nodes:changed` broadcast. Process singleton via `setNodeHub` / `getNodeHub` |
|
|
15
15
|
| Tools | `nodes/tools.ts` | `buildNodeTools(workspaceRoot)` → the fixed 8 tools. Dispatch: hub (in-process) → `NODES_UNAVAILABLE` |
|
|
16
16
|
| Routes | `nodes/routes.ts` | Admin CRUD (`/api/nodes/accounts`) |
|
|
17
|
-
| Client | `
|
|
18
|
-
| Executor | `nodes/node-exec.ts` + `node-path.ts` | Machine side. Wraps the **same** `createWorkspaceTools(base, { accessLevel: 'full' })` the server's agents use; adds the root check |
|
|
19
|
-
| Go client | `packages/node-go/` | `halo-node`: a standalone Go binary (Linux amd64 / arm64 / armv7 and Windows amd64, no cgo) speaking the same protocol v1 with the same flags. It ports the client, the executor and the parts of `workspace-tools.ts` / `sandbox.ts` it needs (the seven ops, POSIX rm guard, process-group / Windows Job Object kill), with JS value, UTF-16 and V8 regex semantics (`internal/jsv`, `internal/jsregex` over regexp2). Ships as release assets via `.github/workflows/node-go.yml`, not npm |
|
|
20
|
-
| Conformance | `packages/server/test/node-go-conformance.test.ts` + `packages/node-go/conformance/` | The TS executor is the oracle: a real hub connects TS `runNode` (`test/helpers/ts-node-main.ts`) and the Go binary side by side, and every result, file and log line is compared. Skipped unless `HALO_NODE_GO_BIN` is set. node-go.yml runs it, along with rm-guard vector `--check` and a 20k regex differential fuzz, on any PR touching either side, so a TS change goes red until Go follows. Accepted differences: [below](#go-client-known-differences-from-the-ts-client) |
|
|
17
|
+
| Client | `packages/node-go/` | `halo-node`: the machine side, a standalone Go binary (no cgo, cross-compiled for Linux amd64 / arm64 / armv7, macOS arm64 / amd64 and Windows amd64). Connects, reconnects with backoff and runs each request through its own port of the local file tools (the seven ops, POSIX rm guard, process-group / Windows Job Object kill) with JS value, UTF-16 and V8 regex semantics (`internal/jsv`, `internal/jsregex` over regexp2), so results read like the server's own tools. Ships as release assets via `.github/workflows/node-go.yml`, not npm |
|
|
21
18
|
|
|
22
19
|
Not built in AgentCore mode (`nodeHub = AGENTCORE ? null : …`), the CLI / TUI or the evo sandbox — there the tools exist but return `NODES_UNAVAILABLE`.
|
|
23
20
|
|
|
24
|
-
###
|
|
21
|
+
### Platforms
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
CI (`node-go.yml`) runs `go vet` / `go test` natively on Linux and Windows and `go test` for Linux arm64 / armv7 under qemu (binfmt) — including `cmd/halo-node/lifecycle*_test.go`, which run the binary against a fake hub (fatal 401 / 404 / 426 / 4001, retried 409 / 429 / 4009 / other HTTP, backoff, a running call killed on 4001 / drop / SIGTERM / cancel, a TERM-ignoring command escalated to SIGKILL), and `internal/protocol`'s drift test, which reads `server/src/nodes/protocol.ts` and fails when a mirrored constant (path, headers, close codes, frame / result caps, version, ops) differs — plus the rm-guard vector `--check` (`testgen/gen-rmguard-vectors.mjs`: the vectors must still equal what the server's `sandbox.ts` `assertRmSafe` says) and a 20k-pattern regex differential fuzz against Node's `RegExp` (`testgen/regex-fuzz.mjs`). Accepted regex gaps against V8 are pinned by `known` vectors in `internal/jsregex/testdata/regex-regress.jsonl` (D11: a capture inside a quantified group isn't reset per iteration; D10: regexp2 engine limits and its 30 s per-match timeout, which aborts the grep / glob call). macOS builds are only cross-compiled — no macOS runner, not tested on a Mac. They use the POSIX code paths (process groups, rm guard); `internal/jsv`'s errno table is Linux-only, so on macOS a filesystem error reads `Unknown system error -<n>: …` instead of Node's `ENOENT: no such file or directory, …` text.
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
| D2 | A multi-byte character split across two `node_run` output chunks: TS decodes each chunk alone (two U+FFFD), Go joins them | Go is right; cases cover single-chunk output |
|
|
32
|
-
| D3 | Killed by an outside signal mid-run, TS waits 500 ms more (`shutdownWithin`), Go doesn't | timing only, same output |
|
|
33
|
-
| D4 | CLI errors start `halo-node:` instead of `halo node:` and HELP names the binary; unknown flags / positional args use Go `flag`'s wording. `--server` values WHATWG still parses but Go's `net/url` doesn't take as a host (`https:/h`, `http:h`, a leading / trailing space) are `Invalid URL` in Go; TS connects | local stderr only; these are typos of `https://h` |
|
|
34
|
-
| D5 | `node_write` content as an array with an array-like plain-object chunk (`[{length:2}]`): TS writes `0000` and succeeds, Go errors `…Received an instance of Object` | off-schema input (case `write D5 array-like object chunk`) |
|
|
35
|
-
| D6, D7 | — (removed: `edit` with `old_string: []` and array `include` in `grep` now match TS) | |
|
|
36
|
-
| D8 | `~/.halo/secrets/{config,settings}.yaml` aren't read: shell timeout only from `HALO_SHELL_TIMEOUT`, grep's 50 matches and the 5 MiB output cap fixed | matters only on a machine that also runs a halo server with those changed |
|
|
37
|
-
| D9 | `mkdir -p` where the parent exists but mkdir keeps failing ENOENT (procfs, e.g. `/proc/sys/x/y.txt`): Node retries forever and TS never replies; Go returns the ENOENT | Go is right; whole-machine writes to procfs only; no case (TS hangs) |
|
|
38
|
-
| D10 | Regex engine limits: V8 throws at `test()` (backtrack stack, `Regular expression too large`), regexp2 matches on; nested nullable quantifiers can backtrack exponentially in regexp2 where V8 prunes. Go caps group nesting at 10000 (`Invalid regular expression: /…/: Stack overflow` at compile, so grep's `Invalid regex pattern` warning; V8 compiles it and throws the same text at `test()` past ~11000 capture / lookaround levels; from 32768 capture groups (`(` × 32768) V8 already refuses at compile with `…: Too many captures` — both warn, the suffix differs). One `test()` over 30s aborts the whole grep / glob with `regex match timed out after 30s on <at> — call aborted …`, `<at>` being `<file>:<line>` for grep's line match and `<file>` for an include / glob match; V8 has no limit, so TS runs into the hub's 120s cancel and keeps spinning. Any other regexp2 match-time failure aborts the same way as `regex engine error on <at> — call aborted` | pathological patterns on both sides; the fuzz only requires Go to compile them |
|
|
39
|
-
| D11 | A capture inside a quantified group isn't reset per iteration: `/^(?:(a)\|b)+\1$/` (also `*`, `{2}`) — `"ab"` is true in V8, false in Go; `"aba"` the reverse. regexp2 keeps the last iteration's capture where ECMAScript clears it | backreference to an alternation's capture under a quantifier only; pinned by `known` vectors |
|
|
40
|
-
| D12 | A lone surrogate escape in a request's JSON (`"\ud83d"`) arrives in Go as U+FFFD (`encoding/json`), in TS as the surrogate: `edit`'s `old_string` and grep's `pattern` can then match a U+FFFD the file decoded to, where TS finds nothing. A NUL byte in a path is `EINVAL: invalid argument, stat '…'` in Go, `ERR_INVALID_ARG_VALUE` (`…without null bytes`) in TS | off-schema input; same outcome (no match / error), different text |
|
|
41
|
-
| D13 | `fs.readFile` limits by bytes, not decoded length: a file over 536870888 bytes (V8's string max) is `Invalid string length` in Go even when it is mostly multi-byte text Node could still hold; over 2 GiB both say `File size (<n>) is greater than 2 GiB`. The cap is on bytes read; decoded, invalid UTF-8 grows to at most 3× (each byte a 3-byte U+FFFD). grep's plain literal patterns search valid UTF-8 directly without widening the line. Other patterns still feed regexp2 one rune per UTF-16 unit (4 bytes each, sized to the actual unit count): a 256 MB ASCII single line still needs ≈1 GB of match input, past what an armv7 process has | ≥512 MiB of text through a node tool; byte count is a safe bound |
|
|
42
|
-
| D14 | `node_run`'s stdin is a pipe (FIFO), TS's a socketpair: reads and writes behave the same; only a test like `[ -S /dev/stdin ]` tells them apart | nothing writes to it either way |
|
|
43
|
-
| D15 | Built with `CGO_ENABLED=0`, user lookup reads `/etc/passwd` only, not NSS: for an LDAP / SSSD user the name in the `--allow-shell` log line and the home used when `HOME` is unset can differ (TS asks libc) | log text / an unset `HOME` only |
|
|
44
|
-
| D16 | A `res` goes out in 32 KiB WebSocket fragments, TS sends one frame: the server's `ws` reassembles before parsing, the 8 MiB cap is per message, and pings can interleave so a slow uplink doesn't starve the pong | wire framing only; pinned by the fragment-count proto check |
|
|
45
|
-
| D17 | Windows cancellation / timeout stops the whole Job Object. Go also observes shell exit independently of output EOF and immediately closes the job, terminating background descendants before draining stdout / stderr. TS's win32 `execAsync` kills only cmd.exe, so workers can survive or keep output pipes open. Go preserves `Command timed out after <ms>ms` and `Command failed: <command>` rather than Node's wrapped `chcp …` error text, and decodes failure output as UTF-8 / GBK too (TS only decodes successful output) | Intentional Windows lifecycle / output correctness; native cancel, timeout, shell-exit and decoder tests. The real TS fix belongs in sandbox.ts's win32 exec branch |
|
|
46
|
-
| D18 | Windows node paths using `\\?\` / `\\.\` namespace or device prefixes (including slash variants), or any component ending in a dot or space other than `.` / `..`, are refused rather than emulated. The same restrictions apply to resolved paths, including shared roots and junction targets, in folder and whole-machine modes | Ordinary Win32 opens can normalize these spellings to a different object after realpath validation. Go rejects them with a node refusal (the existing outside-root message in folder mode); TS does not. Native read/write regressions cover distinct `inside.` / `inside ` directories beside an outward `inside` junction |
|
|
47
|
-
|
|
48
|
-
Windows implementation: the shell starts suspended, is assigned to a `KILL_ON_JOB_CLOSE` job, then resumes. Cancel and timeout call `TerminateJobObject` (no graceful SIGTERM equivalent for hidden cmd.exe), retaining the shared 2 s escalation / 200 ms force-settle ceiling. The byte-capped stdout / stderr streams decode as strict UTF-8, falling back to GBK. Folder checks use case-insensitive Windows `filepath.Rel`, reject other volumes, and resolve the longest existing prefix with `GetFinalPathNameByHandle` (including junctions). A bootstrap cmd.exe gets a new hidden console, runs `chcp 65001`, then starts the command's cmd.exe so it initializes in UTF-8 even without an interactive desktop. The Unicode command passes through `HALO_NODE_COMMAND` in the child environment (delayed expansion in the bootstrap only; the inner `/d /s /c` shell inherits the machine/user `DelayedExpansion` default, like TS), not the bootstrap's legacy-codepage parser. Win32 filesystem errors map through libuv's Node-style errno names. `~/` uses `USERPROFILE`; `~\` stays literal, matching TS.
|
|
49
|
-
|
|
50
|
-
CI keeps the Linux TS-oracle conformance / rm-guard vectors unchanged and adds native Windows vet, tests and exe build. The POSIX conformance fixtures (sh commands, chmod, Unix paths and symlink assumptions) are not run on Windows; native Windows tests cover those platform-specific boundaries instead. The POSIX rm-guard tests are build-excluded there because neither client calls the guard on Windows.
|
|
25
|
+
Windows implementation: the shell starts suspended, is assigned to a `KILL_ON_JOB_CLOSE` job, then resumes. Cancel and timeout call `TerminateJobObject` (no graceful SIGTERM equivalent for hidden cmd.exe), retaining the shared 2 s escalation / 200 ms force-settle ceiling. The byte-capped stdout / stderr streams decode as strict UTF-8, falling back to GBK. Folder checks use case-insensitive Windows `filepath.Rel`, reject other volumes, and resolve the longest existing prefix with `GetFinalPathNameByHandle` (including junctions). A bootstrap cmd.exe gets a new hidden console, runs `chcp 65001`, then starts the command's cmd.exe so it initializes in UTF-8 even without an interactive desktop. The Unicode command passes through `HALO_NODE_COMMAND` in the child environment (delayed expansion in the bootstrap only; the inner `/d /s /c` shell inherits the machine/user `DelayedExpansion` default), not the bootstrap's legacy-codepage parser. Win32 filesystem errors map through libuv's Node-style errno names. `~/` uses `USERPROFILE`; `~\` stays literal.
|
|
26
|
+
|
|
27
|
+
The POSIX rm-guard tests are build-excluded on Windows: the guard isn't called there.
|
|
51
28
|
|
|
52
29
|
## Two modes
|
|
53
30
|
|
|
54
31
|
- **Folder mode** (`--root <dir>`): `root` is realpath'd at start. Every path argument and node_run's `cwd` must resolve inside it, else the call is refused before any I/O (`"<p>" is outside this machine's shared folder (<root>) — …`; machine log `[Node] refused <op> <path>: outside root`). Relative paths resolve against `root`.
|
|
55
|
-
- **Whole-machine mode** (no `--root`, or `--root /`): no prefix check — the same reach as a full-access local session, i.e. whatever the machine user can access. Relative paths and node_run's default cwd resolve against `base` = the directory `halo
|
|
32
|
+
- **Whole-machine mode** (no `--root`, or `--root /`): no prefix check — the same reach as a full-access local session, i.e. whatever the machine user can access. Relative paths and node_run's default cwd resolve against `base` = the directory `halo-node` was started in. A startup line says so. `node_list` reports the machine as `root: null, wholeMachine: true, base: "<dir>"`; the admin row shows "Whole machine".
|
|
56
33
|
|
|
57
34
|
`--allow-shell` is independent of the mode and off by default. Commands run **unsandboxed** as the machine user — in folder mode only their `cwd` is held inside `root`, the command itself can reach anything (v1 accepted trade-off; the startup warning and the guide say so). `{{ns.params.x}}` placeholders are never substituted on the machine (empty namespace set), so no settings secret reaches a command.
|
|
58
35
|
|
|
59
|
-
### Path check (`
|
|
36
|
+
### Path check (`nodepath.InRoot`)
|
|
60
37
|
|
|
61
|
-
Resolve against root, then walk up with **`lstat`** to the longest existing prefix, `realpath` it, re-append the missing tail, and test containment with `
|
|
38
|
+
Resolve against root, then walk up with **`lstat`** to the longest existing prefix, `realpath` it, re-append the missing tail, and test containment with `filepath.Rel` (so `/x/root-other` is not inside `/x/root`; drive letters compare right on Windows). `lstat`, not a following `stat`: that follows a dangling symlink and reports it missing, so `root/link → /outside/new` would stay unresolved, pass the prefix test, and the write would follow the link out of root. With `lstat` the link is found, its `realpath` fails, and the call is refused. A narrow TOCTOU window (a component swapped for a symlink between check and syscall) remains — same as the local tools.
|
|
62
39
|
|
|
63
40
|
When the checked path equals the plain resolution, the agent's own string is passed through, so results read exactly like the local tool's (`File written: notes/a.md`).
|
|
64
41
|
|
|
@@ -74,6 +51,8 @@ When the checked path equals the plain resolution, the agent's own string is pas
|
|
|
74
51
|
| `x-halo-node-info` | base64url JSON `{ root \| null, base, shell, shellTimeoutMs, os, hostname, version }` |
|
|
75
52
|
| any `--header` | passed through (auth proxies) |
|
|
76
53
|
|
|
54
|
+
Forward proxy: `--proxy <http(s)://[user:pass@]host:port>` is used alone; without it `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` apply (Go's `http.ProxyFromEnvironment`: loopback servers are never proxied). A `wss://` server goes through `CONNECT`, so the token stays inside TLS; `user:pass@` becomes `Proxy-Authorization: Basic` and is never printed (the startup line shows `Redacted()`). Redirects are never followed: a 3xx is `server refused the connection (HTTP 3xx)` and the token never goes to the `Location`.
|
|
55
|
+
|
|
77
56
|
`verifyClient` answers in this order: **429** IP locked out (`middleware/brute-force.ts`, own `node-token` bucket: 5 bad tokens in 15 min) → **426** protocol mismatch → **401** unknown token (recorded as a failure) → **404** bound workspace gone → **400** info / instance missing or malformed → **409** account already connected from a *different* instance. The path is in no auth middleware; the token is the auth.
|
|
78
57
|
|
|
79
58
|
**Frames** (JSON text, ≤ `MAX_FRAME_BYTES` = 8 MiB both ways):
|
|
@@ -87,7 +66,7 @@ node → server { t: 'res', id, ok: true, text } | { t: 'res', id, ok: false, e
|
|
|
87
66
|
|
|
88
67
|
`args` are the local tool's input minus `node` (node_run: `{ command, cwd }`). Results longer than `NODE_RESULT_MAX_CHARS` (256K) are cut on the machine with `[Content truncated on the machine: N chars total]`; the agent loop's own caps (8000 to the LLM, 64K to the UI) then apply as for any local tool. A request over 8 MiB is refused before sending.
|
|
89
68
|
|
|
90
|
-
**Close codes**: `4001` token revoked (DELETE in admin) → node exits 1. `4009` replaced: a reconnect from the **same** instance supersedes the old socket (half-open link recovery); a **different** instance is refused (`409` at handshake, or `4009 already connected from another process` if it raced) and keeps retrying with backoff — so starting a second `halo
|
|
69
|
+
**Close codes**: `4001` token revoked (DELETE in admin) → node exits 1. `4009` replaced: a reconnect from the **same** instance supersedes the old socket (half-open link recovery); a **different** instance is refused (`409` at handshake, or `4009 already connected from another process` if it raced) and keeps retrying with backoff — so starting a second `halo-node` with the same token never kicks the first. `1001` server shutting down → reconnect.
|
|
91
70
|
|
|
92
71
|
**Liveness**: server pings every 20 s and terminates after 2 missed pongs; the node's watchdog terminates after 60 s without any traffic. Either side dropping fails all in-flight calls.
|
|
93
72
|
|
|
@@ -102,13 +81,13 @@ Results are never replayed. When the link drops mid-call:
|
|
|
102
81
|
- server side: each pending call rejects with `machine "<n>" went offline during the call (connection lost). The operation may or may not have completed on the machine — check its state before retrying.`
|
|
103
82
|
- machine side: every running call is aborted (node_run's process group gets SIGTERM, SIGKILL 2 s later) and the log reads `connection lost (…) — stopped N running call(s); reconnecting in Xs`.
|
|
104
83
|
|
|
105
|
-
When `halo
|
|
84
|
+
When `halo-node` itself exits — Ctrl-C or a fatal refusal such as a revoked token (4001) — the same abort runs, but the process exits only after the aborted calls have settled, capped at 2.5 s. The SIGKILL follow-up is a timer in the node process and the command's group is detached, so exiting first would leave a TERM-ignoring command running on the machine.
|
|
106
85
|
|
|
107
86
|
Offline vs unknown are distinct errors: `machine "<n>" is offline (…). Do not retry in a loop — tell the user to start it.` vs `unknown machine "<n>" — machines linked to this workspace: a, b (see node_list)`.
|
|
108
87
|
|
|
109
88
|
## Cron runs
|
|
110
89
|
|
|
111
|
-
Cron runs execute in the server process since 2.2 ([cron.md](cron.md#execution-runjob)), so their node tools reach the hub directly like any other server-side session. The loopback relay the old `halo cli` child used (`POST /api/nodes/call`, a per-boot secret in `HALO_NODE_RELAY_URL` / `HALO_NODE_RELAY_TOKEN`) is gone. `child-env.ts` still strips `HALO_NODE_TOKEN` from every child
|
|
90
|
+
Cron runs execute in the server process since 2.2 ([cron.md](cron.md#execution-runjob)), so their node tools reach the hub directly like any other server-side session. The loopback relay the old `halo cli` child used (`POST /api/nodes/call`, a per-boot secret in `HALO_NODE_RELAY_URL` / `HALO_NODE_RELAY_TOKEN`) is gone. `child-env.ts` still strips `HALO_NODE_TOKEN` from every child the server builds, and `halo-node` drops it from its own environment at start and from every `node_run` command's — env hygiene, not a boundary: a full-level command runs unsandboxed as the same user and can read that user's processes and files anyway.
|
|
112
91
|
|
|
113
92
|
## Prompt-cache invariant
|
|
114
93
|
|
|
@@ -118,4 +97,4 @@ Cost: the 8 descriptions measure ≈ 460 tokens (Claude CountTokens); with schem
|
|
|
118
97
|
|
|
119
98
|
## Admin
|
|
120
99
|
|
|
121
|
-
`features/nodes/` adds **Channels → Linked machines** (after Web): a row per token with online dot, name, workspace, `root | Whole machine · shell on/off · os`, token copy and delete. The add dialog takes workspace + name and shows two ready-to-copy commands (token in the env, not a flag). **Linux** is `curl -fsSL <base>/install.sh | sh`, then `PATH="$HOME/.local/bin:$PATH" HALO_NODE_TOKEN=<t> halo-node --server <origin> --root <folder>` — a non-root install lands in `~/.local/bin`, which Debian / Ubuntu / Raspberry Pi OS put on `PATH` only at a login after it exists. **
|
|
100
|
+
`features/nodes/` adds **Channels → Linked machines** (after Web): a row per token with online dot, name, workspace, `root | Whole machine · shell on/off · os`, token copy and delete. The add dialog takes workspace + name and shows two ready-to-copy commands (token in the env, not a flag). **Linux / macOS** is `curl -fsSL <base>/install.sh | sh`, then `PATH="$HOME/.local/bin:$PATH" HALO_NODE_TOKEN=<t> halo-node --server <origin> --root <folder>` — a non-root install lands in `~/.local/bin`, which Debian / Ubuntu / Raspberry Pi OS put on `PATH` only at a login after it exists. **Windows** (PowerShell) downloads `<base>/halo-node-windows-amd64.exe` with `Invoke-WebRequest`, sets `$env:HALO_NODE_TOKEN` and runs `.\halo-node-windows-amd64.exe --server <origin> --root <folder>`. `install.sh` (`packages/node-go/install.sh`, POSIX sh) is uploaded per release with that release's version stamped in. It maps `uname -s` (Linux / Darwin) and `uname -m` (Darwin: `arm64` → arm64, `x86_64` → amd64), downloads the binary, checks it against `SHA256SUMS` and runs its `--version`, then installs to `/usr/local/bin` (root) or `~/.local/bin` by a rename in that directory, so a running `halo-node` is replaced without "Text file busy". No token, no service. The base comes from `/api/health`'s `version`: a plain `x.y.z` pins `releases/download/v<x.y.z>`, so the machine and the server come from the same tag; a dev build (`x.y.z-<sha>`) or a failed fetch uses `releases/latest/download`. Online state comes from the list response, then `nodes:changed` (full snapshot, global broadcast — the channels page is a global view); a WS reconnect drops the pushed snapshot and re-fetches the list. No polling.
|
|
@@ -82,7 +82,7 @@ One per turn — the root span, opened by `beginTurn` and closed by `endTurn`.
|
|
|
82
82
|
| `gen_ai.task.output` | | final assistant text |
|
|
83
83
|
| `gen_ai.system_instructions` | | system prompt, capped 8 KB — once per turn here, not on every `chat` |
|
|
84
84
|
|
|
85
|
-
On error: status `ERROR` + the error message, plus a low-cardinality `error.type` attribute (parsed `<ErrName>Error/Exception` prefix, else `_OTHER`). A turn that ends in a model refusal (`
|
|
85
|
+
On error: status `ERROR` + the error message, plus a low-cardinality `error.type` attribute (parsed `<ErrName>Error/Exception` prefix, else `_OTHER`). A turn that ends in a model refusal (stop reason `refusal` — Anthropic's refusal or an OpenAI-format provider's content-filter stop) counts as an error too: ERROR span with `error.type=_OTHER`, and `halo.turn.duration` records `outcome=error`.
|
|
86
86
|
|
|
87
87
|
### `chat <modelId>`
|
|
88
88
|
|
|
@@ -94,7 +94,7 @@ Child of `invoke_agent`, one per model call — created retroactively on the `us
|
|
|
94
94
|
| `gen_ai.request.model` | ✓ | |
|
|
95
95
|
| `gen_ai.usage.input_tokens` | ✓ | |
|
|
96
96
|
| `gen_ai.usage.output_tokens` | ✓ | |
|
|
97
|
-
| `gen_ai.response.finish_reasons` | `[<stop reason>]` — that call's own stop reason, already normalized by the provider: `end_turn`, `tool_use`, `max_tokens`, `stop_sequence` or `refusal` (a truncated or refused call is no longer exported as `end_turn`); omitted if the call reported none | |
|
|
97
|
+
| `gen_ai.response.finish_reasons` | `[<stop reason>]` — that call's own stop reason, already normalized by the provider: `end_turn`, `tool_use`, `max_tokens`, `stop_sequence` or `refusal` (a truncated or refused call is no longer exported as `end_turn`). `refusal` covers Anthropic's `stop_reason: "refusal"` and the OpenAI-format providers' content-filter stops (finish_reason `content_filter` / `sensitive`, Responses `incomplete_details.reason: "content_filter"` or a `refusal` content part); omitted if the call reported none | |
|
|
98
98
|
| `halo.ttft_ms` | streamed calls only — ms from request start to the first delta (text / thinking / tool-args), same number as the admin `ttft` badge | |
|
|
99
99
|
| `gen_ai.input.messages` | | semconv JSON (see below) — **delta only**: messages appended since this turn's previous `chat` span (first call → the user message; later calls → that cycle's `tool_result`s) |
|
|
100
100
|
| `gen_ai.output.messages` | | semconv JSON, only the trailing assistant message |
|
|
@@ -22,7 +22,7 @@ Relay rides the same "deliver a session's wrap-up to some *other* session when i
|
|
|
22
22
|
|
|
23
23
|
**Hard interrupt** adds one step: if `sendUserMessage` returned `queued` (target was busy), call `target.interruptSession(session_id)`. Order is enqueue-then-abort, same as `querySession(interrupt=true)` — the aborted turn's finally sees a non-empty queue, so it drains into the message instead of firing a relay report for the half-done turn.
|
|
24
24
|
|
|
25
|
-
**Delivery** (`deliverRelayReport(host, session)`, runs at *every* root turn end in every workspace): cheap `reply_to` column read first (no-op for the 99.9 % of sessions without one) → subtree-quiet gate → body = `finalOutput || output`, `[RELAY TARGET ABORTED …]` prefix when `turnError` is set (before the cap, so the marker survives; for an account error — 401/402/403, bad key, no balance — the closing hint says to fix the target's model configuration and not to resume it with `relay_send`, since a re-send fails the same way; for a model refusal — `turnErrorKind === 'refusal'`, the target's history tripped the safety
|
|
25
|
+
**Delivery** (`deliverRelayReport(host, session)`, runs at *every* root turn end in every workspace): cheap `reply_to` column read first (no-op for the 99.9 % of sessions without one) → subtree-quiet gate → body = `finalOutput || output`, `[RELAY TARGET ABORTED …]` prefix when `turnError` is set (before the cap, so the marker survives; for an account error — 401/402/403, bad key, no balance — the closing hint says to fix the target's model configuration and not to resume it with `relay_send`, since a re-send fails the same way; for a model refusal — `turnErrorKind === 'refusal'`, the target's history tripped the provider's safety / content filter — it says not to resume it and to `relay_send` to a NEW session id with a clean brief instead) → cap at `limits.autoReportMax` with a `relay_read(...)` pointer → header `[Relay report · workspace <ws> · session <id> · status: completed]` (`status: aborted` when `turnError` is set) → `clearReplyTo` → `registry.getOrCreate(reply_to.workspace)` → `appendUserMessage` + `sendUserMessage` on the caller. Append-then-send is the same pair the run-ledger nudge uses: `sendUserMessage` alone never writes the UI transcript, so the secretary's admin view would show a reply to nothing.
|
|
26
26
|
|
|
27
27
|
## Invariants and why
|
|
28
28
|
|
|
@@ -156,7 +156,7 @@ Hierarchical encoding: `root_id>child_segment>grandchild_segment`.
|
|
|
156
156
|
|
|
157
157
|
Both true → sets `stoppedAt` + emits `agent_done` + `querySession`s the result back to the parent. The parent's `querySession` clears its own `stoppedAt`, handles the report, and may trigger its own `tryReportToParent` — bubbling up.
|
|
158
158
|
|
|
159
|
-
**Abnormal-termination marker**: when the reported turn was killed by an unrecoverable error (retry budget exhausted / account-level failure), the session's `turnError` field holds the error text and the auto-report is prefixed with an explicit `[SUB-AGENT ABORTED: … Error: <text> … Re-dispatch with query_session(…) to resume.]` block. An **account** error (`turnErrorKind === 'account'`: 401/402/403, bad key, no balance) replaces the trailing hint with "this is a model account / credential / balance / permission problem — retrying or re-dispatching will fail the same way; fix the model configuration first (or tell the user), and do not resume it with `query_session` until then"; `RELAY TARGET ABORTED` ([relay.md](relay.md)) and goal mode's `WORKER ABORTED` get the same swap. A **model refusal** (`
|
|
159
|
+
**Abnormal-termination marker**: when the reported turn was killed by an unrecoverable error (retry budget exhausted / account-level failure), the session's `turnError` field holds the error text and the auto-report is prefixed with an explicit `[SUB-AGENT ABORTED: … Error: <text> … Re-dispatch with query_session(…) to resume.]` block. An **account** error (`turnErrorKind === 'account'`: 401/402/403, bad key, no balance) replaces the trailing hint with "this is a model account / credential / balance / permission problem — retrying or re-dispatching will fail the same way; fix the model configuration first (or tell the user), and do not resume it with `query_session` until then"; `RELAY TARGET ABORTED` ([relay.md](relay.md)) and goal mode's `WORKER ABORTED` get the same swap. A **model refusal** (stop reason `refusal` — Anthropic's or an OpenAI-format content-filter stop, see **Refusal stop** under [Resilient execution loop](#resilient-execution-loop)) also sets `turnError` (`Model declined to respond (<category>): <explanation>`) with `turnErrorKind === 'refusal'`, whose hint reads "this session's conversation history tripped the provider's safety / content filter — resuming it with `query_session` or re-sending will be declined the same way; do not resume it — start a NEW session with start_session and a clean brief instead, rewording the part that tripped the filter" (relay / goal / A2A word it for their own context). Without it the parent LLM consumed mid-turn fragments (or a literal "(no output)") as completed reports — the cron-era "sub-agent reported without finishing" incident, root-caused to a Bedrock h2 hang (`TimeoutError: http2 request did not get a response`, now also in the transient-transport retry list). Prefix, don't suppress: skipping the report would leave the parent waiting forever, the partial trace has diagnostic value, and `stoppedAt` is stamped as usual so the child never shows as falsely running. The marker is prepended *before* the truncation cap so it can never be sliced off.
|
|
160
160
|
|
|
161
161
|
The full finally-chain order is `tryReportToParent → deliverGoalRound → deliverRelayReport → releaseSession`. `deliverGoalRound` is a retained legacy hook (goal mode's round delivery): it keys off the row's `goal_session_id` and works as before for any existing goal binding. After the parent-report check, a root whose row carries a `reply_to` back-pointer (it was dispatched from another workspace via `relay_send`) gets its wrap-up appended + sent into the caller session in that workspace (same subtree-quiet gate, `reply_to` cleared before sending — see [relay.md](relay.md)), then the session is released. Each hook decides on its own db column (`parent_id` / `goal_session_id` / `reply_to`), so the three never fire for the same session.
|
|
162
162
|
|
|
@@ -402,7 +402,7 @@ Frontend network issues don't affect the backend:
|
|
|
402
402
|
| **4xx multimodal rejection** (`Multimodal data is corrupted` / `Could not process image`) | Replace all image blocks in history with text placeholders, persist, retry — see [Multimodal 4xx degrade](#multimodal-4xx-degrade) below |
|
|
403
403
|
| Unrecoverable error | Report to user, stop |
|
|
404
404
|
|
|
405
|
-
**Refusal stop.** `stop_reason: "refusal"` (Anthropic
|
|
405
|
+
**Refusal stop.** `stop_reason: "refusal"` (Anthropic-format providers) is an HTTP 200 with `stop_details {category, explanation}` — not an error, so it never enters the retry matrix above. The OpenAI-format layers map a provider's documented content-filter stop to the same `stopReason: 'refusal'`: `chatCompletionResult` (`openai-chat-format.ts`, every `chat/completions` provider) for finish_reason `content_filter` (OpenAI, DeepSeek, Doubao, Hunyuan TokenHub) or `sensitive` (Zhipu), with `category` = that raw value and no explanation; `MantleAgent` (Responses API) for `incomplete_details.reason: "content_filter"` (category `content_filter`, also with an empty `output[]`, which then skips the `MantleEmptyResponse` retry) or a `refusal` content part (category `refusal`, its text as the explanation). Any other unknown finish value stays `end_turn`. `agent-loop.ts` handles it explicitly: no assistant message is pushed, any partial `tool_use` is **not** executed, the loop ends; `session-manager.ts` emits a `system` event `⚠️ [<agent>] Model declined to respond (<category>): <explanation>` suggesting `/session new`. Previously it fell through the `end_turn` path and looked like a silent stall. It also sets the turn's `turnError` (kind `refusal`), so every abnormal-termination marker sees it — the parent's `[SUB-AGENT ABORTED …]`, `RELAY TARGET ABORTED` / `status: aborted`, goal mode's `WORKER ABORTED`, A2A `FAILED` and an AgentCore invocation's `error` — each with a "start a new session" hint instead of "resume it"; the turn sends no interim report, and a `continue_task` armed in it does not kick (see **Kick position** under [Message queue and drain](#message-queue-and-drain)). Before, a refused sub-agent reported `(no output)` (or the last narration) as a finished result, and the parent kept resuming it into the same refusal.
|
|
406
406
|
|
|
407
407
|
**Classification order: structured signal first, message text last.** The interrupt branch is decided by the attempt's own `AbortController` signal (every interrupt path we own goes through `abortReason()` on it), never by the words `cancelled` / `aborted` in the message — an upstream error body can legitimately read `"The operation was aborted due to …"` (Bedrock), and matching that swallowed a real failure as a user interrupt: silent `break`, no retry, no error event, an empty reply. Likewise the account-level branch trusts `httpStatus` when one was recovered: 401/402/403 is terminal, any *other* status is not, whatever the body says — an OpenAI-compatible 503 whose body reads `"authentication service temporarily unavailable"` used to be classified as a dead key and never retried. The keyword list is only consulted when no status could be extracted. Pinned by `turn-retry-idempotent.test.ts` (seven classification cases).
|
|
408
408
|
|
|
@@ -441,6 +441,9 @@ A value of the form `<<ENV_NAME>>` is replaced at read time with `process.env.EN
|
|
|
441
441
|
| `general.logging.level` | warn | `debug` / `info` / `warn` / `error`. Restart required |
|
|
442
442
|
| `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) |
|
|
443
443
|
| `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) |
|
|
444
|
+
| `general.outbound_proxy.enabled` | `false` | Global-only. Outbound proxy on/off: proxy env vars for spawned children, an undici `ProxyAgent` for web_fetch and A2A requests to remotes. Read live |
|
|
445
|
+
| `general.outbound_proxy.url` | `''` | Global-only. `http(s)://host:port`, no user-info (PUT / PATCH 400). Empty = off |
|
|
446
|
+
| `general.outbound_proxy.token` | `''` | **Secret**, global-only. `user:password` or a bare token; Basic auth (URL user-info for children, `Proxy-Authorization` in-process). See [requirements/settings.md](../requirements/settings.md#outbound-proxy) |
|
|
444
447
|
| `general.observability.endpoint` | `''` | OTLP base URL of an OpenTelemetry collector (e.g. `http://localhost:4318`); empty = off. Restart required (server, cron runs included); evolution runs use a new value on their next run. See [observability.md](observability.md) |
|
|
445
448
|
| `general.observability.service_name` | `halo` | OTel resource `service.name`. Restart required (server, cron included); evolution: next run |
|
|
446
449
|
| `general.observability.headers` | `''` | **Secret.** Extra OTLP request headers, comma-separated `k=v`. Restart required (server, cron included); evolution: next run |
|
|
@@ -671,7 +674,7 @@ All channel types (telegram, web, wechat, slack, feishu, wecom) share one table,
|
|
|
671
674
|
| Column | Type | Notes |
|
|
672
675
|
|---|---|---|
|
|
673
676
|
| account_id | TEXT PK | Channel-specific ID (e.g. `halo_agent_bot`, `abc-im-bot`, `e718bb7b`) |
|
|
674
|
-
| channel_type | TEXT NOT NULL | `'telegram'`, `'web'`, `'wechat'`, `'slack'`, `'feishu'`, `'wecom'`, or `'node'` (a `halo
|
|
677
|
+
| channel_type | TEXT NOT NULL | `'telegram'`, `'web'`, `'wechat'`, `'slack'`, `'feishu'`, `'wecom'`, or `'node'` (a `halo-node` pairing token) |
|
|
675
678
|
| workspace_path | TEXT NOT NULL | Absolute path of the bound workspace |
|
|
676
679
|
| label | TEXT | User-chosen name |
|
|
677
680
|
| enabled | INTEGER | 1 = active, 0 = disabled |
|
|
@@ -120,7 +120,7 @@ Sub-agent text is root-tree noise on this wire: the listener is the root's (sub
|
|
|
120
120
|
|
|
121
121
|
`chat:thinking` / `chat:stream` / `chat:followup` / `agent:tool_call` / `agent:tool_result` additionally carry `replay: true` (plus `sessionId`) when synthesized by the reattach path (never on live events) — see [Reconnect flow](#reconnect-flow) step 6.
|
|
122
122
|
|
|
123
|
-
`chat:system` producers (`session-manager.ts`'s `stop` event handling): a `max_tokens` stop emits `⚠️ [<agent>] Response truncated: output token limit reached.`; a `refusal` stop (Anthropic `stop_reason: "refusal"`, HTTP 200 — the model declined, not an error) emits `⚠️ [<agent>] Model declined to respond (<category>): <explanation> — …` suggesting `/session new` (see [session.md](session.md#resilient-execution-loop)).
|
|
123
|
+
`chat:system` producers (`session-manager.ts`'s `stop` event handling): a `max_tokens` stop emits `⚠️ [<agent>] Response truncated: output token limit reached.`; a `refusal` stop (Anthropic `stop_reason: "refusal"` or an OpenAI-format content-filter stop such as finish_reason `content_filter`, HTTP 200 — the model or the provider's filter declined, not an error) emits `⚠️ [<agent>] Model declined to respond (<category>): <explanation> — …` suggesting `/session new` (see [session.md](session.md#resilient-execution-loop)).
|
|
124
124
|
|
|
125
125
|
Server-internal flags on `AgentSessionEvent` that are **not** carried into the WS frame: `stream.final` (marks the turn's wrap-up text vs. pre-tool filler — consumed by channel responders and the cli, see [session.md](session.md#message-queue-and-drain)), `stream.streamed` / `thinking.streamed` (the text already went out as deltas — decides whether the whole event is forwarded at all), `system.outOfBand` (a `notifySystem` notice from outside the turn — the evo auto-apply line; still a plain `chat:system` here, only the cron run and `halo cli` leave it out of their printed text). The admin renders every streamed block, so none is needed on the wire.
|
|
126
126
|
|
|
@@ -149,7 +149,7 @@ Server-internal flags on `AgentSessionEvent` that are **not** carried into the W
|
|
|
149
149
|
| `skills:changed` | `ws/skills-changed.ts` `notifySkillsChanged` (REST `POST` / `DELETE /skills`, `PATCH /skills/:id/toggle`, `/skill disable` · `enable` · `delete`) · `ws/watcher-pool.ts` (the workspace watcher saw `.halo/skills/**` change) | A skill was created / deleted / toggled, or a workspace skill folder changed on disk (an agent's `file_write`, a hand edit). No payload: the admin bumps its skill bus, and the chat slash-command popup, the Skills page and the agent form's skill picker re-fetch. Scoped on the server: a workspace skill or a toggle (the disable flag lives in that workspace's db) goes via `broadcastToWorkspace`, a global skill via `broadcast`. Only real changes emit: a no-op mutation (409 / 404 / already disabled) sends nothing, and the watcher path compares a per-workspace key (folder ids + each SKILL.md's frontmatter), so body / resource edits and the echo of a mutation that already notified stay silent. The watcher debounce is 300 ms on top of the file watcher's own 300 ms flush. `~/.halo/global/skills/` has no watcher, so a hand edit there shows on the Skills page's window-focus refetch. Also bumped on WS reconnect while the Skills page is open. |
|
|
150
150
|
| `extension:changed` | `extensions/watcher.ts` `rescanAndBroadcast` (broadcast to all clients) | `~/.halo/global/extensions/` changed (install / upgrade / uninstall / on-off switch, from the admin upload, the one-click update, the `extension` skill, or a manual `mv`/`rm`; a switch rewrites `.state.json`, which trips the same watcher) — carries the **full** `ExtensionsSnapshot` (`{extensions, disabled, errors}`: switched on, switched off, refused), not a diff, and only fires when the snapshot key differs from the last one. The admin replaces its extension list wholesale (a missing `disabled`, from an older server, reads as `[]`): the preview registry re-resolves open tabs from `extensions` only (an upgraded extension remounts, or shows a banner if the tab is dirty; an uninstalled or switched-off one falls back to the "no built-in preview" page, or keeps a dirty tab alive with a banner) and the Settings → Extensions panel re-renders. Global, no workspace marker — extensions are server-wide. See [canvas-extensions.md](canvas-extensions.md). |
|
|
151
151
|
| `updates:changed` | `updates/core-check.ts` · `updates/extension-check.ts` (broadcast to all clients) | An update check finished, or (extensions scope) a watcher rescan changed a cached verdict — `{scope: 'core' \| 'extensions', status}`, `status` = the same `CoreUpdateStatus` / `ExtensionsUpdateStatus` that `GET /api/updates?scope=…` returns. Checks start only from that GET (cache ≥ 24 h old) or `POST /api/updates/check`, never on a timer. The Settings sidebar foot (`core`) and Settings → Extensions (`extensions`) replace their status with it; nothing else listens. Global, no workspace marker. See [canvas-extensions.md → Updates](canvas-extensions.md#updates-updatejson). |
|
|
152
|
-
| `nodes:changed` | `nodes/hub.ts` `announce` (broadcast to all clients) | A `halo
|
|
152
|
+
| `nodes:changed` | `nodes/hub.ts` `announce` (broadcast to all clients) | A `halo-node` connected or disconnected — carries the **full** list of online machines, `{online: [{accountId, root, shell, os, since}]}` (`root: null` = whole-machine mode), not a diff. The admin's Channels → Linked machines page replaces its online map wholesale and re-fetches the account list on WS reconnect. Global, no workspace marker (the channels page is a global view). See [node.md](node.md). |
|
|
153
153
|
|
|
154
154
|
### `error` frames: optional `code`
|
|
155
155
|
|
package/bundled-docs/dev/api.md
CHANGED
|
@@ -343,7 +343,7 @@ SSE responses (`/api/web/chat`, `/api/web/subscribe`) carry an SSE comment line
|
|
|
343
343
|
|
|
344
344
|
See [design/web.md](../design/web.md).
|
|
345
345
|
|
|
346
|
-
## Linked machines (`halo
|
|
346
|
+
## Linked machines (`halo-node`)
|
|
347
347
|
|
|
348
348
|
File: `packages/server/src/nodes/routes.ts` (+ `hub.ts` for the WebSocket). Not mounted in AgentCore mode. Design: [design/node.md](../design/node.md).
|
|
349
349
|
|
|
@@ -930,6 +930,7 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:`, `skills/<id>/confi
|
|
|
930
930
|
"description": "...",
|
|
931
931
|
"description_zh": "...",
|
|
932
932
|
"default": "...", // schema-default placeholder; omitted when the schema declares none
|
|
933
|
+
"placeholder": "...", // general.* only: example for an empty input, never a fallback value (e.g. outbound_proxy.url)
|
|
933
934
|
"secret": true, // UI masks input + value
|
|
934
935
|
"globalOnly": false, // general.* only: workspace overrides ignored, workspace writes 400
|
|
935
936
|
"restartRequired": false, // general.* only: read once at boot — UI shows a restart notice after saving
|
|
@@ -966,7 +967,7 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:`, `skills/<id>/confi
|
|
|
966
967
|
{ "ok": true }
|
|
967
968
|
```
|
|
968
969
|
|
|
969
|
-
PUT replaces the full scope; PUT / PATCH / DELETE with `scope: "workspace"` reject a `globalOnly` key with 400 (`<key> is global-only and cannot be set per workspace`) and an unresolvable `projectId` with 404 `Project not found`. DELETE takes `{scope, projectId, key}` and removes the key. PUT / PATCH / DELETE fire the server's `onSettingsChange` listeners (e.g. the sandbox hidden-paths reload); a DELETE of an already-absent key is a no-op — `{ ok: true }`, no file write, no notify. The Settings page uses DELETE for both Reset (current scope removed → falls back to lower scope / unset) and orphan Remove.
|
|
970
|
+
PUT replaces the full scope; PUT / PATCH / DELETE with `scope: "workspace"` reject a `globalOnly` key with 400 (`<key> is global-only and cannot be set per workspace`) and an unresolvable `projectId` with 404 `Project not found`. PUT / PATCH also 400 a value outside a field's `min` / `max`, and a `general.outbound_proxy.url` that isn't `http(s)://` or carries `user:pass@` (`general.outbound_proxy.url: credentials are not allowed in the URL — put them in Token (general.outbound_proxy.token)`; the message never echoes the URL), file untouched. DELETE takes `{scope, projectId, key}` and removes the key. PUT / PATCH / DELETE fire the server's `onSettingsChange` listeners (e.g. the sandbox hidden-paths reload); a DELETE of an already-absent key is a no-op — `{ ok: true }`, no file write, no notify. The Settings page uses DELETE for both Reset (current scope removed → falls back to lower scope / unset) and orphan Remove.
|
|
970
971
|
|
|
971
972
|
### POST `/api/wechat/login/start`
|
|
972
973
|
|
|
@@ -1107,6 +1108,6 @@ It does not remove session files. The full delete (rows + JSON + archive segment
|
|
|
1107
1108
|
| `listener:released` | `sessionId` | Event listener reclaimed — re-send `subscribe` (see above) |
|
|
1108
1109
|
| `exchange:deleted` | `sessionId`, `deletedIds`, `userOrdinal`, `userRows` | An `exchange:delete` this connection sent landed: ids of every soft-deleted row of the active log, the ordinal of the turn just deleted, and the log's user-turn count (without `taskId`) that ordinal runs over (the sender's own reply only — no full `state:snapshot` follows) |
|
|
1109
1110
|
| `skills:changed` | — | A skill was created / deleted / toggled, or a workspace skill folder changed on disk — re-fetch skill lists and `/api/commands`. Workspace-scoped changes reach only that workspace's sockets (see [Skills](#skills)) |
|
|
1110
|
-
| `nodes:changed` | `online: [{accountId, root \| null, shell, os, since}]` | A `halo
|
|
1111
|
+
| `nodes:changed` | `online: [{accountId, root \| null, shell, os, since}]` | A `halo-node` connected / disconnected — full snapshot of online machines (global broadcast; `root: null` = whole machine). See [Linked machines](#linked-machines-halo-node) |
|
|
1111
1112
|
|
|
1112
1113
|
Events with `taskId` set belong to sub-agent turns (nested sessions); events without are the root agent's. Session-scoped frames carry `sessionId` (the root session they belong to); the admin routes each one to the tab holding that session, and a frame without one goes to the tab on screen. Full list in [design/ws.md](../design/ws.md).
|
|
@@ -323,7 +323,7 @@ Use only for a new public release; the retained details here are not extra prepa
|
|
|
323
323
|
5. **npm**: publish from `packages/cli/dist-pub/` **once**, using the intended dist-tag (`--tag alpha` for an alpha, not `latest`). Confirm `npm view @turmind/halo@x.y.z version` and the intended tag via `npm view @turmind/halo dist-tags.alpha` (or `dist-tags.latest` for stable); both must equal the requested version. Registry replication can lag ~2–3 minutes: wait/recheck, never republish. `dist-pub/` is release-only; desktop staging uses `dist-dev/`. Historical reason: a second v1.3.4 publish put `1.3.4-<sha>` on `latest` for ~90s.
|
|
324
324
|
6. **GitHub**: `gh release create vx.y.z --notes-from-tag` (or the short CHANGELOG summary); use `--prerelease` for an alpha.
|
|
325
325
|
7. **Windows exe**: `(cd packages/desktop && HALO_STAGE_FULL=1 CI=true pnpm dist:win)`, then `gh release upload vx.y.z "packages/desktop/dist/Halo Setup x.y.z.exe"`. Verify it appears in `gh release view vx.y.z --json assets`; v1.3.0 and v1.3.1 missed this asset. macOS dmg is the user's step unless requested.
|
|
326
|
-
8. **halo-node binaries**: nothing to build by hand. Publishing the release triggers `.github/workflows/node-go.yml`'s release job (`go vet` + `go test`, then `build.sh`), which uploads `halo-node-linux-
|
|
326
|
+
8. **halo-node binaries**: nothing to build by hand. Publishing the release triggers `.github/workflows/node-go.yml`'s release job (`go vet` + `go test`, then `build.sh`), which uploads eight assets: `halo-node-linux-amd64`, `halo-node-linux-arm64`, `halo-node-linux-armv7`, `halo-node-darwin-arm64`, `halo-node-darwin-amd64`, `halo-node-windows-amd64.exe`, `SHA256SUMS` (the six binaries) and `install.sh` (with the version stamped in; it isn't listed in `SHA256SUMS`). Wait for it to go green, then check that all eight appear in `gh release view vx.y.z --json assets` — the admin's Linux / macOS connect command runs this tag's `install.sh`, the Windows one downloads this tag's exe. If the job failed, rerun it with `gh workflow run node-go.yml -f tag=vx.y.z` (`workflow_dispatch`; `--clobber` makes reruns safe). The trigger needs the release created by a person (`gh release create` as yourself), because a release created with `GITHUB_TOKEN` doesn't start other workflows.
|
|
327
327
|
|
|
328
328
|
**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.
|
|
329
329
|
|
package/bundled-docs/dev/env.md
CHANGED
|
@@ -122,7 +122,8 @@ Source: `packages/server/src/config.ts`
|
|
|
122
122
|
| `HALO_DEFAULT_PROVIDER` | (none) | — | Read by `halo setup --non-interactive`: binds every built-in agent to this provider id (and its default model) and, for each of its env-backed keys whose variable is set, writes the `<<NAME>>` placeholder into `settings.yaml` (Bedrock excepted). An unknown id only prints a warning |
|
|
123
123
|
| `HALO_CLI` | `halo` (`halo.cmd` on Windows) | — | `halo` cli executable the evolution wrapper spawns — a dev override (cron runs in-process since 2.2 and no longer uses it) |
|
|
124
124
|
| `HALO_VERSION` | `dev` | — | Version string reported by `GET /api/health` and the cli; stamped by the bundle / desktop launcher, so normally not set by hand |
|
|
125
|
-
| `HALO_NODE_TOKEN` | (none) | — | `halo
|
|
125
|
+
| `HALO_NODE_TOKEN` | (none) | — | `halo-node` only (on the linked machine): pairing token, used when `--token` isn't given — preferred over the flag, which shows up in `ps` / shell history. Stripped from child-process env (see [design/node.md](../design/node.md)) |
|
|
126
|
+
| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | (none) | — | `halo-node` only (on the linked machine): forward proxy for an `https://` / `http://` server and the hosts that bypass it (Go `ProxyFromEnvironment`; lowercase names too; `localhost` / loopback never proxied). Ignored when `--proxy <url>` is given — the flag wins (see [guide/node.md](../guide/node.md#through-a-forward-proxy)) |
|
|
126
127
|
| `HALO_PROMPT` | (none) | — | `plain` makes `halo setup` use numbered-menu prompts instead of the raw-mode TUI |
|
|
127
128
|
| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | (none) | `settings.yaml general.observability.capture_content` | `true` / `false` / `1` / `0` (any case) overrides `capture_content` either way; unset or any other value → the setting. Read at process start (see [design/observability.md](../design/observability.md)) |
|
|
128
129
|
|