grix-connector 3.19.7 → 3.20.0

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.
@@ -1,3 +1,3 @@
1
- import{accessSync as m,constants as l}from"node:fs";import{spawn as d,execSync as w}from"node:child_process";import{isAbsolute as h,join as u}from"node:path";const S=process.platform==="win32"?";":":";function P(t){try{return m(t,l.X_OK),!0}catch{return!1}}function b(t,i){const s=String(t??"").trim();if(!s||h(s))return s;const a=process.platform==="win32",c=String(i??process.env.PATH??""),e=[],n=new Set;for(const o of c.split(S)){const r=o.trim();if(!r)continue;if(a){const f=u(r,s+".cmd");n.has(f)||(n.add(f),e.push(f))}const p=u(r,s);n.has(p)||(n.add(p),e.push(p))}for(const o of e)if(P(o))return o;return console.warn(`[spawn:resolve] command="${s}" not found in PATH. candidates tried: [${e.join(", ")}] PATH=${c.substring(0,500)}${c.length>500?"...(truncated)":""}`),s}function k(t,i,s={}){const{cwd:a,env:c,...e}=s,n={stdio:e.stdio??["pipe","pipe","pipe"],env:c??{...process.env},cwd:a,windowsHide:!0,...e};if(process.platform==="win32"&&/\.(cmd|bat)$/i.test(t)){const r=process.env.ComSpec||"cmd.exe",p=`""${t}" ${i.join(" ")}"`;return{process:d(r,["/d","/s","/c",p],{...n,windowsVerbatimArguments:!0}),isCmdSpawn:!0}}return{process:d(t,i,{...n,detached:n.detached??!0}),isCmdSpawn:!1}}async function x(t,i=[]){return!t||t<=0?!1:new Promise(s=>{if(process.platform==="win32"){const e=d("wmic",["process","where",`(ParentProcessId=${t})`,"get","ProcessId"],{stdio:"pipe",timeout:3e3});let n="";e.stdout?.on("data",o=>{n+=o.toString()}),e.on("close",()=>{const o=n.split(`
2
- `).map(r=>parseInt(r.trim(),10)).filter(r=>!isNaN(r)&&r>0&&!i.includes(r));s(o.length>0)}),e.on("error",()=>s(!1));return}const a=d("pgrep",["-g",String(t)],{stdio:"pipe",timeout:3e3}),c=[];a.stdout?.on("data",e=>{e.toString().split(`
3
- `).forEach(n=>{const o=parseInt(n.trim(),10);!isNaN(o)&&o>0&&c.push(o)})}),a.on("close",()=>s(c.filter(e=>!i.includes(e)).length>0)),a.on("error",()=>s(!1))})}function y(t,i="SIGTERM"){if(t.pid){if(process.platform==="win32"){try{w(`taskkill /pid ${t.pid} /T /F`,{stdio:"pipe",timeout:5e3})}catch{t.kill(i)}return}try{process.kill(-t.pid,i)}catch{t.kill(i)}}}export{x as hasChildProcesses,y as killProcessGroup,b as resolveCommandPath,k as spawnCommand};
1
+ import{accessSync as m,constants as l}from"node:fs";import{spawn as d,execSync as w}from"node:child_process";import{isAbsolute as h,join as f}from"node:path";const P=process.platform==="win32"?";":":";function S(t){try{return m(t,l.X_OK),!0}catch{return!1}}function b(t,n){const e=String(t??"").trim();if(!e||h(e))return e;const r=process.platform==="win32",i=String(n??process.env.PATH??""),o=[],s=new Set;for(const c of i.split(P)){const p=c.trim();if(!p)continue;if(r){const u=f(p,e+".cmd");s.has(u)||(s.add(u),o.push(u))}const a=f(p,e);s.has(a)||(s.add(a),o.push(a))}for(const c of o)if(S(c))return c;return console.warn(`[spawn:resolve] command="${e}" not found in PATH. candidates tried: [${o.join(", ")}] PATH=${i.substring(0,500)}${i.length>500?"...(truncated)":""}`),e}function k(t,n,e={}){const{cwd:r,env:i,...o}=e,s={stdio:o.stdio??["pipe","pipe","pipe"],env:i??{...process.env},cwd:r,windowsHide:!0,...o};if(process.platform==="win32"&&/\.(cmd|bat)$/i.test(t)){const p=process.env.ComSpec||"cmd.exe",a=`""${t}" ${n.join(" ")}"`;return{process:d(p,["/d","/s","/c",a],{...s,windowsVerbatimArguments:!0}),isCmdSpawn:!0}}return{process:d(t,n,{...s,detached:s.detached??!0}),isCmdSpawn:!1}}async function C(t){return!t||t<=0?[]:new Promise(n=>{if(process.platform==="win32"){const i=d("wmic",["process","where",`(ParentProcessId=${t})`,"get","ProcessId"],{stdio:"pipe",timeout:3e3});let o="";i.stdout?.on("data",s=>{o+=s.toString()}),i.on("close",()=>{n(o.split(`
2
+ `).map(s=>parseInt(s.trim(),10)).filter(s=>!isNaN(s)&&s>0))}),i.on("error",()=>n([]));return}const e=d("pgrep",["-g",String(t)],{stdio:"pipe",timeout:3e3}),r=[];e.stdout?.on("data",i=>{i.toString().split(`
3
+ `).forEach(o=>{const s=parseInt(o.trim(),10);!isNaN(s)&&s>0&&r.push(s)})}),e.on("close",()=>n(r)),e.on("error",()=>n([]))})}async function g(t,n=[]){return(await C(t)).some(r=>!n.includes(r))}function v(t,n="SIGTERM"){if(t.pid){if(process.platform==="win32"){try{w(`taskkill /pid ${t.pid} /T /F`,{stdio:"pipe",timeout:5e3})}catch{t.kill(n)}return}try{process.kill(-t.pid,n)}catch{t.kill(n)}}}export{g as hasChildProcesses,v as killProcessGroup,C as listGroupPids,b as resolveCommandPath,k as spawnCommand};
@@ -1,2 +1,2 @@
1
- import{execFile as E,execFileSync as m}from"node:child_process";import{promisify as w}from"node:util";import{existsSync as a,readFileSync as p,renameSync as y,statfsSync as I,unlinkSync as N,writeFileSync as v}from"node:fs";import{join as f}from"node:path";import{GRIX_PATHS as d}from"../log/index.js";import{appendRotatingFileSync as x}from"../log/rotation.js";import{getMachineName as F}from"../util/host-info.js";import{resolveInstallId as M}from"../util/install-id.js";import{npmInstallWithMirror as k,NPM_REGISTRIES as L}from"../installer/npm-registry.js";const b=w(E);class s extends Error{code;constructor(t,r){super(r),this.name="UpgradeError",this.code=t}}function g(){return f(d.log,"upgrade.log")}function l(){return f(d.data,"upgrade-pending.json")}function C(){return a(l())}function j(){const e=l();if(!a(e))return null;try{return JSON.parse(p(e,"utf-8"))}catch{return null}}function G(e,t){const r={from_version:e,target_version:t,upgraded_at:new Date().toISOString(),crash_count:0},i=l(),n=i+".tmp";v(n,JSON.stringify(r),"utf-8"),y(n,i)}function J(){const e=l();if(a(e))try{N(e)}catch{}}function u(e){const t=`[${new Date().toISOString()}] ${e}
2
- `;try{x(g(),t)}catch{}}function K(e=4096){const t=g();if(!a(t))return"";try{const r=p(t,"utf-8");return r.length<=e?r:r.slice(-e)}catch{return""}}function h(){try{const e=process.platform==="win32";return m(e?"cmd.exe":"npm",e?["/c","npm","--version"]:["--version"],{encoding:"utf-8",timeout:1e4}).trim()}catch{throw new s("NPM_NOT_FOUND","npm is not available or timed out")}}const c=1e6;function _(){if(process.platform==="win32")return c;let e;try{e=m("npm",["prefix","-g"],{encoding:"utf-8",timeout:1e4}).trim()}catch{return c}try{const t=I(e),r=Math.floor(Number(t.bavail)*Number(t.bsize)/(1024*1024));return!Number.isFinite(r)||r<0?c:Math.min(r,c)}catch{return c}}function W(){let e;try{e=h()}catch(r){return{ok:!1,errorCode:"NPM_NOT_FOUND",errorMsg:r instanceof Error?r.message:"npm not available"}}const t=_();return t<100?{ok:!1,errorCode:"DISK_FULL",errorMsg:`Only ${t}MB free disk space (need >= 100MB)`,npmVersion:e,diskFreeMb:t}:{ok:!0,npmVersion:e,diskFreeMb:t}}function B(){let e="";try{e=h()}catch{}let t=c;try{t=_()}catch{}return{npm_version:e,node_version:process.version,disk_free_mb:t,platform:process.platform,arch:process.arch,host_name:F(),install_id:M()}}async function H(e,t,r=12e4){const i=`${e}@${t}`;u(`npm install -g ${i} starting (with mirror fallback)`);try{const{registry:n}=await k(i,r,10485760);u(`npm install succeeded via ${n}`)}catch(n){const o=n instanceof Error?n.message:String(n);throw u(`npm install failed: ${o}`),/timed out|ETIMEDOUT/i.test(o)?new s("NPM_TIMEOUT",`npm install timed out after ${r/1e3}s (tried all mirrors): ${o}`):o.includes("EACCES")||o.includes("permission denied")?new s("NPM_INSTALL_FAILED",`Permission denied: ${o}`):o.includes("ENOSPC")||o.includes("no space left")?new s("DISK_FULL",`Disk full: ${o}`):o.includes("404")||o.includes("not found")?new s("NPM_INSTALL_FAILED",`Package not found: ${o}`):new s("NPM_INSTALL_FAILED",o)}}function Y(e){let t;try{const i=process.platform==="win32";t=m(i?"cmd.exe":"npm",i?["/c","npm","root","-g"]:["root","-g"],{encoding:"utf-8",timeout:1e4}).trim()}catch{throw new s("VERSION_VERIFY_FAILED","Unable to resolve the global npm package directory")}let r;try{const i=f(t,"grix-connector","package.json"),n=JSON.parse(p(i,"utf-8"));if(n.name!=="grix-connector"||typeof n.version!="string"||!n.version.trim())throw new Error("invalid package metadata");r=n.version.trim().replace(/^v(?=\d)/,"")}catch{throw new s("VERSION_VERIFY_FAILED","Unable to read the installed grix-connector version")}if(r!==e)throw new s("VERSION_MISMATCH",`Installed version ${r} does not match expected ${e}`);return r}async function z(e,t){let r=!1;for(const i of L)try{const{stdout:n}=await b("npm",["view",`${e}@${t}`,"version","--registry",i.url,"--no-audit","--no-fund"],{timeout:8e3});if(n.trim())return!0}catch(n){const o=n,S=o.stderr||o.message||String(n);/ETARGET|No matching version|notarget|E404/i.test(S)&&(r=!0)}return!r}export{s as UpgradeError,_ as checkDiskSpace,h as checkNpmAvailable,B as collectEnvInfo,K as getUpgradeLogTail,z as isVersionAvailable,H as npmInstall,C as pendingExists,W as preflightCheck,j as readPending,J as removePending,u as upgradeLog,Y as verifyInstalledVersion,G as writePending};
1
+ import{execFile as C,execFileSync as h}from"node:child_process";import{promisify as j}from"node:util";import{existsSync as c,mkdirSync as P,mkdtempSync as G,readFileSync as k,renameSync as f,rmSync as d,statfsSync as Y,symlinkSync as J,unlinkSync as $,writeFileSync as K}from"node:fs";import{basename as x,join as l,relative as H,resolve as g,sep as D}from"node:path";import{GRIX_PATHS as F}from"../log/index.js";import{appendRotatingFileSync as W}from"../log/rotation.js";import{getMachineName as X}from"../util/host-info.js";import{resolveInstallId as z}from"../util/install-id.js";import{npmInstallWithMirror as N,NPM_REGISTRIES as q}from"../installer/npm-registry.js";const Q=j(C);class a extends Error{code;constructor(t,r){super(r),this.name="UpgradeError",this.code=t}}const M=".grix-upgrade-backup",Z=".grix-upgrade-failed";function R(){return l(F.log,"upgrade.log")}function _(){return l(F.data,"upgrade-pending.json")}function ge(){return c(_())}function S(){const e=_();if(!c(e))return null;try{return JSON.parse(k(e,"utf-8"))}catch{return null}}function me(e,t){const r={from_version:e,target_version:t,upgraded_at:new Date().toISOString(),crash_count:0},i=_();A(i,r)}function A(e,t){const r=e+".tmp";K(r,JSON.stringify(t),"utf-8"),f(r,e)}function E(e,t){const r=g(e);return g(t)===`${r}${M}`&&r.includes(`${D}node_modules${D}`)&&x(r).length>0}function ee(e,t){const r=S();!r||!E(e,t)||(r.package_dir=g(e),r.backup_dir=g(t),A(_(),r))}function he(e=S()){const t=e?.package_dir,r=e?.backup_dir;if(!(!t||!r||!E(t,r)))try{d(r,{recursive:!0,force:!0})}catch{}}function _e(e=S()){const t=e?.package_dir,r=e?.backup_dir;if(!t||!r||!E(t,r)||!c(r))return!1;const i=`${t}${Z}`;try{return d(i,{recursive:!0,force:!0}),c(t)&&f(t,i),f(r,t),d(i,{recursive:!0,force:!0}),p(`atomic rollback restored ${t}`),!0}catch(n){try{!c(t)&&c(i)&&f(i,t)}catch{}return p(`atomic rollback failed: ${n instanceof Error?n.message:String(n)}`),!1}}function ye(){const e=_();if(c(e))try{$(e)}catch{}}function p(e){const t=`[${new Date().toISOString()}] ${e}
2
+ `;try{W(R(),t)}catch{}}function ke(e=4096){const t=R();if(!c(t))return"";try{const r=k(t,"utf-8");return r.length<=e?r:r.slice(-e)}catch{return""}}function L(){try{const e=process.platform==="win32";return h(e?"cmd.exe":"npm",e?["/c","npm","--version"]:["--version"],{encoding:"utf-8",timeout:1e4}).trim()}catch{throw new a("NPM_NOT_FOUND","npm is not available or timed out")}}const m=1e6;function O(){if(process.platform==="win32")return m;let e;try{e=h("npm",["prefix","-g"],{encoding:"utf-8",timeout:1e4}).trim()}catch{return m}try{const t=Y(e),r=Math.floor(Number(t.bavail)*Number(t.bsize)/(1024*1024));return!Number.isFinite(r)||r<0?m:Math.min(r,m)}catch{return m}}function ve(){let e;try{e=L()}catch(r){return{ok:!1,errorCode:"NPM_NOT_FOUND",errorMsg:r instanceof Error?r.message:"npm not available"}}const t=O();return t<100?{ok:!1,errorCode:"DISK_FULL",errorMsg:`Only ${t}MB free disk space (need >= 100MB)`,npmVersion:e,diskFreeMb:t}:{ok:!0,npmVersion:e,diskFreeMb:t}}function Se(){let e="";try{e=L()}catch{}let t=m;try{t=O()}catch{}return{npm_version:e,node_version:process.version,disk_free_mb:t,platform:process.platform,arch:process.arch,host_name:X(),install_id:z()}}function w(e,t,r){const i=l(e,"package.json");let n;try{n=JSON.parse(k(i,"utf-8"))}catch{throw new a("VERSION_VERIFY_FAILED",`Unable to read staged package metadata: ${i}`)}if(n.name!==t||n.version?.replace(/^v(?=\d)/,"")!==r)throw new a("VERSION_MISMATCH",`Staged package ${String(n.name)}@${String(n.version)} does not match ${t}@${r}`);return n}function te(e){return typeof e.bin=="string"?[[x(e.name),e.bin]]:!e.bin||typeof e.bin!="object"?[]:Object.entries(e.bin).filter(t=>typeof t[1]=="string")}function I(e,t){const r=te(t);if(r.length===0)throw new a("VERSION_VERIFY_FAILED",`Staged package ${t.name} has no executable bin`);for(const[,i]of r){const n=l(e,i);if(!c(n))throw new a("VERSION_VERIFY_FAILED",`Staged package entrypoint is missing: ${n}`)}return r}function re(e,t,r){if(process.platform==="win32")return;const i=l(e,"bin");P(i,{recursive:!0});for(const[n,o]of r){const s=l(i,n);if(c(s))continue;const u=`${s}.grix-upgrade-${process.pid}`;try{$(u)}catch{}J(H(i,l(t,o)),u),f(u,s)}}function ne(e){const t=g(e.activePackageDir),r=g(e.stagedPackageDir),i=`${t}${M}`,n=w(r,e.packageName,e.version),o=I(r,n);c(i)&&(c(t)?d(i,{recursive:!0,force:!0}):f(i,t)),ee(t,i);let s=!1;try{c(t)&&(f(t,i),s=!0),f(r,t);const u=w(t,e.packageName,e.version),v=I(t,u);return re(e.npmPrefix,t,v),{packageDir:t,backupDir:i}}catch(u){try{c(t)&&d(t,{recursive:!0,force:!0}),s&&c(i)&&f(i,t)}catch{}throw u}}function b(e){try{return h("npm",e,{encoding:"utf-8",timeout:1e4}).trim()}catch{throw new a("NPM_NOT_FOUND",`Unable to resolve npm ${e.join(" ")}`)}}async function ie(e,t,r){const i=`${e}@${t}`,n=b(["prefix","-g"]),o=b(["root","-g"]);P(n,{recursive:!0});const s=G(l(n,".grix-upgrade-stage-"));try{p(`staging npm install ${i} under ${s}`);const{registry:u}=await N(i,r,10*1024*1024,{prefix:s}),v=b(["root","-g","--prefix",s]),y=l(v,e),U=w(y,e,t),T=I(y,U),V=l(y,T[0][1]);h(process.execPath,[V,"--version"],{encoding:"utf-8",timeout:15e3,maxBuffer:2*1024*1024});const B=ne({packageName:e,version:t,npmPrefix:n,activePackageDir:l(o,e),stagedPackageDir:y});p(`atomic package switch succeeded via ${u}; backup=${B.backupDir}`)}finally{try{d(s,{recursive:!0,force:!0})}catch{}}}async function Ee(e,t,r=12e4){const i=`${e}@${t}`;p(`npm install -g ${i} starting (atomic staging with mirror fallback)`);try{if(process.platform==="win32"){const{registry:n}=await N(i,r,10485760);p(`npm install succeeded via ${n}`)}else await ie(e,t,r)}catch(n){if(n instanceof a)throw n;const o=n instanceof Error?n.message:String(n);throw p(`npm install failed: ${o}`),/timed out|ETIMEDOUT/i.test(o)?new a("NPM_TIMEOUT",`npm install timed out after ${r/1e3}s (tried all mirrors): ${o}`):o.includes("EACCES")||o.includes("permission denied")?new a("NPM_INSTALL_FAILED",`Permission denied: ${o}`):o.includes("ENOSPC")||o.includes("no space left")?new a("DISK_FULL",`Disk full: ${o}`):o.includes("404")||o.includes("not found")?new a("NPM_INSTALL_FAILED",`Package not found: ${o}`):new a("NPM_INSTALL_FAILED",o)}}function we(e){let t;try{const i=process.platform==="win32";t=h(i?"cmd.exe":"npm",i?["/c","npm","root","-g"]:["root","-g"],{encoding:"utf-8",timeout:1e4}).trim()}catch{throw new a("VERSION_VERIFY_FAILED","Unable to resolve the global npm package directory")}let r;try{const i=l(t,"grix-connector","package.json"),n=JSON.parse(k(i,"utf-8"));if(n.name!=="grix-connector"||typeof n.version!="string"||!n.version.trim())throw new Error("invalid package metadata");r=n.version.trim().replace(/^v(?=\d)/,"")}catch{throw new a("VERSION_VERIFY_FAILED","Unable to read the installed grix-connector version")}if(r!==e)throw new a("VERSION_MISMATCH",`Installed version ${r} does not match expected ${e}`);return r}async function Ie(e,t){let r=!1;for(const i of q)try{const{stdout:n}=await Q("npm",["view",`${e}@${t}`,"version","--registry",i.url,"--no-audit","--no-fund"],{timeout:8e3});if(n.trim())return!0}catch(n){const o=n,s=o.stderr||o.message||String(n);/ETARGET|No matching version|notarget|E404/i.test(s)&&(r=!0)}return!r}export{a as UpgradeError,O as checkDiskSpace,L as checkNpmAvailable,he as cleanupPendingBackup,Se as collectEnvInfo,ne as commitStagedGlobalPackage,ke as getUpgradeLogTail,Ie as isVersionAvailable,Ee as npmInstall,ge as pendingExists,ve as preflightCheck,S as readPending,ye as removePending,_e as rollbackPendingInstall,p as upgradeLog,we as verifyInstalledVersion,me as writePending};
@@ -1 +1 @@
1
- import{existsSync as h,mkdirSync as U,readFileSync as w,renameSync as C,writeFileSync as M}from"node:fs";import{join as g,dirname as $}from"node:path";import{spawn as R}from"node:child_process";import{log as o}from"../log/index.js";import{GRIX_PATHS as m}from"../log/index.js";import{resolveClientVersion as _}from"../util/client-version.js";import{UpgradeError as L,collectEnvInfo as O,getUpgradeLogTail as D,isVersionAvailable as N,npmInstall as F,pendingExists as v,preflightCheck as H,readPending as x,removePending as u,upgradeLog as s,verifyInstalledVersion as z,writePending as B}from"./npm-upgrader.js";import{isRetriableRegistryError as G}from"../installer/npm-registry.js";const V=360*60*1e3,j=300*1e3,T=1800*1e3,K=2,X=3,S="grix-connector",k=1e4,A=15,q=2e3,J=8e3,Y=1e3;function b(l){return new URL(l.replace(/^wss:/,"https:").replace(/^ws:/,"http:")).origin}function P(){return g(m.data,"upgrade-state.json")}function E(){const l=P();if(!h(l))return{daily_attempts:{},version_attempts:{}};try{return JSON.parse(w(l,"utf-8"))}catch{return{daily_attempts:{},version_attempts:{}}}}function Z(l){const t=P();U($(t),{recursive:!0});const e=t+".tmp";M(e,JSON.stringify(l),"utf-8"),C(e,t)}function I(){return new Date().toISOString().slice(0,10)}class ot{agentConfigs;isBusy;timer=null;initialTimer=null;running=!1;stopped=!1;constructor(t,e){this.agentConfigs=t,this.isBusy=e}async start(){await this.handlePendingOnStartup(),this.initialTimer=setTimeout(()=>{this.stopped||(this.runCheck(),!this.stopped&&(this.timer=setInterval(()=>this.runCheck(),V)))},j)}stop(){this.stopped=!0,this.initialTimer&&(clearTimeout(this.initialTimer),this.initialTimer=null),this.timer&&(clearInterval(this.timer),this.timer=null)}triggerCheck(){this.stopped||this.runCheck()}async checkForUpdate(){return this.agentConfigs.length===0?{available:!1}:(await Promise.all(this.agentConfigs.map(i=>this.queryUpgrade(i)))).find(i=>i.available&&i.release)??{available:!1}}async handlePendingOnStartup(){if(!v())return;const t=x();if(!t){u();return}const e=_(),i=e===t.target_version,r=i?{from_version:t.from_version,to_version:t.target_version,status:"success"}:{from_version:t.from_version,to_version:t.target_version,status:"rolled_back",error_code:"STARTUP_CRASH",crash_count:t.crash_count};if(s(i?`daemon startup: version matches target ${t.target_version}, upgrade succeeded`:`daemon startup: version ${e} != target ${t.target_version}, rolled back`),i){const a=await this.confirmHealthz(J);s(a?"healthz confirmed healthy, reporting success":"healthz not confirmed within window; reporting success on version-match (guardian still guards crashes)")}if(await this.reportUpgrade(r)){u();return}this.retryStartupReport(r)}async retryStartupReport(t){for(let e=2;e<=A;e++){if(this.stopped)return;const i=q*(e-1)+Math.floor(Math.random()*1e3);if(await new Promise(r=>setTimeout(r,i)),this.stopped)return;if(await this.reportUpgrade(t)){u();return}}s(`startup report (${t.status}) not delivered after ${A} attempts; clearing pending to unblock future checks`),u()}async confirmHealthz(t){let e="19579";try{const a=g(m.data,"health-port");h(a)&&(e=w(a,"utf-8").trim()||e)}catch{}const i=`http://127.0.0.1:${e}/healthz`,r=Date.now()+t;for(;Date.now()<r&&!this.stopped;){try{if((await fetch(i,{signal:AbortSignal.timeout(2e3)})).ok)return!0}catch{}await new Promise(a=>setTimeout(a,Y))}return!1}async runCheck(){if(!this.running){this.running=!0;try{await this.check()}catch(t){o.error("upgrade",`Check failed: ${t instanceof Error?t.message:t}`)}finally{this.running=!1}}}async check(){if(v()||this.agentConfigs.length===0)return;const i=(await Promise.all(this.agentConfigs.map(n=>this.queryUpgrade(n)))).find(n=>n.available&&n.release)?.release;if(!i)return;const r=i.version,a=_();if(!i.force&&!this.checkRateLimit(r))return;const p=H();if(!p.ok){await this.reportUpgrade({from_version:a,to_version:r,status:"failed",error_code:p.errorCode,error_msg:p.errorMsg});return}if(!await N(S,r)){s(`target ${r} not resolvable on any registry yet; skipping this cycle (no failure recorded)`);return}s(`upgrade start: ${a} -> ${r}`);const y=Date.now();try{if(B(a,r),await F(S,r),z(r),s("npm install verified, reporting installed"),await this.reportUpgrade({from_version:a,to_version:r,status:"installed",duration_ms:Date.now()-y}),s("shutting down for restart"),!await this.waitUntilIdle()){s("upgrade aborted: checker stopped during wait");return}this.startGuardian(),process.kill(process.pid,"SIGTERM")}catch(n){const d=n instanceof L?n.code:"NPM_INSTALL_FAILED",f=n instanceof Error?n.message:String(n);s(`upgrade failed: ${d} ${f}`),u(),G(f)||d==="NPM_TIMEOUT"?s(`failure is retriable (${d}); skipping cooldown, will retry next cycle`):this.recordFailure(r),await this.reportUpgrade({from_version:a,to_version:r,status:"failed",error_code:d,error_msg:f,duration_ms:Date.now()-y,upgrade_log:D()})}}async waitUntilIdle(){if(!this.isBusy?.())return!this.stopped;s("active tasks detected, waiting until idle before restart (no max wait)");const t=5e3,e=600*1e3;let i=Date.now();for(;this.isBusy()&&!this.stopped;)await new Promise(r=>setTimeout(r,t)),Date.now()-i>=e&&(i=Date.now(),s("still waiting for active tasks to finish before restart"));return this.stopped?!1:(s("all tasks completed, proceeding with restart"),!0)}checkRateLimit(t){const e=E();if(e.last_failure_at){const c=Date.now()-new Date(e.last_failure_at).getTime();if(c<T)return o.info("upgrade",`In cooldown, ${(T-c)/6e4}m remaining`),!1}const i=e.version_attempts[t]??0;if(i>=X)return o.info("upgrade",`Version ${t} already tried ${i} times, skipping`),!1;const r=I(),a=e.daily_attempts[r]??0;return a>=K?(o.info("upgrade",`Already ${a} attempts today, skipping`),!1):!0}recordFailure(t){const e=E(),i=I();e.last_failure_at=new Date().toISOString(),e.last_failure_version=t,e.daily_attempts[i]=(e.daily_attempts[i]??0)+1,e.version_attempts[t]=(e.version_attempts[t]??0)+1,Z(e)}async queryUpgrade(t){const i=`${b(t.wsUrl)}/v1/agent-api/upgrade/check?`+new URLSearchParams({client_type:"grix-connector",client_version:_(),channel:t.channel??"stable",platform:process.platform,arch:process.arch}).toString();try{const r=await fetch(i,{headers:{Authorization:`Bearer ${t.apiKey}`},signal:AbortSignal.timeout(k)});if(!r.ok)return o.warn("upgrade",`Check API returned ${r.status}`),{available:!1};const a=await r.json();return a.code!==0?{available:!1}:a.data}catch(r){return o.warn("upgrade",`Check API error: ${r instanceof Error?r.message:r}`),{available:!1}}}async reportUpgrade(t){if(this.agentConfigs.length===0)return!1;const e={client_type:"grix-connector",...t.npm_version?t:{...t,...O()}};return(await Promise.all(this.agentConfigs.map(async r=>{const c=`${b(r.wsUrl)}/v1/agent-api/upgrade/report`;try{return(await fetch(c,{method:"POST",headers:{Authorization:`Bearer ${r.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(e),signal:AbortSignal.timeout(k)})).ok}catch{return!1}}))).some(r=>r)}startGuardian(){const t=g(m.base,"bin","upgrade-guardian.sh");if(process.platform==="win32"||!h(t)){o.info("upgrade","Guardian not available on this platform, skipping");return}try{const e=R(t,[],{detached:!0,stdio:"ignore"});e.unref(),s(`guardian started (pid ${e.pid})`)}catch(e){o.warn("upgrade",`Failed to start guardian: ${e instanceof Error?e.message:e}`)}}}export{ot as UpgradeChecker};
1
+ import{existsSync as h,mkdirSync as C,readFileSync as v,renameSync as M,writeFileSync as $}from"node:fs";import{join as g,dirname as R}from"node:path";import{spawn as L}from"node:child_process";import{log as o}from"../log/index.js";import{GRIX_PATHS as m}from"../log/index.js";import{resolveClientVersion as _}from"../util/client-version.js";import{UpgradeError as O,cleanupPendingBackup as y,collectEnvInfo as D,getUpgradeLogTail as N,isVersionAvailable as F,npmInstall as H,pendingExists as S,preflightCheck as x,readPending as B,removePending as u,rollbackPendingInstall as z,upgradeLog as s,verifyInstalledVersion as G,writePending as V}from"./npm-upgrader.js";import{isRetriableRegistryError as j}from"../installer/npm-registry.js";const K=360*60*1e3,X=300*1e3,T=1800*1e3,q=2,J=3,k="grix-connector",b=1e4,A=15,Y=2e3,Z=8e3,W=1e3;function P(l){return new URL(l.replace(/^wss:/,"https:").replace(/^ws:/,"http:")).origin}function E(){return g(m.data,"upgrade-state.json")}function I(){const l=E();if(!h(l))return{daily_attempts:{},version_attempts:{}};try{return JSON.parse(v(l,"utf-8"))}catch{return{daily_attempts:{},version_attempts:{}}}}function Q(l){const t=E();C(R(t),{recursive:!0});const e=t+".tmp";$(e,JSON.stringify(l),"utf-8"),M(e,t)}function U(){return new Date().toISOString().slice(0,10)}class ct{agentConfigs;isBusy;timer=null;initialTimer=null;running=!1;stopped=!1;constructor(t,e){this.agentConfigs=t,this.isBusy=e}async start(){await this.handlePendingOnStartup(),this.initialTimer=setTimeout(()=>{this.stopped||(this.runCheck(),!this.stopped&&(this.timer=setInterval(()=>this.runCheck(),K)))},X)}stop(){this.stopped=!0,this.initialTimer&&(clearTimeout(this.initialTimer),this.initialTimer=null),this.timer&&(clearInterval(this.timer),this.timer=null)}triggerCheck(){this.stopped||this.runCheck()}async checkForUpdate(){return this.agentConfigs.length===0?{available:!1}:(await Promise.all(this.agentConfigs.map(a=>this.queryUpgrade(a)))).find(a=>a.available&&a.release)??{available:!1}}async handlePendingOnStartup(){if(!S())return;const t=B();if(!t){u();return}const e=_(),a=e===t.target_version,r=a?{from_version:t.from_version,to_version:t.target_version,status:"success"}:{from_version:t.from_version,to_version:t.target_version,status:"rolled_back",error_code:"STARTUP_CRASH",crash_count:t.crash_count};if(s(a?`daemon startup: version matches target ${t.target_version}, upgrade succeeded`:`daemon startup: version ${e} != target ${t.target_version}, rolled back`),a){const i=await this.confirmHealthz(Z);s(i?"healthz confirmed healthy, reporting success":"healthz not confirmed within window; reporting success on version-match (guardian still guards crashes)")}if(await this.reportUpgrade(r)){a&&y(t),u();return}this.retryStartupReport(r,a?t:null)}async retryStartupReport(t,e){for(let a=2;a<=A;a++){if(this.stopped)return;const r=Y*(a-1)+Math.floor(Math.random()*1e3);if(await new Promise(i=>setTimeout(i,r)),this.stopped)return;if(await this.reportUpgrade(t)){y(e),u();return}}s(`startup report (${t.status}) not delivered after ${A} attempts; clearing pending to unblock future checks`),y(e),u()}async confirmHealthz(t){let e="19579";try{const i=g(m.data,"health-port");h(i)&&(e=v(i,"utf-8").trim()||e)}catch{}const a=`http://127.0.0.1:${e}/healthz`,r=Date.now()+t;for(;Date.now()<r&&!this.stopped;){try{if((await fetch(a,{signal:AbortSignal.timeout(2e3)})).ok)return!0}catch{}await new Promise(i=>setTimeout(i,W))}return!1}async runCheck(){if(!this.running){this.running=!0;try{await this.check()}catch(t){o.error("upgrade",`Check failed: ${t instanceof Error?t.message:t}`)}finally{this.running=!1}}}async check(){if(S()||this.agentConfigs.length===0)return;const a=(await Promise.all(this.agentConfigs.map(n=>this.queryUpgrade(n)))).find(n=>n.available&&n.release)?.release;if(!a)return;const r=a.version,i=_();if(!a.force&&!this.checkRateLimit(r))return;const p=x();if(!p.ok){await this.reportUpgrade({from_version:i,to_version:r,status:"failed",error_code:p.errorCode,error_msg:p.errorMsg});return}if(!await F(k,r)){s(`target ${r} not resolvable on any registry yet; skipping this cycle (no failure recorded)`);return}s(`upgrade start: ${i} -> ${r}`);const w=Date.now();try{if(V(i,r),await H(k,r),G(r),s("npm install verified, reporting installed"),await this.reportUpgrade({from_version:i,to_version:r,status:"installed",duration_ms:Date.now()-w}),s("shutting down for restart"),!await this.waitUntilIdle()){s("upgrade aborted: checker stopped during wait");return}this.startGuardian(),process.kill(process.pid,"SIGTERM")}catch(n){const d=n instanceof O?n.code:"NPM_INSTALL_FAILED",f=n instanceof Error?n.message:String(n);s(`upgrade failed: ${d} ${f}`),z()&&s("upgrade failed before restart; previous package restored atomically"),u(),j(f)||d==="NPM_TIMEOUT"?s(`failure is retriable (${d}); skipping cooldown, will retry next cycle`):this.recordFailure(r),await this.reportUpgrade({from_version:i,to_version:r,status:"failed",error_code:d,error_msg:f,duration_ms:Date.now()-w,upgrade_log:N()})}}async waitUntilIdle(){if(!this.isBusy?.())return!this.stopped;s("active tasks detected, waiting until idle before restart (no max wait)");const t=5e3,e=600*1e3;let a=Date.now();for(;this.isBusy()&&!this.stopped;)await new Promise(r=>setTimeout(r,t)),Date.now()-a>=e&&(a=Date.now(),s("still waiting for active tasks to finish before restart"));return this.stopped?!1:(s("all tasks completed, proceeding with restart"),!0)}checkRateLimit(t){const e=I();if(e.last_failure_at){const c=Date.now()-new Date(e.last_failure_at).getTime();if(c<T)return o.info("upgrade",`In cooldown, ${(T-c)/6e4}m remaining`),!1}const a=e.version_attempts[t]??0;if(a>=J)return o.info("upgrade",`Version ${t} already tried ${a} times, skipping`),!1;const r=U(),i=e.daily_attempts[r]??0;return i>=q?(o.info("upgrade",`Already ${i} attempts today, skipping`),!1):!0}recordFailure(t){const e=I(),a=U();e.last_failure_at=new Date().toISOString(),e.last_failure_version=t,e.daily_attempts[a]=(e.daily_attempts[a]??0)+1,e.version_attempts[t]=(e.version_attempts[t]??0)+1,Q(e)}async queryUpgrade(t){const a=`${P(t.wsUrl)}/v1/agent-api/upgrade/check?`+new URLSearchParams({client_type:"grix-connector",client_version:_(),channel:t.channel??"stable",platform:process.platform,arch:process.arch}).toString();try{const r=await fetch(a,{headers:{Authorization:`Bearer ${t.apiKey}`},signal:AbortSignal.timeout(b)});if(!r.ok)return o.warn("upgrade",`Check API returned ${r.status}`),{available:!1};const i=await r.json();return i.code!==0?{available:!1}:i.data}catch(r){return o.warn("upgrade",`Check API error: ${r instanceof Error?r.message:r}`),{available:!1}}}async reportUpgrade(t){if(this.agentConfigs.length===0)return!1;const e={client_type:"grix-connector",...t.npm_version?t:{...t,...D()}};return(await Promise.all(this.agentConfigs.map(async r=>{const c=`${P(r.wsUrl)}/v1/agent-api/upgrade/report`;try{return(await fetch(c,{method:"POST",headers:{Authorization:`Bearer ${r.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(e),signal:AbortSignal.timeout(b)})).ok}catch{return!1}}))).some(r=>r)}startGuardian(){const t=g(m.base,"bin","upgrade-guardian.sh");if(process.platform==="win32"||!h(t)){o.info("upgrade","Guardian not available on this platform, skipping");return}try{const e=L(t,[],{detached:!0,stdio:"ignore"});e.unref(),s(`guardian started (pid ${e.pid})`)}catch(e){o.warn("upgrade",`Failed to start guardian: ${e instanceof Error?e.message:e}`)}}}export{ct as UpgradeChecker};
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: grix-widget-visitor-ban
3
+ description: Ban a widget visitor session with the typed `grix_widget_visitor_ban` tool — blocks the visitor's chat session and automatically adds the session's most recent init IP to the owner's global IP ban list (7-day expiry by default). Trigger when the owner asks to ban, block, or kick an abusive/spamming widget visitor.
4
+ trigger: 当用户要封禁、拉黑或踢掉某个骚扰/滥用的 widget 访客会话时
5
+ ---
6
+
7
+ # Grix Widget Visitor Ban
8
+
9
+ Banning a widget visitor is a server-side enforced moderation action:
10
+
11
+ - **Permission is granted per agent by the owner.** The platform checks that the
12
+ calling agent has the "ban widget visitor" permission; the agent is only the
13
+ caller. If the owner has not granted it, the call is rejected — nothing the
14
+ agent says or retries can bypass that.
15
+ - **The ban is twofold.** The visitor's session is banned, and the session's
16
+ most recent init IP is automatically added to the owner's global IP ban list
17
+ (expires in 7 days by default).
18
+
19
+ ## Tool contract
20
+
21
+ Call `grix_widget_visitor_ban` with:
22
+
23
+ - `session_id` (required) — the widget visitor session ID to ban. It must be a
24
+ widget visitor session owned by your owner, not a regular user/agent session.
25
+
26
+ On success the result carries the banned `session_id`, the resolved
27
+ `visitor_key`, and the new session `status`.
28
+
29
+ ## Rules
30
+
31
+ 1. Banning is an irreversible moderation action (the IP ban outlives the
32
+ session) — confirm with the owner before calling, and pass exactly the
33
+ `session_id` they mean.
34
+ 2. `session_id` must be a widget visitor session; do not pass regular chat
35
+ session IDs.
36
+ 3. If the call fails with a permission error, the owner has not granted this
37
+ agent the "ban widget visitor" permission — tell the owner to enable it in
38
+ the agent's permission settings in the App. Do not retry; it will keep
39
+ failing.
40
+ 4. If the call fails with a not-found error, the session is invalid or does not
41
+ belong to your owner — report that instead of guessing another ID.
42
+ 5. On any failure, report the exact reason to the owner instead of silently
43
+ retrying.
package/dist/grix.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import k from"node:path";import{writeFileSync as Y}from"node:fs";import{Manager as me}from"./manager.js";import{ensureGrixDirs as he,initLogger as ye,log as s,installProcessLogRotation as we,setConsoleOutput as q}from"./core/log/index.js";import{HealthServer as Se,bindPortOrFail as z}from"./core/runtime/index.js";import{writePidFile as Re,removePidFile as C,readDaemonPid as Ae}from"./core/runtime/index.js";import{resolveRuntimePaths as H}from"./core/config/index.js";import{supportsNonMitmRelayConfig as B}from"./core/config/provider-env.js";import{ServiceManager as xe}from"./service/service-manager.js";import{killProcessesByCommandLine as Ee,isWindowsElevated as ve}from"./service/process-control.js";import{buildServiceID as be}from"./service/service-paths.js";import{acquireDaemonLock as De,isLockHolderSameProcess as Pe,readDaemonLock as M,releaseDaemonLock as T}from"./runtime/daemon-lock.js";import{writeDaemonStatus as _,removeDaemonStatus as Ie}from"./runtime/service-state.js";import{AdminServer as ke,generateToken as Ce,writeTokenFile as Te}from"./core/admin/index.js";import{initSentry as Ue,closeSentry as G,reportFatal as N}from"./core/observability/sentry.js";import{initProxyManager as $e,getProxyManager as m,relayHostsForClientType as Q}from"./core/proxy/index.js";import{stopProxyIfNobodyNeedsIt as _e,disableAllRelayAndRestart as Fe,setOneAgentRelayAndRestart as Z}from"./core/proxy/relay-orchestration.js";import{setAgentRelayCredential as ee,RelayCredentialError as te}from"./core/proxy/relay-credential.js";import{RelayFetchError as re,RelayToggleCoordinator as Le}from"./core/proxy/relay-credential-fetch.js";import{applyHermesProfileRelay as Oe,clearHermesProfileRelay as He,discoverHermesProfiles as Be}from"./core/proxy/hermes-profile-relay.js";import{disableRelayCredential as Me,enableRelayCredential as Ge,RelayRouteError as oe}from"./core/proxy/relay-credential-router.js";import{resolveClientVersion as Ne}from"./core/util/client-version.js";import{parseCliArgs as je}from"./core/util/cli-args.js";const{command:x,flags:S,unknownFlags:j}=je(process.argv.slice(2));if(j.length>0&&(console.error(`Unknown option${j.length>1?"s":""}: ${j.map(o=>`--${o}`).join(", ")}
3
- Run \`grix-connector --help\` to see the supported commands and options.`),process.exit(1)),S.version&&(console.log(Ne()),process.exit(0)),S.help&&(console.log(`grix-connector \u2014 Unified AI Agent Bridge
2
+ import I from"node:path";import{writeFileSync as ee}from"node:fs";import{Manager as Ae}from"./manager.js";import{ensureGrixDirs as xe,initLogger as Ee,log as s,installProcessLogRotation as be,setConsoleOutput as te}from"./core/log/index.js";import{HealthServer as De,bindPortOrFail as re}from"./core/runtime/index.js";import{writePidFile as ve,removePidFile as F,readDaemonPid as Pe}from"./core/runtime/index.js";import{resolveRuntimePaths as G}from"./core/config/index.js";import{supportsNonMitmRelayConfig as Ie}from"./core/config/provider-env.js";import{ServiceManager as ke}from"./service/service-manager.js";import{killProcessesByCommandLine as Ce,isWindowsElevated as Te}from"./service/process-control.js";import{buildServiceID as Ue}from"./service/service-paths.js";import{acquireDaemonLock as _e,isLockHolderSameProcess as $e,readDaemonLock as j,releaseDaemonLock as L}from"./runtime/daemon-lock.js";import{writeDaemonStatus as H,removeDaemonStatus as Fe}from"./runtime/service-state.js";import{AdminServer as Le,generateToken as Oe,writeTokenFile as He}from"./core/admin/index.js";import{initSentry as Be,closeSentry as K,reportFatal as V}from"./core/observability/sentry.js";import{initProxyManager as Me,getProxyManager as f,relayHostsForClientType as ne,RelayStateStore as Ne,SecretStore as Ge}from"./core/proxy/index.js";import{stopProxyIfNobodyNeedsIt as oe,switchAgentRelayMode as W,reconcileRelayStates as ae}from"./core/proxy/relay-orchestration.js";import{setAgentRelayCredential as se,RelayCredentialError as ie}from"./core/proxy/relay-credential.js";import{RelayFetchError as le,RelayToggleCoordinator as je}from"./core/proxy/relay-credential-fetch.js";import{applyHermesProfileRelay as Ke,clearHermesProfileRelay as Ve,discoverHermesProfiles as We}from"./core/proxy/hermes-profile-relay.js";import{disableRelayCredential as Xe,enableRelayCredential as Je,RelayRouteError as ce}from"./core/proxy/relay-credential-router.js";import{resolveClientVersion as Ye}from"./core/util/client-version.js";import{parseCliArgs as qe}from"./core/util/cli-args.js";const{command:b,flags:S,unknownFlags:X}=qe(process.argv.slice(2));if(X.length>0&&(console.error(`Unknown option${X.length>1?"s":""}: ${X.map(o=>`--${o}`).join(", ")}
3
+ Run \`grix-connector --help\` to see the supported commands and options.`),process.exit(1)),S.version&&(console.log(Ye()),process.exit(0)),S.help&&(console.log(`grix-connector \u2014 Unified AI Agent Bridge
4
4
 
5
5
  Usage: grix-connector <command> [options]
6
6
 
@@ -34,9 +34,9 @@ Examples:
34
34
  grix-connector start # Start as system service
35
35
  grix-connector status # Check service status
36
36
  grix-connector restart # Restart the service
37
- `),process.exit(0)),x==="reload"){const o=Ae();o||(console.error("reload failed: daemon is not running (no pid file)"),process.exit(1));try{process.kill(o,0)}catch{console.error(`reload failed: daemon process ${o} is not running (stale pid file)`),process.exit(1)}{const c=M(H().daemonLockFile);c&&c.pid===o&&!Pe(c)&&(console.error(`reload failed: pid ${o} has been reused by another process (stale pid file)`),process.exit(1))}try{process.kill(o,"SIGHUP"),console.log(JSON.stringify({ok:!0,signaled:o},null,2)),process.exit(0)}catch(c){console.error(`reload failed: ${c instanceof Error?c.message:c}`),process.exit(1)}}const ne=["start","stop","restart","status"];if(x&&ne.includes(x)){process.platform==="win32"&&["start","stop","restart"].includes(x)&&!ve()&&console.warn(`Warning: Not running as administrator. Task Scheduler registration is skipped;
37
+ `),process.exit(0)),b==="reload"){const o=Pe();o||(console.error("reload failed: daemon is not running (no pid file)"),process.exit(1));try{process.kill(o,0)}catch{console.error(`reload failed: daemon process ${o} is not running (stale pid file)`),process.exit(1)}{const d=j(G().daemonLockFile);d&&d.pid===o&&!$e(d)&&(console.error(`reload failed: pid ${o} has been reused by another process (stale pid file)`),process.exit(1))}try{process.kill(o,"SIGHUP"),console.log(JSON.stringify({ok:!0,signaled:o},null,2)),process.exit(0)}catch(d){console.error(`reload failed: ${d instanceof Error?d.message:d}`),process.exit(1)}}const de=["start","stop","restart","status"];if(b&&de.includes(b)){process.platform==="win32"&&["start","stop","restart"].includes(b)&&!Te()&&console.warn(`Warning: Not running as administrator. Task Scheduler registration is skipped;
38
38
  using Startup folder auto-start instead. For full Task Scheduler integration,
39
- right-click the terminal and select "Run as administrator".`);const o=H(),c=S["config-dir"]??(S.profile?k.join(o.configDir,S.profile):void 0),h=k.resolve(process.argv[1]||`${o.rootDir}/dist/grix.js`),p=new xe({cliPath:h,nodePath:process.execPath});try{let u;switch(x){case"start":(await p.status({rootDir:o.rootDir})).installed?u=await p.start({rootDir:o.rootDir}):u=await p.install({rootDir:o.rootDir,configDir:c});break;case"stop":u=await p.stop({rootDir:o.rootDir});break;case"restart":(await p.status({rootDir:o.rootDir})).installed?u=await p.restart({rootDir:o.rootDir}):u=await p.install({rootDir:o.rootDir,configDir:c});break;case"status":u=await p.status({rootDir:o.rootDir});break}console.log(JSON.stringify(u,null,2)),process.exit(0)}catch(u){console.error(`${x} failed: ${u instanceof Error?u.message:u}`),process.exit(1)}}else x&&(console.error(`Unknown command: ${x}
40
- Valid commands: ${ne.join(", ")}`),process.exit(1));const l=H(),Ke=S["config-dir"]??(S.profile?`${l.configDir}/${S.profile}`:void 0),r=new me,F=new Se,ae=Ce(),E=new ke(ae);let K=!1,D;async function se(o){process.stderr.write(o.message+`
41
- `),s.error("main",o.message.replace(/\n/g," \u2014 "));const c=M(l.daemonLockFile);c?.pid===process.pid&&c.instance_id===D&&await _(l.daemonStatusFile,{state:"failed",pid:process.pid,instance_id:D,updated_at:Date.now(),reason:`port_bind_${o.kind}:${o.label}:${o.port}`}).catch(()=>{}),await G(),T(l.daemonLockFile).catch(()=>{}),C(),process.exit(1)}async function L(o){if(K)return;K=!0,s.info("main",`Received ${o}, shutting down...`),F.markShuttingDown();const c=setTimeout(()=>{s.error("main","Shutdown timed out, forcing exit"),T(l.daemonLockFile).catch(()=>{}),C(),process.exit(2)},1e4);try{await r.stop(),await m()?.stop().catch(()=>{}),await E.stop(),await F.stop(),await G(),await T(l.daemonLockFile),await Ie(l.daemonStatusFile).catch(()=>{}),clearTimeout(c),C(),s.info("main","Shutdown complete"),process.exit(0)}catch(h){s.error("main",`Shutdown error: ${h}`),T(l.daemonLockFile).catch(()=>{}),C(),process.exit(2)}}async function Ve(){if(he(),ye(),we(l.stdoutLogFile,l.stderrLogFile),q(!1),await Ue(),process.platform==="win32"){const e=be(l.rootDir,"win32");await Ee(e,{platform:"win32"})}try{D=(await De(l.daemonLockFile,l.rootDir)).instance_id}catch(e){console.error(e instanceof Error?e.message:e),process.exit(1)}Re(),s.info("main",`grix-connector starting (PID ${process.pid})`),await _(l.daemonStatusFile,{state:"starting",pid:process.pid,instance_id:D,updated_at:Date.now()});const o=parseInt(S["health-port"]??process.env.GRIX_HEALTH_PORT??"19579",10);{const e=await z({label:"health",port:o,envVar:"GRIX_HEALTH_PORT",cliFlag:"health-port",start:n=>F.start(n)});e&&await se(e)}const c=k.join(l.dataDir,"health-port");Y(c,String(o),"utf-8"),process.on("SIGINT",()=>L("SIGINT")),process.on("SIGTERM",()=>L("SIGTERM")),process.on("SIGHUP",()=>{K||(s.info("main","Received SIGHUP, reloading config..."),r.reload().then(e=>s.info("main",`reload done: ${JSON.stringify(e)}`)).catch(e=>s.error("main",`reload failed: ${e instanceof Error?e.message:e}`)))});let h="",p=0,u;process.on("uncaughtException",e=>{const n=e instanceof Error?e.stack??e.message:String(e);n===h?(p++,(p<=3||p%100===0)&&s.error("main",`Uncaught exception (x${p}): ${n}`)):(p>3&&s.error("main",`Previous exception repeated ${p} times total`),h=n,p=1,s.error("main",`Uncaught exception: ${e instanceof Error?e.stack:e}`),u||(u=setTimeout(()=>{p>3&&s.error("main",`Previous exception repeated ${p} times total`),h="",p=0,u=void 0},1e4).unref())),!ie(e)&&(N(e,"uncaughtException"),L("uncaughtException"))}),process.on("unhandledRejection",e=>{s.error("main",`Unhandled rejection: ${e}`),!ie(e)&&(N(e,"unhandledRejection"),L("unhandledRejection"))});const b=$e(l.dataDir);await b.clearRuntimeState();const P=b.getRelayAgents();if(P.length>0)try{await b.startIfAnyRelayAgent(),s.info("main",`mitm proxy started for relay agents: ${P.join(", ")} (per-agent; others stay direct)`)}catch(e){s.error("main",`mitm proxy FAILED to start (relay agents: ${P.join(", ")}): ${e}. These agents will REFUSE to start (they must not silently fall back to your own account). Fix the proxy or turn relay off for them. Other agents are unaffected.`)}else s.info("main",'mitm proxy idle (no agent has Grix relay enabled; enable: PUT /api/proxy/agents/<name>/enabled {"enabled":true})');let I=null;if(m()){const e=async()=>{const t=m();t&&await _e(t,r)&&s.info("main","no agent uses Grix relay anymore; mitm proxy stopped")};r.setRelayEnvSettledHandler(()=>{e().catch(t=>{s.warn("main",`failed to stop idle mitm proxy: ${t}`)})});const n=()=>{const t=m(),a=t.getRelayAgents(),i=t.getDegradedRelayAgents();return{enabled:a.length>0,relayAgents:a,staleRelayAgents:r.getAgentsWithStaleRelayEnv(),degradedRelayAgents:i,runtime:t.getRuntimeInfo(),config:t.getConfigSnapshot()}},d=new Le,R={status:()=>n(),setAgentRelay:async(t,a,i)=>{const f=m(),w={info:y=>s.info("main",y),warn:y=>s.warn("main",y)},A=y=>r.getAgentsStatus().find(v=>v.name===y),g=A(t);if(!a){d.cancel(t);const y=await d.runExclusive(t,async()=>{if(g&&B(g.clientType))return await r.setAgentProvider(t,void 0),{...n(),restarted:!0,busy:!1};const{restarted:v,busy:O}=await Z(f,r,w,{agentName:t,enabled:!1});return{...n(),restarted:v,busy:O}});return r.reportRelayStateLocalChange(t),y}if(!g)throw Object.assign(new Error(`Agent "${t}" not found`),{code:"NOT_FOUND"});if(!B(g.clientType)&&Q(g.clientType).length===0)throw Object.assign(new Error(`client type "${g.clientType??""}" does not support Grix relay`),{code:"UNSUPPORTED_CLIENT_TYPE"});const de=await d.runExclusive(t,async()=>{const y=d.epoch(t),v=A(t);if(!v)throw Object.assign(new Error(`Agent "${t}" not found`),{code:"NOT_FOUND"});const O=B(v.clientType),X=String(v.agentId??"").trim();if(!X)throw new te(`local agent "${t}" has no agent_id registered`,"MISSING_AGENT_ID");const J=d.waitCancelled(t,y);let $;try{$=await Promise.race([r.fetchRelayCredential(t,{model:i}),J.promise.then(()=>{throw new re("relay enable cancelled by a later disable","CANCELLED")})])}finally{J.dispose()}d.assertCurrent(t,y);const{restarted:pe,busy:ge}=await ee(f,r,A,w,{localName:t,agentId:X,virtualKey:$.apiKey,anthropicBaseUrl:$.anthropicBaseUrl,openaiBaseUrl:$.openaiBaseUrl,model:i},(ue,fe)=>r.setAgentProvider(ue,fe));if(d.epoch(t)!==y)throw O?await r.setAgentProvider(t,void 0):await Z(f,r,w,{agentName:t,enabled:!1}),new re("relay enable cancelled by a later disable","CANCELLED");return{...n(),restarted:pe,busy:ge}});return r.reportRelayStateLocalChange(t),de},setAgentRelayCredential:async(t,a)=>{const i=m(),f=r.getAgentsStatus(),{restarted:w,busy:A}=await ee(i,r,g=>f.find(U=>U.name===g),{info:g=>s.info("main",g),warn:g=>s.warn("main",g)},{localName:t,agentId:a.agentId,virtualKey:a.virtualKey,anthropicBaseUrl:a.anthropicBaseUrl,openaiBaseUrl:a.openaiBaseUrl,model:a.model},(g,U)=>r.setAgentProvider(g,U));return r.reportRelayStateLocalChange(t),{...n(),restarted:w,busy:A}},disableAll:async()=>{const t=m();for(const i of r.getAgentsStatus())d.cancel(i.name);await Fe(t,r,{info:i=>s.info("main",i),warn:i=>s.warn("main",i)});const a=await r.clearDirectProviderConfigs();return a.length>0&&s.info("main",`relay disable-all cleared native provider config for: ${a.join(", ")}`),n()},setRoute:async(t,a)=>{const i=a,f=m();return f.setRoute({routeKey:t,targetBaseUrl:i.targetBaseUrl,...i.headers?{headers:i.headers}:{},...i.model?{model:i.model}:{},...i.modelMap?{modelMap:i.modelMap}:{},...i.passthrough?{passthrough:!0}:{},...i.relayOnly?{relayOnly:!0}:{},...i.codexResponsesToChat?{codexResponsesToChat:!0}:{}}),await f.persistConfig(),n()},deleteRoute:async t=>{const a=m();a.deleteRoute(t),await a.persistConfig()},setDefaultRoute:async t=>{const a=m();return a.setDefaultRouteKey(t),await a.persistConfig(),n()},setInterceptHosts:async t=>{const a=m();return a.setInterceptHosts(t),await a.persistConfig(),n()}};I=R,E.setProxyHandler(R),r.setRelayStateApplier({getLocalState:t=>{const a=m();if(a.getRelayAgents().includes(t)){const f=r.getAgentsStatus().find(g=>g.name===t),w=`grix-gateway-${String(f?.clientType??"").trim().toLowerCase()}`;return{enabled:!0,model:a.getConfigSnapshot().routes?.find(g=>g.routeKey===w)?.model?.trim()||void 0}}const i=r.getAgentProvider(t);return i?.api_key?.trim()?{enabled:!0,model:i.model?.trim()||void 0}:{enabled:!1}},applyEnable:async(t,a,i)=>{const f=r.getAgentsStatus().find(A=>A.name===t),w=String(f?.agentId??"").trim();if(!w)throw new te(`local agent "${t}" has no agent_id registered`,"MISSING_AGENT_ID");await R.setAgentRelayCredential(t,{agentId:w,virtualKey:a.apiKey,anthropicBaseUrl:a.anthropicBaseUrl,openaiBaseUrl:a.openaiBaseUrl,model:i??a.model})},applyDisable:async t=>{await R.setAgentRelay(t,!1)}})}if(F.setStatusProvider(()=>r.getAgentsStatus()),await r.start(Ke),P.length>0){const e=r.getAgentsStatus();for(const n of P){const d=e.find(t=>t.name===n);if(!d)continue;const R=Q(d.clientType);R.length!==0&&await b.setAgentRelayEnabled(n,!0,{relayHosts:R}).catch(t=>{s.warn("main",`failed to reconcile relay hosts for "${n}": ${t}`)})}}const V=parseInt(S["admin-port"]??process.env.GRIX_ADMIN_PORT??"19580",10);E.setAgentHandler({list:()=>r.getAgentsStatus(),add:e=>r.addAgent(e),remove:e=>r.removeAgent(e),restart:e=>r.restartAgent(e),reload:()=>r.reload()}),E.setUpgradeHandler({check:()=>r.checkUpgrade(),trigger:()=>r.triggerUpgrade()}),E.setProbeHandler({probeAll:e=>r.probeAll(e),probeOne:(e,n)=>r.probeOne(e,n)}),E.setInstallHandler({listInstallable:()=>r.listInstallable(),installAgent:e=>r.installAgent(e),getInstallProgress:e=>r.getInstallProgress(e)});const W={listAgents:()=>r.getAgentsStatus(),enableManaged:(e,n,d)=>{if(!I)throw new oe(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return I.setAgentRelayCredential(e,{agentId:n,virtualKey:d.virtualKey,anthropicBaseUrl:d.anthropicBaseUrl,openaiBaseUrl:d.openaiBaseUrl,model:d.model})},disableManaged:e=>{if(!I)throw new oe(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return I.setAgentRelay(e,!1)},enableHermesProfile:(e,n)=>Oe({agentId:e,baseUrl:n.openaiBaseUrl??"",virtualKey:n.virtualKey,model:n.model??""}),disableHermesProfile:e=>He(e)};E.setRelayCredentialHandler({listHermesProfiles:()=>Be(),enable:async(e,n)=>{const{target:d,result:R}=await Ge(W,e,n);return{target:d,...R}},disable:async e=>{const{target:n,result:d}=await Me(W,e);return{target:n,...d}}});{const e=await z({label:"admin",port:V,envVar:"GRIX_ADMIN_PORT",cliFlag:"admin-port",start:n=>E.start(n)});e&&await se(e)}const le=k.join(l.dataDir,"admin-token"),ce=k.join(l.dataDir,"admin-port");Te(le,ae),Y(ce,String(V),"utf-8"),await _(l.daemonStatusFile,{state:"running",pid:process.pid,instance_id:D,updated_at:Date.now()}),process.send&&process.send("ready"),s.info("main","grix-connector ready")}Ve().catch(async o=>{const c=`Fatal: ${o instanceof Error?o.stack??o.message:String(o)}`;try{process.stderr.write(`${c}
42
- `)}catch{}q(!1),s.error("main",c),N(o,"startup");const h=M(l.daemonLockFile);h?.pid===process.pid&&h.instance_id===D&&await _(l.daemonStatusFile,{state:"failed",pid:process.pid,instance_id:h.instance_id,updated_at:Date.now(),reason:"startup_error"}).catch(()=>{}),await G(),T(l.daemonLockFile).catch(()=>{}),C(),process.exit(1)});const We=new Set(["ECONNRESET","ECONNREFUSED","ETIMEDOUT","EPIPE","EAI_AGAIN","ENOTFOUND","EHOSTUNREACH","ENETUNREACH","EIO"]);function ie(o){return o instanceof Error&&"code"in o?We.has(o.code):!1}
39
+ right-click the terminal and select "Run as administrator".`);const o=G(),d=S["config-dir"]??(S.profile?I.join(o.configDir,S.profile):void 0),h=I.resolve(process.argv[1]||`${o.rootDir}/dist/grix.js`),g=new ke({cliPath:h,nodePath:process.execPath});try{let u;switch(b){case"start":(await g.status({rootDir:o.rootDir})).installed?u=await g.start({rootDir:o.rootDir}):u=await g.install({rootDir:o.rootDir,configDir:d});break;case"stop":u=await g.stop({rootDir:o.rootDir});break;case"restart":(await g.status({rootDir:o.rootDir})).installed?u=await g.restart({rootDir:o.rootDir}):u=await g.install({rootDir:o.rootDir,configDir:d});break;case"status":u=await g.status({rootDir:o.rootDir});break}console.log(JSON.stringify(u,null,2)),process.exit(0)}catch(u){console.error(`${b} failed: ${u instanceof Error?u.message:u}`),process.exit(1)}}else b&&(console.error(`Unknown command: ${b}
40
+ Valid commands: ${de.join(", ")}`),process.exit(1));const l=G(),ze=S["config-dir"]??(S.profile?`${l.configDir}/${S.profile}`:void 0),n=new Ae,B=new De,pe=Oe(),D=new Le(pe);let J=!1,C;async function ge(o){process.stderr.write(o.message+`
41
+ `),s.error("main",o.message.replace(/\n/g," \u2014 "));const d=j(l.daemonLockFile);d?.pid===process.pid&&d.instance_id===C&&await H(l.daemonStatusFile,{state:"failed",pid:process.pid,instance_id:C,updated_at:Date.now(),reason:`port_bind_${o.kind}:${o.label}:${o.port}`}).catch(()=>{}),await K(),L(l.daemonLockFile).catch(()=>{}),F(),process.exit(1)}async function M(o){if(J)return;J=!0,s.info("main",`Received ${o}, shutting down...`),B.markShuttingDown();const d=setTimeout(()=>{s.error("main","Shutdown timed out, forcing exit"),L(l.daemonLockFile).catch(()=>{}),F(),process.exit(2)},1e4);try{await n.stop(),await f()?.stop().catch(()=>{}),await D.stop(),await B.stop(),await K(),await L(l.daemonLockFile),await Fe(l.daemonStatusFile).catch(()=>{}),clearTimeout(d),F(),s.info("main","Shutdown complete"),process.exit(0)}catch(h){s.error("main",`Shutdown error: ${h}`),L(l.daemonLockFile).catch(()=>{}),F(),process.exit(2)}}async function Qe(){if(xe(),Ee(),be(l.stdoutLogFile,l.stderrLogFile),te(!1),await Be(),process.platform==="win32"){const e=Ue(l.rootDir,"win32");await Ce(e,{platform:"win32"})}try{C=(await _e(l.daemonLockFile,l.rootDir)).instance_id}catch(e){console.error(e instanceof Error?e.message:e),process.exit(1)}ve(),s.info("main",`grix-connector starting (PID ${process.pid})`),await H(l.daemonStatusFile,{state:"starting",pid:process.pid,instance_id:C,updated_at:Date.now()});const o=parseInt(S["health-port"]??process.env.GRIX_HEALTH_PORT??"19579",10);{const e=await re({label:"health",port:o,envVar:"GRIX_HEALTH_PORT",cliFlag:"health-port",start:a=>B.start(a)});e&&await ge(e)}const d=I.join(l.dataDir,"health-port");ee(d,String(o),"utf-8"),process.on("SIGINT",()=>M("SIGINT")),process.on("SIGTERM",()=>M("SIGTERM")),process.on("SIGHUP",()=>{J||(s.info("main","Received SIGHUP, reloading config..."),n.reload().then(e=>s.info("main",`reload done: ${JSON.stringify(e)}`)).catch(e=>s.error("main",`reload failed: ${e instanceof Error?e.message:e}`)))});let h="",g=0,u;process.on("uncaughtException",e=>{const a=e instanceof Error?e.stack??e.message:String(e);a===h?(g++,(g<=3||g%100===0)&&s.error("main",`Uncaught exception (x${g}): ${a}`)):(g>3&&s.error("main",`Previous exception repeated ${g} times total`),h=a,g=1,s.error("main",`Uncaught exception: ${e instanceof Error?e.stack:e}`),u||(u=setTimeout(()=>{g>3&&s.error("main",`Previous exception repeated ${g} times total`),h="",g=0,u=void 0},1e4).unref())),!ue(e)&&(V(e,"uncaughtException"),M("uncaughtException"))}),process.on("unhandledRejection",e=>{s.error("main",`Unhandled rejection: ${e}`),!ue(e)&&(V(e,"unhandledRejection"),M("unhandledRejection"))});const v=Me(l.dataDir);await v.clearRuntimeState();const P=new Ne(I.join(l.dataDir,"proxy","relay-state.json"),{warn:e=>s.warn("main",e)}),O=new Ge(I.join(l.dataDir,"secrets"));n.setRelaySecretResolver(e=>O.get(e));const T={hasDirectConfig:e=>n.hasDirectRelayConfig(e),clearDirectConfig:(e,a)=>n.setAgentProvider(e,void 0,{deferRestart:a?.deferRestart===!0})},N=()=>({pm:f(),agents:n,direct:T,store:P,secrets:O}),U=v.getRelayAgents();if(U.length>0)try{await v.startIfAnyRelayAgent(),s.info("main",`mitm proxy started for relay agents: ${U.join(", ")} (per-agent; others stay direct)`)}catch(e){s.error("main",`mitm proxy FAILED to start (relay agents: ${U.join(", ")}): ${e}. These agents will REFUSE to start (they must not silently fall back to your own account). Fix the proxy or turn relay off for them. Other agents are unaffected.`)}else s.info("main",'mitm proxy idle (no agent has Grix relay enabled; enable: PUT /api/proxy/agents/<name>/enabled {"enabled":true})');let _=null;if(f()){const e=async()=>{const t=f();t&&await oe(t,n)&&s.info("main","no agent uses Grix relay anymore; mitm proxy stopped")};n.setRelayEnvSettledHandler(()=>{const t=f();t&&ae(t,T,P),e().catch(r=>{s.warn("main",`failed to stop idle mitm proxy: ${r}`)})});const a=()=>{const t=f(),r=t.getRelayAgents(),i=t.getDegradedRelayAgents();return{enabled:r.length>0,relayAgents:r,staleRelayAgents:n.getAgentsWithStaleRelayEnv(),degradedRelayAgents:i,relayState:P.list(),runtime:t.getRuntimeInfo(),config:t.getConfigSnapshot()}},c=new je,A={status:()=>a(),setAgentRelay:async(t,r,i)=>{const p=f(),R={info:y=>s.info("main",y),warn:y=>s.warn("main",y)},w=y=>n.getAgentsStatus().find(x=>x.name===y),m=w(t);if(!r){c.cancel(t);const y=await c.runExclusive(t,async()=>{const x=await W(N(),R,{agentName:t,target:"off"});return{...a(),restarted:x.restarted,busy:x.busy,...x.pending?{pending:x.pending}:{}}});return n.reportRelayStateLocalChange(t),y}if(!m)throw Object.assign(new Error(`Agent "${t}" not found`),{code:"NOT_FOUND"});if(!Ie(m.clientType)&&ne(m.clientType).length===0)throw Object.assign(new Error(`client type "${m.clientType??""}" does not support Grix relay`),{code:"UNSUPPORTED_CLIENT_TYPE"});const $=await c.runExclusive(t,async()=>{const y=c.epoch(t),x=w(t);if(!x)throw Object.assign(new Error(`Agent "${t}" not found`),{code:"NOT_FOUND"});const z=String(x.agentId??"").trim();if(!z)throw new ie(`local agent "${t}" has no agent_id registered`,"MISSING_AGENT_ID");const Q=c.waitCancelled(t,y);let k;try{k=await Promise.race([n.fetchRelayCredential(t,{model:i}),Q.promise.then(()=>{throw new le("relay enable cancelled by a later disable","CANCELLED")})])}finally{Q.dispose()}c.assertCurrent(t,y);const{restarted:ye,busy:he,pending:Z}=await se(p,n,w,R,{localName:t,agentId:z,virtualKey:k.apiKey,anthropicBaseUrl:k.anthropicBaseUrl,openaiBaseUrl:k.openaiBaseUrl,model:i,...k.directRelay!==void 0?{directRelay:k.directRelay}:{}},(we,Re,Se)=>n.setAgentProvider(we,Re,{deferRestart:Se?.deferRestart===!0}),{store:P,secrets:O,direct:T});if(c.epoch(t)!==y)throw await W(N(),R,{agentName:t,target:"off"}),new le("relay enable cancelled by a later disable","CANCELLED");return{...a(),restarted:ye,busy:he,...Z?{pending:Z}:{}}});return n.reportRelayStateLocalChange(t),$},setAgentRelayCredential:async(t,r)=>{const i=f(),p=n.getAgentsStatus(),{restarted:R,busy:w,pending:m}=await se(i,n,E=>p.find($=>$.name===E),{info:E=>s.info("main",E),warn:E=>s.warn("main",E)},{localName:t,agentId:r.agentId,virtualKey:r.virtualKey,anthropicBaseUrl:r.anthropicBaseUrl,openaiBaseUrl:r.openaiBaseUrl,model:r.model,...r.directRelay!==void 0?{directRelay:r.directRelay}:{}},(E,$,y)=>n.setAgentProvider(E,$,{deferRestart:y?.deferRestart===!0}),{store:P,secrets:O,direct:T});return n.reportRelayStateLocalChange(t),{...a(),restarted:R,busy:w,...m?{pending:m}:{}}},disableAll:async()=>{const t=f(),r={info:p=>s.info("main",p),warn:p=>s.warn("main",p)},i=new Set([...t.getRelayAgents(),...n.getDirectRelayConfigAgentNames(),...Object.keys(P.list())]);for(const p of i)c.cancel(p);for(const p of i)await c.runExclusive(p,()=>W(N(),r,{agentName:p,target:"off"})),n.reportRelayStateLocalChange(p);return await oe(t,n),a()},setRoute:async(t,r)=>{const i=r,p=f();return p.setRoute({routeKey:t,targetBaseUrl:i.targetBaseUrl,...i.headers?{headers:i.headers}:{},...i.model?{model:i.model}:{},...i.modelMap?{modelMap:i.modelMap}:{},...i.passthrough?{passthrough:!0}:{},...i.relayOnly?{relayOnly:!0}:{},...i.codexResponsesToChat?{codexResponsesToChat:!0}:{}}),await p.persistConfig(),a()},deleteRoute:async t=>{const r=f();r.deleteRoute(t),await r.persistConfig()},setDefaultRoute:async t=>{const r=f();return r.setDefaultRouteKey(t),await r.persistConfig(),a()},setInterceptHosts:async t=>{const r=f();return r.setInterceptHosts(t),await r.persistConfig(),a()}};_=A,D.setProxyHandler(A),n.setRelayStateApplier({getLocalState:t=>{const r=f();if(r.getRelayAgents().includes(t)){const p=n.getAgentsStatus().find(m=>m.name===t),R=`grix-gateway-${String(p?.clientType??"").trim().toLowerCase()}`;return{enabled:!0,model:r.getConfigSnapshot().routes?.find(m=>m.routeKey===R)?.model?.trim()||void 0}}const i=n.getAgentProvider(t);return i?.api_key?.trim()?{enabled:!0,model:i.model?.trim()||void 0}:{enabled:!1}},applyEnable:async(t,r,i)=>{const p=n.getAgentsStatus().find(m=>m.name===t),R=String(p?.agentId??"").trim();if(!R)throw new ie(`local agent "${t}" has no agent_id registered`,"MISSING_AGENT_ID");const w=await A.setAgentRelayCredential(t,{agentId:R,virtualKey:r.apiKey,anthropicBaseUrl:r.anthropicBaseUrl,openaiBaseUrl:r.openaiBaseUrl,model:i??r.model,...r.directRelay!==void 0?{directRelay:r.directRelay}:{}});return{restarted:w.restarted===!0,busy:w.busy===!0,...w.pending?{pending:w.pending}:{}}},applyDisable:async t=>{const r=await A.setAgentRelay(t,!1);return{restarted:r.restarted===!0,busy:r.busy===!0,...r.pending?{pending:r.pending}:{}}}})}if(B.setStatusProvider(()=>n.getAgentsStatus()),await n.start(ze),U.length>0){const e=n.getAgentsStatus();for(const a of U){const c=e.find(t=>t.name===a);if(!c)continue;const A=ne(c.clientType);A.length!==0&&await v.setAgentRelayEnabled(a,!0,{relayHosts:A}).catch(t=>{s.warn("main",`failed to reconcile relay hosts for "${a}": ${t}`)})}}ae(v,T,P);const Y=parseInt(S["admin-port"]??process.env.GRIX_ADMIN_PORT??"19580",10);D.setAgentHandler({list:()=>n.getAgentsStatus(),add:e=>n.addAgent(e),remove:e=>n.removeAgent(e),restart:e=>n.restartAgent(e),reload:()=>n.reload()}),D.setUpgradeHandler({check:()=>n.checkUpgrade(),trigger:()=>n.triggerUpgrade()}),D.setProbeHandler({probeAll:e=>n.probeAll(e),probeOne:(e,a)=>n.probeOne(e,a)}),D.setInstallHandler({listInstallable:()=>n.listInstallable(),installAgent:e=>n.installAgent(e),getInstallProgress:e=>n.getInstallProgress(e)});const q={listAgents:()=>n.getAgentsStatus(),enableManaged:(e,a,c)=>{if(!_)throw new ce(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return _.setAgentRelayCredential(e,{agentId:a,virtualKey:c.virtualKey,anthropicBaseUrl:c.anthropicBaseUrl,openaiBaseUrl:c.openaiBaseUrl,model:c.model,...c.directRelay!==void 0?{directRelay:c.directRelay}:{}})},disableManaged:e=>{if(!_)throw new ce(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return _.setAgentRelay(e,!1)},enableHermesProfile:(e,a)=>Ke({agentId:e,baseUrl:a.openaiBaseUrl??"",virtualKey:a.virtualKey,model:a.model??""}),disableHermesProfile:e=>Ve(e)};D.setRelayCredentialHandler({listHermesProfiles:()=>We(),enable:async(e,a)=>{const{target:c,result:A}=await Je(q,e,a);return{target:c,...A}},disable:async e=>{const{target:a,result:c}=await Xe(q,e);return{target:a,...c}}});{const e=await re({label:"admin",port:Y,envVar:"GRIX_ADMIN_PORT",cliFlag:"admin-port",start:a=>D.start(a)});e&&await ge(e)}const fe=I.join(l.dataDir,"admin-token"),me=I.join(l.dataDir,"admin-port");He(fe,pe),ee(me,String(Y),"utf-8"),await H(l.daemonStatusFile,{state:"running",pid:process.pid,instance_id:C,updated_at:Date.now()}),process.send&&process.send("ready"),s.info("main","grix-connector ready")}Qe().catch(async o=>{const d=`Fatal: ${o instanceof Error?o.stack??o.message:String(o)}`;try{process.stderr.write(`${d}
42
+ `)}catch{}te(!1),s.error("main",d),V(o,"startup");const h=j(l.daemonLockFile);h?.pid===process.pid&&h.instance_id===C&&await H(l.daemonStatusFile,{state:"failed",pid:process.pid,instance_id:h.instance_id,updated_at:Date.now(),reason:"startup_error"}).catch(()=>{}),await K(),L(l.daemonLockFile).catch(()=>{}),F(),process.exit(1)});const Ze=new Set(["ECONNRESET","ECONNREFUSED","ETIMEDOUT","EPIPE","EAI_AGAIN","ENOTFOUND","EHOSTUNREACH","ENETUNREACH","EIO"]);function ue(o){return o instanceof Error&&"code"in o?Ze.has(o.code):!1}
package/dist/manager.js CHANGED
@@ -1,2 +1,2 @@
1
- import{readFileSync as Z,readdirSync as ee,writeFileSync as te}from"node:fs";import{join as v}from"node:path";import{homedir as ne}from"node:os";import{AgentInstance as D}from"./bridge/bridge.js";import{buildSharedInstanceConfig as ae,diffSharedOwners as re,sharedInstanceKey as se,SharedOwnersCache as ie}from"./manager-share-config.js";import{isAgentDeletedError as R}from"./core/aibot/errors.js";import{GRIX_PATHS as y,log as d}from"./core/log/index.js";import{resolveClientVersion as oe}from"./core/util/client-version.js";import{UpgradeChecker as ce}from"./core/upgrade/upgrade-checker.js";import{SkillSyncer as le}from"./core/skill-sync/skill-syncer.js";import{AgentGlobalConfigStore as de}from"./core/persistence/agent-global-config-store.js";import{resolveSkillScanMode as pe}from"./adapter/claude/skill-scanner.js";import{buildWireSkills as me}from"./core/skill-sync/sync-state.js";import{buildLibrarySkills as ue}from"./core/skill-sync/library-skills.js";import{AUDIT_LOCAL_ACTION_TYPES as ge}from"./core/aibot/types.js";import{buildReportedSkills as he,logDefaultSkillsCheck as fe,cleanupProjectedSkills as q}from"./default-skills/index.js";import{supportsKiroCwRelay as N,supportsNonMitmRelayConfig as _e}from"./core/config/provider-env.js";import{ensureKiroRelay as H,stopKiroRelay as U}from"./core/proxy/kiro-relay.js";import{resolveCopilotCommand as L}from"./core/runtime/copilot-resolve.js";import{resolveCursorCommand as B}from"./core/runtime/cursor-resolve.js";import{getCliVersion as ye,resolveCliPath as we}from"./core/util/cli-probe.js";import{AgentInstaller as Se}from"./core/installer/installer.js";import{reportInstallFailure as ve}from"./core/observability/sentry.js";import{createDefaultAuditProducer as Ae}from"./audit/runtime/default-audit-recorder.js";import{normalizeCodexConfigOverrides as be}from"./adapter/codex/config-overrides.js";const ke=8e3;function Ce(){const n=L();return[{clientType:"openclaw",command:"openclaw"},{clientType:"claude",command:"claude"},{clientType:"codex",command:"codex"},{clientType:"qwen",command:"qwen"},{clientType:"hermes",command:"hermes"},{clientType:"reasonix",command:"reasonix"},{clientType:"codewhale",command:"codewhale"},{clientType:"opencode",command:"opencode"},{clientType:"cursor",command:B()},{clientType:"pi",command:"pi"},{clientType:"openhuman",command:"openhuman-core"},{clientType:"kiro",command:"kiro-cli"},{clientType:"kimi",command:"kimi"},{clientType:"copilot",command:n.command},{clientType:"agy",command:"agy"}]}async function xe(){return Promise.all(Ce().map(async n=>{const e=await we(n.command);if(!e)return{client_type:n.clientType,command:n.command,installed:!1,path:null,version:null};const t=await ye(e,n.versionArgs??["--version"]);return{client_type:n.clientType,command:n.command,installed:!0,path:e,version:t.version,error:t.error}}))}function $e(n){switch(n){case"claude":return{adapterType:"claude",command:"claude"};case"codex":return{adapterType:"codex",command:"codex",options:{sandboxMode:"danger-full-access"}};case"gemini":return{adapterType:"acp",command:"gemini",autoInjectArgs:{acp:!0},enableSessionBinding:!0};case"qwen":return{adapterType:"acp",command:"qwen",adapterHint:"qwen/base",autoInjectArgs:{acp:!0},enableSessionBinding:!0};case"pi":return{adapterType:"pi",command:"pi"};case"cursor":return{adapterType:"cursor",command:B()};case"reasonix":return{adapterType:"acp",command:"reasonix",args:["acp"],enableSessionBinding:!0};case"codewhale":return{adapterType:"codewhale",command:"codewhale",enableSessionBinding:!0};case"openhuman":return{adapterType:"openhuman",command:"openhuman-core",enableSessionBinding:!0};case"kiro":return{adapterType:"acp",command:"kiro-cli",args:["acp"],enableSessionBinding:!0};case"kimi":return{adapterType:"acp",command:"kimi",args:["acp"],options:{initial_mode:"yolo"},enableSessionBinding:!0};case"opencode":return{adapterType:"opencode",command:"opencode",args:["serve"],enableSessionBinding:!0};case"copilot":{const e=L();return{adapterType:"acp",command:e.command,args:[...e.prefixArgs,"--acp"],enableSessionBinding:!0}}case"agy":return{adapterType:"agy",command:"agy",enableSessionBinding:!0};case"hermes":return{adapterType:"acp",command:"hermes",args:["acp"],enableSessionBinding:!0};default:throw new Error(`Unsupported client_type: ${n}`)}}function A(n,e){return String(n??"").trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||String(e??"").trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"default"}function Te(n,e){return v(y.data,`session-bindings-${A(n,e)}.json`)}function Ie(n,e){return v(y.data,`active-events-${A(n,e)}.json`)}function Ee(n,e){const t=A(n,"agent"),a=A(e,"unknown");return v(y.data,`terminal-outbox-${t}-${a}.json`)}function Me(...n){const e=[],t=new Set;for(const a of n)for(const r of a??[]){const o=String(r??"").trim(),s=o.toLowerCase();!o||t.has(s)||(t.add(s),e.push(o))}return e.length>0?e:void 0}function Pe(n,e){const t={claude:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},codex:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},cursor:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},acp:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},pi:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},codewhale:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},openhuman:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},opencode:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},agy:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0}},a=t[n]??t.acp;return{maxConcurrent:e?.max_concurrent??a.maxConcurrent??1,maxQueued:e?.max_queued??a.maxQueued??3,queueTimeoutMs:e?.queue_timeout_ms??a.queueTimeoutMs??0,cancelableQueued:!0,cancelableRunning:!0}}function b(n,e={}){const t=oe(),a=String(n.client_type??"").trim().toLowerCase(),r=$e(a),o=String(n.ws_url??"").trim(),s="get_session_usage",m="get_rate_limits",c="get_agent_global_config",i=ge;if(!n.name?.trim())throw new Error("agent name is required");if(!o)throw new Error(`agent ${n.name}: ws_url is required`);if(!n.agent_id?.trim())throw new Error(`agent ${n.name}: agent_id is required`);if(!n.api_key?.trim())throw new Error(`agent ${n.name}: api_key is required`);const l=r.adapterType,u=l==="acp",_=a==="qwen",p={...r.options??{}};if(n.codex_config_overrides!==void 0){if(l!=="codex")throw new Error(`agent ${n.name}: codex_config_overrides is only supported for client_type=codex`);be(n.codex_config_overrides,`agent ${n.name}: codex_config_overrides`),p.codexConfigOverrides=n.codex_config_overrides}const h=l==="codex"?{capabilities:["local_action_v1","agent_invoke"],localActions:["session_control","get_context","set_model","set_mode","set_reasoning_effort","set_service_tier","set_sandbox_mode","exec_approve","exec_reject","file_list","create_folder","turn_interrupt","permission_approve","permission_reject","thread_compact",s,m]}:null,f=l==="claude"?{localActions:["session_control","set_mode","set_model","claude_interaction_reply","exec_approve","exec_reject","file_list","create_folder","thread_compact",s,m]}:null,g=_?{capabilities:["stream_chunk","local_action_v1"],localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",s],adapterHint:"qwen/base"}:null,S=l==="pi"?{adapterHint:"pi/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","get_context","file_list","create_folder",s,m]}:null,w=l==="openhuman"?{adapterHint:"openhuman/base",capabilities:["local_action_v1"],localActions:["session_control","set_model","file_list","create_folder"]}:null,C=l==="cursor"?{adapterHint:"cursor/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","set_mode","get_context","file_list","create_folder","thread_compact","claude_interaction_reply",m]}:null,E=l==="codewhale"?{capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",s]}:null,x=l==="opencode"?{adapterHint:"opencode/base",capabilities:["stream_chunk","local_action_v1"],localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","file_list","create_folder"]}:null,$=l==="agy"?{adapterHint:"agy/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder"]}:null,M=new Set(["hermes","reasonix","kimi","copilot"]),Q=u&&!M.has(a),K=u&&!_?{localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",...Q?[s]:[]]}:null,W=a==="kiro"?{localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder","thread_compact",s,m]}:null,z=l==="codex"||l==="claude"||a==="gemini"?["session_control","set_model","set_mode"]:void 0,G=l==="cursor"||l==="opencode"||l==="openhuman"||l==="agy"||u&&M.has(a)?[m]:[s,m];u&&p.raw_transport===void 0&&(p.raw_transport=a==="gemini");const J=`${a}/base`,T=pe(l,a),V=T==="kiro"?void 0:process.cwd(),P=he({mode:T,projectDir:V}),j=P.filter(I=>I.source==="connector").map(I=>I.name);j.length>0&&!e.quiet&&d.info("manager",`[${n.name}] injecting connector skills: [${j.join(", ")}]`);const O=me(P,y.skills);let X=O.length>0?O:void 0;const Y=ue({skillsDir:y.skills,mode:T,home:ne()});return{name:n.name,adapterType:l,aibot:{url:o,agentId:n.agent_id,apiKey:n.api_key,clientType:a,clientVersion:t,adapterHint:r.adapterHint??g?.adapterHint??S?.adapterHint??w?.adapterHint??C?.adapterHint??x?.adapterHint??$?.adapterHint??J,capabilities:h?.capabilities??E?.capabilities??S?.capabilities??w?.capabilities??C?.capabilities??x?.capabilities??$?.capabilities??g?.capabilities??["stream_chunk","local_action_v1","connector_upgrade"],localActions:Me(h?.localActions??E?.localActions??f?.localActions??S?.localActions??w?.localActions??C?.localActions??x?.localActions??$?.localActions??g?.localActions??W?.localActions??K?.localActions??["exec_approve","exec_reject"],z,G,["connector_rollback","connector_upgrade_push",c,"configure_gateway_provider","skill_upload","skill_enable","skill_disable","skill_refresh",...i]),skills:X,librarySkills:Y,terminalOutboxPath:Ee(n.name,n.agent_id)},agent:{command:r.command,args:[...r.args??[],...n.args??[]],env:n.env,clientType:a,...n.provider?{provider:{...n.provider.provider_id?.trim()?{providerId:n.provider.provider_id.trim()}:{},...n.provider.base_url?.trim()?{baseUrl:n.provider.base_url.trim()}:{},...n.provider.quota_base_url?.trim()?{quotaBaseUrl:n.provider.quota_base_url.trim()}:{},...n.provider.api_key?.trim()?{apiKey:n.provider.api_key.trim()}:{},...n.provider.model?.trim()?{model:n.provider.model.trim()}:{}}}:{}},adapterOptions:p,acpAuthMethod:p.auth_method,acpInitialMode:p.initial_mode,acpMcpTools:p.acp_mcp_tools,promptTimeoutMs:n.prompt_timeout_ms,bindingsPath:Te(n.name,n.agent_id),activeEventStorePath:Ie(n.name,n.agent_id),...r.enableSessionBinding||u?{enableSessionBinding:!0}:{},...r.autoInjectArgs||a==="reasonix"&&n.provider?.model?.trim()?{autoInjectArgs:{...r.autoInjectArgs??{},...a==="reasonix"&&n.provider?.model?.trim()?{model:n.provider.model.trim()}:{}}}:{},poolMaxSize:n.pool?.maxSize,poolIdleTimeoutMs:n.pool?.idleTimeoutMs,eventQueue:Pe(l,n.event_queue),logDir:y.log,providerBaseUrl:n.provider_base_url?.trim()||void 0,providerApiKey:n.provider_api_key?.trim()||void 0}}function je(){const n=process.env.GRIX_AGENT_STARTUP_WAIT_MS,e=Number(n);return Number.isFinite(e)&&e>=500?Math.floor(e):ke}function F(n){const e=[],t=[],a=[],r=ee(n).filter(o=>o.endsWith(".json")).sort();for(const o of r)try{const s=JSON.parse(Z(v(n,o),"utf-8"));if(s===null||typeof s!="object"||Array.isArray(s)||!("agents"in s)){a.push(o);continue}if(!Array.isArray(s.agents)){t.push({file:o,error:"agents is not an array"});continue}if(s.agents.length===0){t.push({file:o,error:"agents array is empty"});continue}for(const m of s.agents)e.push({entry:m,file:o})}catch(s){t.push({file:o,error:s instanceof Error?s.message:String(s)})}return{entries:e,fileErrors:t,skipped:a}}function k(n){if(n===null||typeof n!="object")return JSON.stringify(n)??"null";if(Array.isArray(n))return`[${n.map(k).join(",")}]`;const e=n;return`{${Object.keys(e).sort().map(a=>`${JSON.stringify(a)}:${k(e[a])}`).join(",")}}`}function Oe(n,e){return k(n)===k(e)}class ct{instances=[];configMap=new Map;rawConfigMap=new Map;reloadChain=Promise.resolve();sharedInstances=new Map;shareSyncChains=new Map;stopping=!1;deletedAgentCleanups=new Set;upgradeChecker=null;skillSyncer=null;globalConfigStore;configDir=y.config;sharedOwnersCache=new ie(y.data);installer=new Se;auditProducer=Ae();async start(e){const t=e??y.config;this.configDir=t,d.info("manager",`Loading configs from ${t}`),fe(),q(),this.globalConfigStore=new de(v(y.data,"agent-global-configs.json")),this.globalConfigStore.load();const{entries:a,fileErrors:r,skipped:o}=F(t);for(const i of r)d.error("manager",`Failed to load ${i.file}: ${i.error}`);for(const i of o)d.info("manager",`Skipped ${i}: not an agent config (no "agents" key)`);if(a.length===0&&r.length===0&&o.length===0){d.warn("manager",`No config files found in ${t}; daemon idle (waiting for config). Add agents under this directory then run: grix-connector reload`);return}const s=[];let m=r.length;for(const{entry:i,file:l}of a)try{const u=b(i);s.push({config:u,entry:i,file:l}),d.info("manager",`Loaded ${u.name} (${u.adapterType??"acp"}) from ${l}`)}catch(u){const _=typeof i?.name=="string"?i.name:"<unknown>";d.error("manager",`Invalid agent config in ${l} (name=${_}): ${u}`),m++}let c=0;if(s.length>0){const i=je();d.info("manager",`Starting ${s.length} agent(s), startup wait=${i}ms`);const l=p=>{this.instances=this.instances.filter(h=>h!==p)},u=s.map(({config:p,entry:h,file:f})=>{const g=this.createConfiguredInstance(p);return this.instances.push(g),this.configMap.set(p.name,p),this.rawConfigMap.set(p.name,{entry:h,file:f}),{config:p,instance:g,startPromise:this.prepareProviderRuntime(p,h).then(()=>g.start())}}),_=await Promise.all(u.map(async p=>{const h=await new Promise(f=>{let g=!1;const S=setTimeout(()=>{g||(g=!0,f({kind:"timeout"}))},i);p.startPromise.then(()=>{g||(g=!0,clearTimeout(S),f({kind:"started"}))}).catch(w=>{g||(g=!0,clearTimeout(S),f({kind:"failed",error:w}))})});return{task:p,outcome:h}}));for(const{task:p,outcome:h}of _){if(h.kind==="started"){this.restoreCachedSharedInstances(p.config);continue}if(h.kind==="failed"){if(R(h.error)){this.onAgentDeletedByPlatform(p.config.name);continue}l(p.instance),d.error("manager",`Failed to start ${p.config.name}: ${h.error}`);continue}c++,d.warn("manager",`Startup pending for ${p.config.name}, continue retrying in background`),p.startPromise.then(()=>{d.info("manager",`Delayed start succeeded: ${p.config.name}`),this.restoreCachedSharedInstances(p.config)}).catch(f=>{if(R(f)){this.onAgentDeletedByPlatform(p.config.name);return}l(p.instance),d.error("manager",`Delayed start failed: ${p.config.name}: ${f}`)})}if(this.instances.length>0){const p=Math.max(0,this.instances.length-c);d.info("manager",`${p}/${s.length} agent(s) running now`)}c>0&&d.warn("manager",`${c} agent(s) still connecting in background`)}if(this.instances.length===0&&s.length>0)throw new Error("All agent configurations failed to start");await this.ensureAuxServices()}async ensureAuxServices(){if(this.configMap.size===0)return;const e=[...this.configMap.values()].map(t=>({apiKey:t.aibot.apiKey,wsUrl:t.aibot.url}));this.upgradeChecker||(this.upgradeChecker=new ce(e,()=>this.instances.some(t=>t.hasPendingOrBackgroundWork())||[...this.sharedInstances.values()].some(t=>t.hasPendingOrBackgroundWork())),await this.upgradeChecker.start()),this.skillSyncer||(this.skillSyncer=new le(e,y.skills),this.skillSyncer.setOnSyncSuccess(()=>this.refreshAllInstanceSkills()),await this.skillSyncer.start())}refreshAllInstanceSkills(){for(const e of this.instances)try{e.forceRefreshSkills()}catch(t){d.warn("manager",`forceRefreshSkills failed for ${e.name}: ${t}`)}for(const e of this.sharedInstances.values())try{e.forceRefreshSkills()}catch(t){d.warn("manager",`forceRefreshSkills failed for shared instance: ${t}`)}}async stop(){d.info("manager","Stopping all agents..."),this.stopping=!0,this.upgradeChecker?.stop(),this.skillSyncer?.stop(),await Promise.allSettled([...this.shareSyncChains.values()]),this.shareSyncChains.clear();const e=[...this.sharedInstances.values()];this.sharedInstances.clear(),await Promise.allSettled([...this.instances.map(t=>t.stop()),...e.map(t=>t.stop())]),await this.globalConfigStore?.flush(),await this.auditProducer.stop({timeoutMs:5e3}).catch(()=>!1),this.instances=[],q(),d.info("manager","All stopped")}onShareSet(e,t){this.sharedOwnersCache.save(e.name,t);const r=(this.shareSyncChains.get(e.name)??Promise.resolve()).catch(()=>{}).then(()=>this.syncSharedInstances(e,t));this.shareSyncChains.set(e.name,r)}async syncSharedInstances(e,t){if(this.stopping)return;const{toAdd:a,toRemove:r}=re(e.name,this.sharedInstances.keys(),t);for(const o of a){if(this.stopping)break;const s=se(e.name,o);try{const m=ae(e,o),c=new D(m,this.globalConfigStore,this.auditProducer);this.sharedInstances.set(s,c),await c.start(),d.info("manager",`shared instance started: ${s}`)}catch(m){this.sharedInstances.delete(s);const c=m instanceof Error?m.message:String(m);/Auth failed/i.test(c)?(this.sharedOwnersCache.remove(e.name,o),d.warn("manager",`shared instance auth rejected, removed from cache: ${s}`)):d.error("manager",`start shared instance failed ${s}: ${m}`)}}for(const{key:o}of r){const s=this.sharedInstances.get(o);s&&(this.sharedInstances.delete(o),s.stop().catch(m=>d.error("manager",`stop shared instance failed ${o}: ${m}`)),d.info("manager",`shared instance stopped: ${o}`))}}restoreCachedSharedInstances(e){const t=this.sharedOwnersCache.load(e.name);t.length!==0&&(d.info("manager",`restoring ${t.length} cached shared instance(s) for ${e.name}`),this.onShareSet(e,t))}getAgentsStatus(){return this.instances.map(e=>e.getStatus())}async addAgent(e){await this.addAgentInternal(e,"agents.json"),this.persistAgentsConfig(),await this.ensureAuxServices(),d.info("manager",`Added agent: ${e.name}`)}async addAgentInternal(e,t){const a=b(e);if(this.instances.some(r=>r.name===a.name))throw new Error(`Agent "${a.name}" already exists`);await this.startInstanceFromConfig(a,e,t)}createConfiguredInstance(e){const t=new D(e,this.globalConfigStore,this.auditProducer);return t.setUpgradeTrigger(()=>this.upgradeChecker?.triggerCheck()),t.setShareSetHandler(a=>this.onShareSet(e,a)),t.setAgentDeletedHandler(()=>this.onAgentDeletedByPlatform(e.name)),t.setSkillSyncHandler(()=>this.skillSyncer?.triggerSync()),t.setProviderConfigHandler(a=>this.setAgentProvider(e.name,a)),t.setRelayStateApplyPorts({getLocalState:()=>this.relayStateApplier?.getLocalState(e.name)??{enabled:!1},applyEnable:(a,r)=>{const o=this.relayStateApplier;return o?o.applyEnable(e.name,a,r):Promise.reject(new Error("relay state applier is not configured"))},applyDisable:()=>{const a=this.relayStateApplier;return a?a.applyDisable(e.name):Promise.reject(new Error("relay state applier is not configured"))}}),t.setRelayEnvSettledHandler(()=>this.relayEnvSettledHandler?.()),t}async prepareProviderRuntime(e,t){N(t.client_type)&&t.provider?.base_url?.trim()&&t.provider?.api_key?.trim()&&t.provider?.model?.trim()&&await H(e.name,{baseUrl:t.provider.base_url.trim(),apiKey:t.provider.api_key.trim(),model:t.provider.model.trim()})}async startInstanceFromConfig(e,t,a){await this.prepareProviderRuntime(e,t);const r=this.createConfiguredInstance(e);await r.start(),this.instances.push(r),this.configMap.set(e.name,e),this.rawConfigMap.set(e.name,{entry:t,file:a}),this.relayEnvSettledHandler?.()}async removeAgent(e){if(!this.instances.some(t=>t.name===e))throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});await this.removeAgentInternal(e),this.persistAgentsConfig(),d.info("manager",`Removed agent: ${e}`),this.relayEnvSettledHandler?.()}async removeAgentInternal(e,t){const a=this.instances.findIndex(m=>m.name===e),r=a===-1?void 0:this.instances[a];a!==-1&&this.instances.splice(a,1),this.configMap.delete(e),this.rawConfigMap.delete(e);const o=this.shareSyncChains.get(e);this.shareSyncChains.delete(e),o&&await o.catch(()=>{});const s=`${e}#shared:`;for(const[m,c]of[...this.sharedInstances.entries()])m.startsWith(s)&&(this.sharedInstances.delete(m),c.stop().catch(i=>d.error("manager",`stop shared instance failed ${m}: ${i}`)));t?.keepShareCache||this.sharedOwnersCache.delete(e),r&&await r.stop(),t?.keepShareCache||await U(e).catch(()=>{})}onAgentDeletedByPlatform(e){this.stopping||this.deletedAgentCleanups.has(e)||(this.deletedAgentCleanups.add(e),(async()=>{try{if(!(this.rawConfigMap.has(e)||this.instances.some(a=>a.name===e)))return;d.warn("manager",`Agent "${e}" \u5DF2\u5728\u5E73\u53F0\u5220\u9664\uFF0C\u505C\u6B62\u5B9E\u4F8B\u5E76\u6E05\u7406\u672C\u5730\u914D\u7F6E`),await this.removeAgentInternal(e),this.persistAgentsConfig(),d.info("manager",`Agent "${e}" \u5DF2\u5728\u5E73\u53F0\u5220\u9664\uFF0C\u672C\u5730\u914D\u7F6E\u5DF2\u6E05\u7406\uFF08agents.json \u5DF2\u79FB\u9664\u8BE5\u6761\u76EE\uFF09`)}catch(t){d.error("manager",`cleanup for deleted agent "${e}" failed: ${t}`)}finally{this.deletedAgentCleanups.delete(e)}})())}persistAgentsConfig(){const e=v(this.configDir,"agents.json");try{const t=[];for(const[,r]of this.configMap){const o=this.rawConfigMap.get(r.name)?.entry.provider;t.push({name:r.name,ws_url:r.aibot.url,agent_id:r.aibot.agentId,api_key:r.aibot.apiKey,client_type:r.aibot.clientType,...o?{provider:o}:{}})}te(e,JSON.stringify({agents:t},null,4)+`
2
- `,"utf-8")}catch(t){d.error("manager",`Failed to persist agents config: ${t}`)}}isAgentBusy(e){return this.instances.find(a=>a.name===e)?.hasPendingWork()??!1}getAgentsWithStaleRelayEnv(){return this.instances.filter(e=>e.hasStaleRelayEnv()).map(e=>e.name)}markAgentRelayEnvStale(e){const t=this.instances.find(a=>a.name===e);return t?(t.markRelayEnvStale(),!0):!1}relayEnvSettledHandler;setRelayEnvSettledHandler(e){this.relayEnvSettledHandler=e}relayStateApplier;setRelayStateApplier(e){this.relayStateApplier=e}reportRelayStateLocalChange(e){this.instances.find(a=>a.name===e)?.reportRelayStateLocalChange()}getAgentProvider(e){return this.rawConfigMap.get(e)?.entry.provider}async restartAgent(e){const t=this.rawConfigMap.get(e);if(!t)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});await this.replaceInstance(e,t.entry,t.file),d.info("manager",`Restarted agent: ${e}`)}async setAgentProvider(e,t){const a=this.rawConfigMap.get(e);if(!a)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});const r=String(a.entry.client_type??"").trim().toLowerCase();N(r)&&(t?.base_url?.trim()&&t?.api_key?.trim()&&t?.model?.trim()?await H(e,{baseUrl:t.base_url.trim(),apiKey:t.api_key.trim(),model:t.model.trim()}):await U(e));const o={...a.entry,provider:t};await this.replaceInstance(e,o,a.file),this.persistAgentsConfig(),d.info("manager",`Updated provider config for agent: ${e}`)}async fetchRelayCredential(e,t){const a=this.instances.find(r=>r.name===e);if(!a)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});return a.fetchRelayCredential(t)}async clearDirectProviderConfigs(){const e=[...this.rawConfigMap.entries()].filter(([,a])=>!!a.entry.provider&&_e(a.entry.client_type)).map(([a])=>a),t=[];for(const a of e)try{await this.setAgentProvider(a,void 0),t.push(a)}catch(r){d.warn("manager",`Failed to clear provider config for "${a}": ${r instanceof Error?r.message:String(r)}`)}return t}async replaceInstance(e,t,a){const r=b(t);await this.removeAgentInternal(e,{keepShareCache:!0}),await this.startInstanceFromConfig(r,t,a),this.restoreCachedSharedInstances(r)}async reload(){const e=this.reloadChain.catch(()=>{}).then(()=>this.doReload());return this.reloadChain=e.catch(()=>{}),e}async doReload(){if(this.stopping)throw Object.assign(new Error("manager is stopping"),{code:"RELOAD_UNSAFE"});const{entries:e,fileErrors:t,skipped:a}=F(this.configDir);for(const c of a)d.info("manager",`reload: skipped ${c}: not an agent config (no "agents" key)`);if(t.length>0){const c=t.map(i=>`${i.file}: ${i.error}`).join("; ");throw Object.assign(new Error(`reload aborted: ${t.length} config file(s) failed to parse [${c}]`),{code:"RELOAD_UNSAFE"})}if(e.length===0)throw Object.assign(new Error("reload aborted: no valid agent config found"),{code:"RELOAD_UNSAFE"});const r=[];for(const{entry:c}of e)try{b(c,{quiet:!0})}catch(i){const l=c?.name,u=typeof l=="string"&&l.trim()?l:"<unknown>";r.push({name:u,error:i instanceof Error?i.message:String(i)})}if(r.length>0){const c=r.map(i=>`${i.name}: ${i.error}`).join("; ");throw Object.assign(new Error(`reload aborted: ${r.length} agent config(s) invalid [${c}]`),{code:"RELOAD_UNSAFE"})}const o=new Map;for(const c of e){const i=String(c.entry?.name??"").trim();i&&(o.has(i)&&d.warn("manager",`reload: duplicate agent name "${i}", last one wins`),o.set(i,c))}const s={added:[],removed:[],restarted:[],unchanged:[],failed:[]},m=new Set(this.rawConfigMap.keys());for(const c of m)if(!o.has(c))try{await this.removeAgentInternal(c),s.removed.push(c)}catch(i){s.failed.push({name:c,error:i instanceof Error?i.message:String(i)})}for(const[c,i]of o){const l=this.rawConfigMap.get(c);if(l)if(Oe(l.entry,i.entry))this.rawConfigMap.set(c,i),s.unchanged.push(c);else try{await this.replaceInstance(c,i.entry,i.file),s.restarted.push(c)}catch(u){s.failed.push({name:c,error:u instanceof Error?u.message:String(u)})}else try{await this.addAgentInternal(i.entry,i.file),s.added.push(c)}catch(u){s.failed.push({name:c,error:u instanceof Error?u.message:String(u)})}}return d.info("manager",`reload done: +${s.added.length} -${s.removed.length} ~${s.restarted.length} =${s.unchanged.length} !${s.failed.length}`),await this.ensureAuxServices(),s}async checkUpgrade(){return this.upgradeChecker?this.upgradeChecker.checkForUpdate():{available:!1}}triggerUpgrade(){this.upgradeChecker?.triggerCheck()}async probeAll(e={}){return De(this.instances,e)}async probeOne(e,t={}){return Re(this.instances,e,t)}listInstallable(){return this.installer.listInstallable()}async installAgent(e){const t=await this.installer.install(e);return ve(t),t}getInstallProgress(e){return this.installer.getProgress(e)??null}}async function De(n,e){const t=e.concurrency??4,a=Date.now(),r=new Array(n.length);await new Promise(i=>{let l=0,u=0;const _=n.length;if(_===0){i();return}function p(g){const S=n[g];S.probe(e).then(w=>{r[g]=w,h()},w=>{r[g]={agent_name:S.name,client_type:"unknown",adapter_type:"acp",ok:!1,status:"error",probed_at:Date.now(),duration_ms:0,cached:!1,cli:{command:"",installed:!1,path:null,version:null,error:{code:"internal",message:w?.message??String(w)}},conversation:{attempted:!1,ok:!1,latency_ms:null},config:{model:null,base_url:null,source:{model:"unknown",base_url:"unknown"}},process:{started:!1,alive:!1,busy:!1}},h()})}function h(){u++,l<_?p(l++):u===_&&i()}const f=Math.min(t,_);for(let g=0;g<f;g++)p(l++)});const o=r.filter(i=>i.status==="healthy").length,s=r.filter(i=>i.status==="degraded").length,m=r.filter(i=>i.status==="unavailable").length,c=await xe();return{ok:o===r.length&&r.length>0,total:r.length,healthy:o,degraded:s,unavailable:m,installed_clients:c,agents:r,probed_at:a,duration_ms:Date.now()-a}}async function Re(n,e,t){const a=n.find(r=>r.name===e);if(!a)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});return a.probe(t)}export{ct as Manager,xe as probeInstalledClientCommands,De as probeInstances,Re as probeOneInstance};
1
+ import{readFileSync as Z,readdirSync as ee,writeFileSync as te}from"node:fs";import{join as A}from"node:path";import{homedir as ne}from"node:os";import{AgentInstance as D}from"./bridge/bridge.js";import{buildSharedInstanceConfig as re,diffSharedOwners as ae,sharedInstanceKey as ie,SharedOwnersCache as se}from"./manager-share-config.js";import{isAgentDeletedError as O}from"./core/aibot/errors.js";import{GRIX_PATHS as y,log as p}from"./core/log/index.js";import{resolveClientVersion as oe}from"./core/util/client-version.js";import{UpgradeChecker as ce}from"./core/upgrade/upgrade-checker.js";import{SkillSyncer as le}from"./core/skill-sync/skill-syncer.js";import{AgentGlobalConfigStore as de}from"./core/persistence/agent-global-config-store.js";import{resolveSkillScanMode as pe}from"./adapter/claude/skill-scanner.js";import{buildWireSkills as me}from"./core/skill-sync/sync-state.js";import{buildLibrarySkills as ue}from"./core/skill-sync/library-skills.js";import{AUDIT_LOCAL_ACTION_TYPES as ge}from"./core/aibot/types.js";import{buildReportedSkills as fe,logDefaultSkillsCheck as he,cleanupProjectedSkills as N}from"./default-skills/index.js";import{supportsKiroCwRelay as q,supportsNonMitmRelayConfig as _e}from"./core/config/provider-env.js";import{ensureKiroRelay as H,stopKiroRelay as L}from"./core/proxy/kiro-relay.js";import{resolveCopilotCommand as U}from"./core/runtime/copilot-resolve.js";import{resolveCursorCommand as B}from"./core/runtime/cursor-resolve.js";import{getCliVersion as ye,resolveCliPath as we}from"./core/util/cli-probe.js";import{AgentInstaller as ve}from"./core/installer/installer.js";import{reportInstallFailure as Se}from"./core/observability/sentry.js";import{createDefaultAuditProducer as Ae}from"./audit/runtime/default-audit-recorder.js";import{normalizeCodexConfigOverrides as ke}from"./adapter/codex/config-overrides.js";import{resolveCodexDirectHome as be}from"./adapter/codex/direct-home.js";const Ce=8e3;function xe(){const n=U();return[{clientType:"openclaw",command:"openclaw"},{clientType:"claude",command:"claude"},{clientType:"codex",command:"codex"},{clientType:"qwen",command:"qwen"},{clientType:"hermes",command:"hermes"},{clientType:"reasonix",command:"reasonix"},{clientType:"codewhale",command:"codewhale"},{clientType:"opencode",command:"opencode"},{clientType:"cursor",command:B()},{clientType:"pi",command:"pi"},{clientType:"openhuman",command:"openhuman-core"},{clientType:"kiro",command:"kiro-cli"},{clientType:"kimi",command:"kimi"},{clientType:"copilot",command:n.command},{clientType:"agy",command:"agy"}]}async function $e(){return Promise.all(xe().map(async n=>{const e=await we(n.command);if(!e)return{client_type:n.clientType,command:n.command,installed:!1,path:null,version:null};const t=await ye(e,n.versionArgs??["--version"]);return{client_type:n.clientType,command:n.command,installed:!0,path:e,version:t.version,error:t.error}}))}function Ie(n){switch(n){case"claude":return{adapterType:"claude",command:"claude"};case"codex":return{adapterType:"codex",command:"codex",options:{sandboxMode:"danger-full-access"}};case"gemini":return{adapterType:"acp",command:"gemini",autoInjectArgs:{acp:!0},enableSessionBinding:!0};case"qwen":return{adapterType:"acp",command:"qwen",adapterHint:"qwen/base",autoInjectArgs:{acp:!0},enableSessionBinding:!0};case"pi":return{adapterType:"pi",command:"pi"};case"cursor":return{adapterType:"cursor",command:B()};case"reasonix":return{adapterType:"acp",command:"reasonix",args:["acp"],enableSessionBinding:!0};case"codewhale":return{adapterType:"codewhale",command:"codewhale",enableSessionBinding:!0};case"openhuman":return{adapterType:"openhuman",command:"openhuman-core",enableSessionBinding:!0};case"kiro":return{adapterType:"acp",command:"kiro-cli",args:["acp"],enableSessionBinding:!0};case"kimi":return{adapterType:"acp",command:"kimi",args:["acp"],options:{initial_mode:"yolo"},enableSessionBinding:!0};case"opencode":return{adapterType:"opencode",command:"opencode",args:["serve"],enableSessionBinding:!0};case"copilot":{const e=U();return{adapterType:"acp",command:e.command,args:[...e.prefixArgs,"--acp"],enableSessionBinding:!0}}case"agy":return{adapterType:"agy",command:"agy",enableSessionBinding:!0};case"hermes":return{adapterType:"acp",command:"hermes",args:["acp"],enableSessionBinding:!0};default:throw new Error(`Unsupported client_type: ${n}`)}}function k(n,e){return String(n??"").trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||String(e??"").trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"default"}function Te(n,e){return A(y.data,`session-bindings-${k(n,e)}.json`)}function Ee(n,e){return A(y.data,`active-events-${k(n,e)}.json`)}function Re(n,e){const t=k(n,"agent"),r=k(e,"unknown");return A(y.data,`terminal-outbox-${t}-${r}.json`)}function Pe(...n){const e=[],t=new Set;for(const r of n)for(const a of r??[]){const o=String(a??"").trim(),i=o.toLowerCase();!o||t.has(i)||(t.add(i),e.push(o))}return e.length>0?e:void 0}function je(n,e){const t={claude:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},codex:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},cursor:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},acp:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},pi:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},codewhale:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},openhuman:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},opencode:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},agy:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0}},r=t[n]??t.acp;return{maxConcurrent:e?.max_concurrent??r.maxConcurrent??1,maxQueued:e?.max_queued??r.maxQueued??3,queueTimeoutMs:e?.queue_timeout_ms??r.queueTimeoutMs??0,cancelableQueued:!0,cancelableRunning:!0}}function Me(n,e={}){const t=oe(),r=String(n.client_type??"").trim().toLowerCase(),a=Ie(r),o=String(n.ws_url??"").trim(),i="get_session_usage",m="get_rate_limits",c="get_agent_global_config",s=ge;if(!n.name?.trim())throw new Error("agent name is required");if(!o)throw new Error(`agent ${n.name}: ws_url is required`);if(!n.agent_id?.trim())throw new Error(`agent ${n.name}: agent_id is required`);if(!n.api_key?.trim())throw new Error(`agent ${n.name}: api_key is required`);const l=a.adapterType,u=l==="acp",w=r==="qwen",d={...a.options??{}};if(n.codex_config_overrides!==void 0){if(l!=="codex")throw new Error(`agent ${n.name}: codex_config_overrides is only supported for client_type=codex`);ke(n.codex_config_overrides,`agent ${n.name}: codex_config_overrides`),d.codexConfigOverrides=n.codex_config_overrides}if(l==="codex"&&n.provider?.wire_api!==void 0){const f=n.provider;if(f.wire_api!=="responses"||!f.base_url?.trim()||!f.api_key?.trim()||!f.model?.trim()||!f.catalog_version?.trim()||!f.catalog_sha256?.trim()||!f.catalog_json?.trim())throw Object.assign(new Error(`agent ${n.name}: incomplete Codex direct relay config`),{code:"INVALID_CODEX_DIRECT_CONFIG"});const M=be(y.data,n.name);d.codexHome=M,d.codexDirect={codexHome:M,config:{baseUrl:f.base_url.trim(),model:f.model.trim(),...f.reasoning_effort?.trim()?{reasoningEffort:f.reasoning_effort.trim()}:{},token:f.api_key.trim()},catalog:{version:f.catalog_version.trim(),sha256:f.catalog_sha256.trim(),json:f.catalog_json}}}const h=l==="codex"?{capabilities:["local_action_v1","agent_invoke"],localActions:["session_control","get_context","set_model","set_mode","set_reasoning_effort","set_service_tier","set_sandbox_mode","exec_approve","exec_reject","file_list","create_folder","turn_interrupt","permission_approve","permission_reject","thread_compact",i,m]}:null,_=l==="claude"?{localActions:["session_control","set_mode","set_model","claude_interaction_reply","exec_approve","exec_reject","file_list","create_folder","thread_compact",i,m]}:null,g=w?{capabilities:["stream_chunk","local_action_v1"],localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",i],adapterHint:"qwen/base"}:null,S=l==="pi"?{adapterHint:"pi/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","get_context","file_list","create_folder",i,m]}:null,v=l==="openhuman"?{adapterHint:"openhuman/base",capabilities:["local_action_v1"],localActions:["session_control","set_model","file_list","create_folder"]}:null,C=l==="cursor"?{adapterHint:"cursor/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","set_mode","get_context","file_list","create_folder","thread_compact","claude_interaction_reply",m]}:null,T=l==="codewhale"?{capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",i]}:null,x=l==="opencode"?{adapterHint:"opencode/base",capabilities:["stream_chunk","local_action_v1"],localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","file_list","create_folder"]}:null,$=l==="agy"?{adapterHint:"agy/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder"]}:null,E=new Set(["hermes","reasonix","kimi","copilot"]),Q=u&&!E.has(r),K=u&&!w?{localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",...Q?[i]:[]]}:null,W=r==="kiro"?{localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder","thread_compact",i,m]}:null,z=l==="codex"||l==="claude"||r==="gemini"?["session_control","set_model","set_mode"]:void 0,G=l==="cursor"||l==="opencode"||l==="openhuman"||l==="agy"||u&&E.has(r)?[m]:[i,m];u&&d.raw_transport===void 0&&(d.raw_transport=r==="gemini");const V=`${r}/base`,I=pe(l,r),J=I==="kiro"?void 0:process.cwd(),R=fe({mode:I,projectDir:J}),P=R.filter(f=>f.source==="connector").map(f=>f.name);P.length>0&&!e.quiet&&p.info("manager",`[${n.name}] injecting connector skills: [${P.join(", ")}]`);const j=me(R,y.skills);let X=j.length>0?j:void 0;const Y=ue({skillsDir:y.skills,mode:I,home:ne()});return{name:n.name,adapterType:l,aibot:{url:o,agentId:n.agent_id,apiKey:n.api_key,clientType:r,clientVersion:t,adapterHint:a.adapterHint??g?.adapterHint??S?.adapterHint??v?.adapterHint??C?.adapterHint??x?.adapterHint??$?.adapterHint??V,capabilities:h?.capabilities??T?.capabilities??S?.capabilities??v?.capabilities??C?.capabilities??x?.capabilities??$?.capabilities??g?.capabilities??["stream_chunk","local_action_v1","connector_upgrade"],localActions:Pe(h?.localActions??T?.localActions??_?.localActions??S?.localActions??v?.localActions??C?.localActions??x?.localActions??$?.localActions??g?.localActions??W?.localActions??K?.localActions??["exec_approve","exec_reject"],z,G,["connector_rollback","connector_upgrade_push",c,"configure_gateway_provider","skill_upload","skill_enable","skill_disable","skill_refresh",...s]),skills:X,librarySkills:Y,terminalOutboxPath:Re(n.name,n.agent_id)},agent:{command:a.command,args:[...a.args??[],...n.args??[]],env:n.env,clientType:r,...n.provider?{provider:{...n.provider.provider_id?.trim()?{providerId:n.provider.provider_id.trim()}:{},...n.provider.base_url?.trim()?{baseUrl:n.provider.base_url.trim()}:{},...n.provider.quota_base_url?.trim()?{quotaBaseUrl:n.provider.quota_base_url.trim()}:{},...n.provider.api_key?.trim()?{apiKey:n.provider.api_key.trim()}:{},...n.provider.model?.trim()?{model:n.provider.model.trim()}:{},...n.provider.fast_model?.trim()?{fastModel:n.provider.fast_model.trim()}:{},...n.provider.effort?.trim()?{effort:n.provider.effort.trim()}:{}}}:{}},adapterOptions:d,acpAuthMethod:d.auth_method,acpInitialMode:d.initial_mode,acpMcpTools:d.acp_mcp_tools,promptTimeoutMs:n.prompt_timeout_ms,bindingsPath:Te(n.name,n.agent_id),activeEventStorePath:Ee(n.name,n.agent_id),...a.enableSessionBinding||u?{enableSessionBinding:!0}:{},...a.autoInjectArgs||r==="reasonix"&&n.provider?.model?.trim()?{autoInjectArgs:{...a.autoInjectArgs??{},...r==="reasonix"&&n.provider?.model?.trim()?{model:n.provider.model.trim()}:{}}}:{},poolMaxSize:n.pool?.maxSize,poolIdleTimeoutMs:n.pool?.idleTimeoutMs,eventQueue:je(l,n.event_queue),logDir:y.log,providerBaseUrl:n.provider_base_url?.trim()||void 0,providerApiKey:n.provider_api_key?.trim()||void 0}}function De(){const n=process.env.GRIX_AGENT_STARTUP_WAIT_MS,e=Number(n);return Number.isFinite(e)&&e>=500?Math.floor(e):Ce}function F(n){const e=[],t=[],r=[],a=ee(n).filter(o=>o.endsWith(".json")).sort();for(const o of a)try{const i=JSON.parse(Z(A(n,o),"utf-8"));if(i===null||typeof i!="object"||Array.isArray(i)||!("agents"in i)){r.push(o);continue}if(!Array.isArray(i.agents)){t.push({file:o,error:"agents is not an array"});continue}if(i.agents.length===0){t.push({file:o,error:"agents array is empty"});continue}for(const m of i.agents)e.push({entry:m,file:o})}catch(i){t.push({file:o,error:i instanceof Error?i.message:String(i)})}return{entries:e,fileErrors:t,skipped:r}}function b(n){if(n===null||typeof n!="object")return JSON.stringify(n)??"null";if(Array.isArray(n))return`[${n.map(b).join(",")}]`;const e=n;return`{${Object.keys(e).sort().map(r=>`${JSON.stringify(r)}:${b(e[r])}`).join(",")}}`}function Oe(n,e){return b(n)===b(e)}class pt{instances=[];configMap=new Map;rawConfigMap=new Map;reloadChain=Promise.resolve();sharedInstances=new Map;shareSyncChains=new Map;stopping=!1;deletedAgentCleanups=new Set;upgradeChecker=null;skillSyncer=null;globalConfigStore;configDir=y.config;sharedOwnersCache=new se(y.data);installer=new ve;relaySecretResolver;auditProducer=Ae();async start(e){const t=e??y.config;this.configDir=t,p.info("manager",`Loading configs from ${t}`),he(),N(),this.globalConfigStore=new de(A(y.data,"agent-global-configs.json")),this.globalConfigStore.load();const{entries:r,fileErrors:a,skipped:o}=F(t);for(const s of a)p.error("manager",`Failed to load ${s.file}: ${s.error}`);for(const s of o)p.info("manager",`Skipped ${s}: not an agent config (no "agents" key)`);if(r.length===0&&a.length===0&&o.length===0){p.warn("manager",`No config files found in ${t}; daemon idle (waiting for config). Add agents under this directory then run: grix-connector reload`);return}const i=[];let m=a.length;for(const{entry:s,file:l}of r)try{const u=this.toRuntimeInstanceConfig(s);i.push({config:u,entry:s,file:l}),p.info("manager",`Loaded ${u.name} (${u.adapterType??"acp"}) from ${l}`)}catch(u){const w=typeof s?.name=="string"?s.name:"<unknown>";p.error("manager",`Invalid agent config in ${l} (name=${w}): ${u}`),m++}let c=0;if(i.length>0){const s=De();p.info("manager",`Starting ${i.length} agent(s), startup wait=${s}ms`);const l=d=>{this.instances=this.instances.filter(h=>h!==d)},u=i.map(({config:d,entry:h,file:_})=>{const g=this.createConfiguredInstance(d);return this.instances.push(g),this.configMap.set(d.name,d),this.rawConfigMap.set(d.name,{entry:h,file:_}),{config:d,instance:g,startPromise:this.prepareProviderRuntime(d,h).then(()=>g.start())}}),w=await Promise.all(u.map(async d=>{const h=await new Promise(_=>{let g=!1;const S=setTimeout(()=>{g||(g=!0,_({kind:"timeout"}))},s);d.startPromise.then(()=>{g||(g=!0,clearTimeout(S),_({kind:"started"}))}).catch(v=>{g||(g=!0,clearTimeout(S),_({kind:"failed",error:v}))})});return{task:d,outcome:h}}));for(const{task:d,outcome:h}of w){if(h.kind==="started"){this.restoreCachedSharedInstances(d.config);continue}if(h.kind==="failed"){if(O(h.error)){this.onAgentDeletedByPlatform(d.config.name);continue}l(d.instance),p.error("manager",`Failed to start ${d.config.name}: ${h.error}`);continue}c++,p.warn("manager",`Startup pending for ${d.config.name}, continue retrying in background`),d.startPromise.then(()=>{p.info("manager",`Delayed start succeeded: ${d.config.name}`),this.restoreCachedSharedInstances(d.config)}).catch(_=>{if(O(_)){this.onAgentDeletedByPlatform(d.config.name);return}l(d.instance),p.error("manager",`Delayed start failed: ${d.config.name}: ${_}`)})}if(this.instances.length>0){const d=Math.max(0,this.instances.length-c);p.info("manager",`${d}/${i.length} agent(s) running now`)}c>0&&p.warn("manager",`${c} agent(s) still connecting in background`)}if(this.instances.length===0&&i.length>0)throw new Error("All agent configurations failed to start");await this.ensureAuxServices()}async ensureAuxServices(){if(this.configMap.size===0)return;const e=[...this.configMap.values()].map(t=>({apiKey:t.aibot.apiKey,wsUrl:t.aibot.url}));this.upgradeChecker||(this.upgradeChecker=new ce(e,()=>this.instances.some(t=>t.hasPendingOrBackgroundWork())||[...this.sharedInstances.values()].some(t=>t.hasPendingOrBackgroundWork())),await this.upgradeChecker.start()),this.skillSyncer||(this.skillSyncer=new le(e,y.skills),this.skillSyncer.setOnSyncSuccess(()=>this.refreshAllInstanceSkills()),await this.skillSyncer.start())}refreshAllInstanceSkills(){for(const e of this.instances)try{e.forceRefreshSkills()}catch(t){p.warn("manager",`forceRefreshSkills failed for ${e.name}: ${t}`)}for(const e of this.sharedInstances.values())try{e.forceRefreshSkills()}catch(t){p.warn("manager",`forceRefreshSkills failed for shared instance: ${t}`)}}async stop(){p.info("manager","Stopping all agents..."),this.stopping=!0,this.upgradeChecker?.stop(),this.skillSyncer?.stop(),await Promise.allSettled([...this.shareSyncChains.values()]),this.shareSyncChains.clear();const e=[...this.sharedInstances.values()];this.sharedInstances.clear(),await Promise.allSettled([...this.instances.map(t=>t.stop()),...e.map(t=>t.stop())]),await this.globalConfigStore?.flush(),await this.auditProducer.stop({timeoutMs:5e3}).catch(()=>!1),this.instances=[],N(),p.info("manager","All stopped")}onShareSet(e,t){this.sharedOwnersCache.save(e.name,t);const a=(this.shareSyncChains.get(e.name)??Promise.resolve()).catch(()=>{}).then(()=>this.syncSharedInstances(e,t));this.shareSyncChains.set(e.name,a)}async syncSharedInstances(e,t){if(this.stopping)return;const{toAdd:r,toRemove:a}=ae(e.name,this.sharedInstances.keys(),t);for(const o of r){if(this.stopping)break;const i=ie(e.name,o);try{const m=re(e,o),c=new D(m,this.globalConfigStore,this.auditProducer);this.sharedInstances.set(i,c),await c.start(),p.info("manager",`shared instance started: ${i}`)}catch(m){this.sharedInstances.delete(i);const c=m instanceof Error?m.message:String(m);/Auth failed/i.test(c)?(this.sharedOwnersCache.remove(e.name,o),p.warn("manager",`shared instance auth rejected, removed from cache: ${i}`)):p.error("manager",`start shared instance failed ${i}: ${m}`)}}for(const{key:o}of a){const i=this.sharedInstances.get(o);i&&(this.sharedInstances.delete(o),i.stop().catch(m=>p.error("manager",`stop shared instance failed ${o}: ${m}`)),p.info("manager",`shared instance stopped: ${o}`))}}restoreCachedSharedInstances(e){const t=this.sharedOwnersCache.load(e.name);t.length!==0&&(p.info("manager",`restoring ${t.length} cached shared instance(s) for ${e.name}`),this.onShareSet(e,t))}getAgentsStatus(){return this.instances.map(e=>e.getStatus())}async addAgent(e){await this.addAgentInternal(e,"agents.json"),this.persistAgentsConfig(),await this.ensureAuxServices(),p.info("manager",`Added agent: ${e.name}`)}async addAgentInternal(e,t){const r=this.toRuntimeInstanceConfig(e);if(this.instances.some(a=>a.name===r.name))throw new Error(`Agent "${r.name}" already exists`);await this.startInstanceFromConfig(r,e,t)}createConfiguredInstance(e){const t=new D(e,this.globalConfigStore,this.auditProducer);return t.setUpgradeTrigger(()=>this.upgradeChecker?.triggerCheck()),t.setShareSetHandler(r=>this.onShareSet(e,r)),t.setAgentDeletedHandler(()=>this.onAgentDeletedByPlatform(e.name)),t.setSkillSyncHandler(()=>this.skillSyncer?.triggerSync()),t.setProviderConfigHandler(r=>this.setAgentProvider(e.name,r)),t.setRelayStateApplyPorts({getLocalState:()=>this.relayStateApplier?.getLocalState(e.name)??{enabled:!1},applyEnable:(r,a)=>{const o=this.relayStateApplier;return o?o.applyEnable(e.name,r,a):Promise.reject(new Error("relay state applier is not configured"))},applyDisable:()=>{const r=this.relayStateApplier;return r?r.applyDisable(e.name):Promise.reject(new Error("relay state applier is not configured"))}}),t.setRelayEnvSettledHandler(()=>this.relayEnvSettledHandler?.()),t.setProviderResolver(()=>this.getAgentProvider(e.name)),t}async prepareProviderRuntime(e,t){const r=this.resolveAgentProvider(t.provider);q(t.client_type)&&r?.base_url?.trim()&&r?.api_key?.trim()&&r?.model?.trim()&&await H(e.name,{baseUrl:r.base_url.trim(),apiKey:r.api_key.trim(),model:r.model.trim()})}async startInstanceFromConfig(e,t,r){await this.prepareProviderRuntime(e,t);const a=this.createConfiguredInstance(e);await a.start(),this.instances.push(a),this.configMap.set(e.name,e),this.rawConfigMap.set(e.name,{entry:t,file:r}),this.relayEnvSettledHandler?.()}async removeAgent(e){if(!this.instances.some(t=>t.name===e))throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});await this.removeAgentInternal(e),this.persistAgentsConfig(),p.info("manager",`Removed agent: ${e}`),this.relayEnvSettledHandler?.()}async removeAgentInternal(e,t){const r=this.instances.findIndex(m=>m.name===e),a=r===-1?void 0:this.instances[r];r!==-1&&this.instances.splice(r,1),this.configMap.delete(e),this.rawConfigMap.delete(e);const o=this.shareSyncChains.get(e);this.shareSyncChains.delete(e),o&&await o.catch(()=>{});const i=`${e}#shared:`;for(const[m,c]of[...this.sharedInstances.entries()])m.startsWith(i)&&(this.sharedInstances.delete(m),c.stop().catch(s=>p.error("manager",`stop shared instance failed ${m}: ${s}`)));t?.keepShareCache||this.sharedOwnersCache.delete(e),a&&await a.stop(),t?.keepShareCache||await L(e).catch(()=>{})}onAgentDeletedByPlatform(e){this.stopping||this.deletedAgentCleanups.has(e)||(this.deletedAgentCleanups.add(e),(async()=>{try{if(!(this.rawConfigMap.has(e)||this.instances.some(r=>r.name===e)))return;p.warn("manager",`Agent "${e}" \u5DF2\u5728\u5E73\u53F0\u5220\u9664\uFF0C\u505C\u6B62\u5B9E\u4F8B\u5E76\u6E05\u7406\u672C\u5730\u914D\u7F6E`),await this.removeAgentInternal(e),this.persistAgentsConfig(),p.info("manager",`Agent "${e}" \u5DF2\u5728\u5E73\u53F0\u5220\u9664\uFF0C\u672C\u5730\u914D\u7F6E\u5DF2\u6E05\u7406\uFF08agents.json \u5DF2\u79FB\u9664\u8BE5\u6761\u76EE\uFF09`)}catch(t){p.error("manager",`cleanup for deleted agent "${e}" failed: ${t}`)}finally{this.deletedAgentCleanups.delete(e)}})())}persistAgentsConfig(){const e=A(this.configDir,"agents.json");try{const t=[];for(const[,a]of this.configMap){const o=this.rawConfigMap.get(a.name)?.entry.provider;t.push({name:a.name,ws_url:a.aibot.url,agent_id:a.aibot.agentId,api_key:a.aibot.apiKey,client_type:a.aibot.clientType,...o?{provider:o}:{}})}te(e,JSON.stringify({agents:t},null,4)+`
2
+ `,"utf-8")}catch(t){p.error("manager",`Failed to persist agents config: ${t}`)}}isAgentBusy(e){return this.instances.find(r=>r.name===e)?.hasPendingWork()??!1}getAgentsWithStaleRelayEnv(){return this.instances.filter(e=>e.hasStaleRelayEnv()).map(e=>e.name)}markAgentRelayEnvStale(e){const t=this.instances.find(r=>r.name===e);return t?(t.markRelayEnvStale(),!0):!1}relayEnvSettledHandler;setRelayEnvSettledHandler(e){this.relayEnvSettledHandler=e}relayStateApplier;setRelayStateApplier(e){this.relayStateApplier=e}reportRelayStateLocalChange(e){this.instances.find(r=>r.name===e)?.reportRelayStateLocalChange()}setRelaySecretResolver(e){this.relaySecretResolver=e}resolveAgentProvider(e){if(!e?.api_key_ref?.trim())return e;const t=e.api_key_ref.trim(),r=this.relaySecretResolver?.(t);if(!r)throw Object.assign(new Error(`relay credential "${t}" is unavailable`),{code:"RELAY_SECRET_UNAVAILABLE"});return{...e,api_key:r}}toRuntimeInstanceConfig(e,t={}){const r=this.resolveAgentProvider(e.provider);return Me(r===e.provider?e:{...e,provider:r},t)}getAgentProvider(e){return this.resolveAgentProvider(this.rawConfigMap.get(e)?.entry.provider)}async restartAgent(e){const t=this.rawConfigMap.get(e);if(!t)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});await this.replaceInstance(e,t.entry,t.file),p.info("manager",`Restarted agent: ${e}`)}async setAgentProvider(e,t,r){const a=this.rawConfigMap.get(e);if(!a)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});const o=String(a.entry.client_type??"").trim().toLowerCase();q(o)&&(t?.base_url?.trim()&&t?.api_key?.trim()&&t?.model?.trim()?await H(e,{baseUrl:t.base_url.trim(),apiKey:t.api_key.trim(),model:t.model.trim()}):await L(e));const i=t?.api_key_ref?.trim()?Object.fromEntries(Object.entries(t).filter(([c])=>c!=="api_key")):t,m={...a.entry,provider:i};if(r?.deferRestart){this.rawConfigMap.set(e,{...a,entry:m}),this.instances.find(c=>c.name===e)?.markRelayEnvStale(),this.persistAgentsConfig(),p.info("manager",`Updated provider config for agent (deferred restart): ${e}`);return}await this.replaceInstance(e,m,a.file),this.persistAgentsConfig(),p.info("manager",`Updated provider config for agent: ${e}`)}hasDirectRelayConfig(e){const t=this.rawConfigMap.get(e)?.entry.provider;return!!(t?.api_key_ref?.trim()||t?.api_key?.trim())}getDirectRelayConfigAgentNames(){return[...this.rawConfigMap.entries()].filter(([,e])=>!!e.entry.provider&&(_e(e.entry.client_type)||["claude","codex"].includes(String(e.entry.client_type??"").trim().toLowerCase()))).map(([e])=>e)}async fetchRelayCredential(e,t){const r=this.instances.find(a=>a.name===e);if(!r)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});return r.fetchRelayCredential(t)}async clearDirectProviderConfigs(){const e=this.getDirectRelayConfigAgentNames(),t=[];for(const r of e)try{await this.setAgentProvider(r,void 0,{deferRestart:this.isAgentBusy(r)}),t.push(r)}catch(a){p.warn("manager",`Failed to clear provider config for "${r}": ${a instanceof Error?a.message:String(a)}`)}return t}async replaceInstance(e,t,r){const a=this.toRuntimeInstanceConfig(t);await this.removeAgentInternal(e,{keepShareCache:!0}),await this.startInstanceFromConfig(a,t,r),this.restoreCachedSharedInstances(a)}async reload(){const e=this.reloadChain.catch(()=>{}).then(()=>this.doReload());return this.reloadChain=e.catch(()=>{}),e}async doReload(){if(this.stopping)throw Object.assign(new Error("manager is stopping"),{code:"RELOAD_UNSAFE"});const{entries:e,fileErrors:t,skipped:r}=F(this.configDir);for(const c of r)p.info("manager",`reload: skipped ${c}: not an agent config (no "agents" key)`);if(t.length>0){const c=t.map(s=>`${s.file}: ${s.error}`).join("; ");throw Object.assign(new Error(`reload aborted: ${t.length} config file(s) failed to parse [${c}]`),{code:"RELOAD_UNSAFE"})}if(e.length===0)throw Object.assign(new Error("reload aborted: no valid agent config found"),{code:"RELOAD_UNSAFE"});const a=[];for(const{entry:c}of e)try{this.toRuntimeInstanceConfig(c,{quiet:!0})}catch(s){const l=c?.name,u=typeof l=="string"&&l.trim()?l:"<unknown>";a.push({name:u,error:s instanceof Error?s.message:String(s)})}if(a.length>0){const c=a.map(s=>`${s.name}: ${s.error}`).join("; ");throw Object.assign(new Error(`reload aborted: ${a.length} agent config(s) invalid [${c}]`),{code:"RELOAD_UNSAFE"})}const o=new Map;for(const c of e){const s=String(c.entry?.name??"").trim();s&&(o.has(s)&&p.warn("manager",`reload: duplicate agent name "${s}", last one wins`),o.set(s,c))}const i={added:[],removed:[],restarted:[],unchanged:[],failed:[]},m=new Set(this.rawConfigMap.keys());for(const c of m)if(!o.has(c))try{await this.removeAgentInternal(c),i.removed.push(c)}catch(s){i.failed.push({name:c,error:s instanceof Error?s.message:String(s)})}for(const[c,s]of o){const l=this.rawConfigMap.get(c);if(l)if(Oe(l.entry,s.entry))this.rawConfigMap.set(c,s),i.unchanged.push(c);else try{await this.replaceInstance(c,s.entry,s.file),i.restarted.push(c)}catch(u){i.failed.push({name:c,error:u instanceof Error?u.message:String(u)})}else try{await this.addAgentInternal(s.entry,s.file),i.added.push(c)}catch(u){i.failed.push({name:c,error:u instanceof Error?u.message:String(u)})}}return p.info("manager",`reload done: +${i.added.length} -${i.removed.length} ~${i.restarted.length} =${i.unchanged.length} !${i.failed.length}`),await this.ensureAuxServices(),i}async checkUpgrade(){return this.upgradeChecker?this.upgradeChecker.checkForUpdate():{available:!1}}triggerUpgrade(){this.upgradeChecker?.triggerCheck()}async probeAll(e={}){return Ne(this.instances,e)}async probeOne(e,t={}){return qe(this.instances,e,t)}listInstallable(){return this.installer.listInstallable()}async installAgent(e){const t=await this.installer.install(e);return Se(t),t}getInstallProgress(e){return this.installer.getProgress(e)??null}}async function Ne(n,e){const t=e.concurrency??4,r=Date.now(),a=new Array(n.length);await new Promise(s=>{let l=0,u=0;const w=n.length;if(w===0){s();return}function d(g){const S=n[g];S.probe(e).then(v=>{a[g]=v,h()},v=>{a[g]={agent_name:S.name,client_type:"unknown",adapter_type:"acp",ok:!1,status:"error",probed_at:Date.now(),duration_ms:0,cached:!1,cli:{command:"",installed:!1,path:null,version:null,error:{code:"internal",message:v?.message??String(v)}},conversation:{attempted:!1,ok:!1,latency_ms:null},config:{model:null,base_url:null,source:{model:"unknown",base_url:"unknown"}},process:{started:!1,alive:!1,busy:!1}},h()})}function h(){u++,l<w?d(l++):u===w&&s()}const _=Math.min(t,w);for(let g=0;g<_;g++)d(l++)});const o=a.filter(s=>s.status==="healthy").length,i=a.filter(s=>s.status==="degraded").length,m=a.filter(s=>s.status==="unavailable").length,c=await $e();return{ok:o===a.length&&a.length>0,total:a.length,healthy:o,degraded:i,unavailable:m,installed_clients:c,agents:a,probed_at:r,duration_ms:Date.now()-r}}async function qe(n,e,t){const r=n.find(a=>a.name===e);if(!r)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});return r.probe(t)}export{pt as Manager,$e as probeInstalledClientCommands,Ne as probeInstances,qe as probeOneInstance};
@@ -1 +1 @@
1
- function a(t){const o=new Set([`http://127.0.0.1:${t.serverPort}`,`http://localhost:${t.serverPort}`,...t.allowedOrigins]),e=new Set([`127.0.0.1:${t.serverPort}`,`localhost:${t.serverPort}`,...t.allowedHosts]);return{validateRequest(s){const r=i(s,o);if(!r.ok)return r;const n=l(s,e);return n.ok?{ok:!0}:n}}}function i(t,o){const e=t.headers.origin;return e?o.has(e)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${e}`}:{ok:!0}}function l(t,o){const e=t.headers.host;return e?o.has(e)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${e}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
1
+ function a(e){const t=new Set([`http://127.0.0.1:${e.serverPort}`,`http://localhost:${e.serverPort}`,...e.allowedOrigins]),o=new Set([`127.0.0.1:${e.serverPort}`,`localhost:${e.serverPort}`,...e.allowedHosts]);return{validateRequest(s){const r=i(s,t);if(!r.ok)return r;const n=l(s,o);return n.ok?{ok:!0}:n}}}function i(e,t){const o=e.headers.origin;return o?t.has(o)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${o}`}:{ok:!0}}function l(e,t){const o=e.headers.host;return o?t.has(o)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${o}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
@@ -9643,6 +9643,23 @@ var TOOLS = [
9643
9643
  }
9644
9644
  }
9645
9645
  },
9646
+ {
9647
+ name: "grix_widget_visitor_ban",
9648
+ description: "Ban a widget visitor session: blocks the session and adds its most recent init IP to the owner's global IP ban list (7-day expiry).",
9649
+ inputSchema: {
9650
+ type: "object",
9651
+ properties: {
9652
+ session_id: { type: "string", description: "Widget visitor session ID (required)." }
9653
+ },
9654
+ required: ["session_id"]
9655
+ },
9656
+ validation: {
9657
+ required: ["session_id"],
9658
+ properties: {
9659
+ session_id: { type: "string" }
9660
+ }
9661
+ }
9662
+ },
9646
9663
  {
9647
9664
  name: "grix_skill_set",
9648
9665
  description: "Create, update, or delete one of your owner's custom skills in the platform skill library. The skill library auto-syncs to every machine the owner runs an agent on (a per-machine `grix/skills` directory), so defining a skill here makes it available on all of them. A skill is a plain SKILL.md package (frontmatter + body), same standard as other Grix skills. Provide `name` and the full `content` (the SKILL.md text) to create or overwrite by name. To delete a skill, pass its `name` with an empty `content` string. How and when any agent actually loads/uses a synced skill is arranged by the owner (in a system prompt or in conversation); this tool only manages the library.",
@@ -9794,6 +9811,7 @@ var TOOL_SKILL_MAP = {
9794
9811
  grix_chat_state_update: "grix-chat-state",
9795
9812
  grix_status: "grix-chat-state",
9796
9813
  grix_access_control: "grix-access-control",
9814
+ grix_widget_visitor_ban: "grix-widget-visitor-ban",
9797
9815
  grix_file_link: "tailnet-file-share"
9798
9816
  };
9799
9817
  function skillGuidance(skill) {
@@ -9872,6 +9890,8 @@ function toolCallToInvoke(toolName, args) {
9872
9890
  return buildChatStateUpdateInvoke(args);
9873
9891
  case "grix_access_control":
9874
9892
  return buildAccessControlInvoke(args);
9893
+ case "grix_widget_visitor_ban":
9894
+ return buildWidgetVisitorBanInvoke(args);
9875
9895
  case "grix_skill_set":
9876
9896
  return buildSkillSetInvoke(args);
9877
9897
  case "grix_skill_get":
@@ -10024,6 +10044,9 @@ function buildAccessControlInvoke(args) {
10024
10044
  if (args.policy != null) payload.policy = args.policy;
10025
10045
  return { action: "claude_access_control", params: { verb, payload }, timeoutMs: 3e4 };
10026
10046
  }
10047
+ function buildWidgetVisitorBanInvoke(args) {
10048
+ return { action: "widget_visitor_ban", params: { session_id: args.session_id } };
10049
+ }
10027
10050
  function buildChatStateUpdateInvoke(args) {
10028
10051
  const params = {
10029
10052
  session_id: args.session_id,
@@ -10071,6 +10094,7 @@ var PHASE1_INVOKE_ACTIONS = /* @__PURE__ */ new Set([
10071
10094
  "chat_state_query",
10072
10095
  "chat_state_update",
10073
10096
  "claude_access_control",
10097
+ "widget_visitor_ban",
10074
10098
  "skill_set",
10075
10099
  "skill_get"
10076
10100
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grix-connector",
3
- "version": "3.19.7",
3
+ "version": "3.20.0",
4
4
  "description": "Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,6 +30,7 @@
30
30
  "test:watch": "node --experimental-vm-modules node_modules/vitest/vitest.mjs",
31
31
  "test:e2e": "RUN_REAL_AGENT_E2E=1 node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e",
32
32
  "test:e2e:smoke": "RUN_REAL_AGENT_E2E=1 node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e/smoke.test.ts",
33
+ "test:e2e:direct-relay": "RUN_REAL_AGENT_E2E=1 node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e/direct-relay-live.test.ts",
33
34
  "test:e2e:codex-developer-instructions": "RUN_REAL_AGENT_E2E=1 node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e/codex-developer-instructions-protocol.test.ts",
34
35
  "test:e2e:claude-audit": "RUN_REAL_AGENT_E2E=1 node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e/claude-audit-replay.test.ts",
35
36
  "test:e2e:codex-audit": "RUN_REAL_AGENT_E2E=1 node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e/codex-audit-replay.test.ts",
@@ -15,8 +15,14 @@ if (process.platform === 'win32') {
15
15
  const GRIX_HOME = process.env.GRIX_CONNECTOR_HOME || join(homedir(), '.grix');
16
16
  const GUARDIAN_DEST = join(GRIX_HOME, 'bin', 'upgrade-guardian.sh');
17
17
 
18
- const INIT_CWD = process.env.INIT_CWD || resolve(import.meta.dirname, '..');
19
- const GUARDIAN_SRC = join(INIT_CWD, 'scripts', 'upgrade-guardian.sh');
18
+ // npm 的 INIT_CWD 是“调用 npm 的工作目录”,自动升级时通常是用户 cwd,
19
+ // 并不是新包目录。优先从当前 postinstall 脚本所在 package 取 guardian。
20
+ const PACKAGE_ROOT = resolve(import.meta.dirname, '..');
21
+ const packageGuardian = join(PACKAGE_ROOT, 'scripts', 'upgrade-guardian.sh');
22
+ const initCwdGuardian = process.env.INIT_CWD
23
+ ? join(process.env.INIT_CWD, 'scripts', 'upgrade-guardian.sh')
24
+ : '';
25
+ const GUARDIAN_SRC = existsSync(packageGuardian) ? packageGuardian : initCwdGuardian;
20
26
 
21
27
  if (!existsSync(GUARDIAN_SRC)) {
22
28
  process.exit(0);
@@ -8,11 +8,10 @@ set -e
8
8
  GRIX_HOME="${GRIX_CONNECTOR_HOME:-$HOME/.grix}"
9
9
  GUARDIAN_DEST="$GRIX_HOME/bin/upgrade-guardian.sh"
10
10
 
11
- # Locate source: INIT_CWD is set by npm to the package root during lifecycle scripts
12
- GUARDIAN_SRC="${INIT_CWD:-$(dirname "$0")}/scripts/upgrade-guardian.sh"
13
- if [ ! -f "$GUARDIAN_SRC" ]; then
14
- # Fallback: try relative to this script
15
- GUARDIAN_SRC="$(cd "$(dirname "$0")" && pwd)/upgrade-guardian.sh"
11
+ # INIT_CWD 是调用 npm 的工作目录,不保证是 package root。优先使用脚本自身目录。
12
+ GUARDIAN_SRC="$(cd "$(dirname "$0")" && pwd)/upgrade-guardian.sh"
13
+ if [ ! -f "$GUARDIAN_SRC" ] && [ -n "$INIT_CWD" ]; then
14
+ GUARDIAN_SRC="$INIT_CWD/scripts/upgrade-guardian.sh"
16
15
  fi
17
16
 
18
17
  if [ ! -f "$GUARDIAN_SRC" ]; then
@@ -44,6 +44,8 @@ TARGET_VERSION=$(cat "$PENDING_FILE" | grep -o '"target_version":"[^"]*"' | cut
44
44
  UPGRADED_AT=$(cat "$PENDING_FILE" | grep -o '"upgraded_at":"[^"]*"' | cut -d'"' -f4)
45
45
  CRASH_COUNT=$(cat "$PENDING_FILE" | grep -o '"crash_count":[0-9]*' | cut -d: -f2)
46
46
  CRASH_COUNT=${CRASH_COUNT:-0}
47
+ PACKAGE_DIR=$(cat "$PENDING_FILE" | grep -o '"package_dir":"[^"]*"' | cut -d'"' -f4)
48
+ BACKUP_DIR=$(cat "$PENDING_FILE" | grep -o '"backup_dir":"[^"]*"' | cut -d'"' -f4)
47
49
 
48
50
  # Validate pending file
49
51
  if [ -z "$FROM_VERSION" ]; then
@@ -85,6 +87,29 @@ CRASH_COUNT=$((CRASH_COUNT + 1))
85
87
  log "healthz timeout, crash_count=$CRASH_COUNT"
86
88
 
87
89
  if [ $CRASH_COUNT -ge $MAX_CRASHES ]; then
90
+ # 新版升级器会把完整旧包保留在同文件系统的固定备份目录。优先原子恢复,
91
+ # 不依赖网络/npm,也不会再次制造全局 bin 缺失窗口。
92
+ case "$PACKAGE_DIR" in
93
+ */node_modules/*)
94
+ if [ -n "$BACKUP_DIR" ] && [ "$BACKUP_DIR" = "${PACKAGE_DIR}.grix-upgrade-backup" ] && [ -d "$BACKUP_DIR" ]; then
95
+ FAILED_DIR="${PACKAGE_DIR}.grix-upgrade-failed-$$"
96
+ log "atomic rollback: restoring $BACKUP_DIR"
97
+ rm -rf "$FAILED_DIR"
98
+ if { [ ! -e "$PACKAGE_DIR" ] || mv "$PACKAGE_DIR" "$FAILED_DIR"; } \
99
+ && mv "$BACKUP_DIR" "$PACKAGE_DIR"; then
100
+ rm -rf "$FAILED_DIR"
101
+ rm -f "$PENDING_FILE"
102
+ log "atomic rollback succeeded"
103
+ exit 0
104
+ fi
105
+ log "atomic rollback failed; restoring current package before npm fallback"
106
+ if [ ! -e "$PACKAGE_DIR" ] && [ -e "$FAILED_DIR" ]; then
107
+ mv "$FAILED_DIR" "$PACKAGE_DIR" 2>/dev/null || true
108
+ fi
109
+ fi
110
+ ;;
111
+ esac
112
+
88
113
  log "rollback: installing ${NPM_PACKAGE}@${FROM_VERSION}"
89
114
  # P5: 先用默认源,失败再显式回退官方 npm(默认源可能是淘宝镜像,旧版若没同步同样会失败)。
90
115
  if npm install -g "${NPM_PACKAGE}@${FROM_VERSION}" --prefer-online --no-audit --no-fund >> "$LOG_FILE" 2>&1 \
@@ -99,6 +124,10 @@ if [ $CRASH_COUNT -ge $MAX_CRASHES ]; then
99
124
  fi
100
125
 
101
126
  # Under threshold — update crash_count and exit, let process manager restart
102
- echo "{\"from_version\":\"$FROM_VERSION\",\"target_version\":\"$TARGET_VERSION\",\"upgraded_at\":\"$UPGRADED_AT\",\"crash_count\":$CRASH_COUNT}" > "$PENDING_FILE.tmp"
127
+ PATH_FIELDS=""
128
+ if [ -n "$PACKAGE_DIR" ] && [ -n "$BACKUP_DIR" ]; then
129
+ PATH_FIELDS=",\"package_dir\":\"$PACKAGE_DIR\",\"backup_dir\":\"$BACKUP_DIR\""
130
+ fi
131
+ echo "{\"from_version\":\"$FROM_VERSION\",\"target_version\":\"$TARGET_VERSION\",\"upgraded_at\":\"$UPGRADED_AT\",\"crash_count\":$CRASH_COUNT$PATH_FIELDS}" > "$PENDING_FILE.tmp"
103
132
  mv "$PENDING_FILE.tmp" "$PENDING_FILE"
104
133
  log "updated crash_count to $CRASH_COUNT, exiting"