@typhons/sandbox-tools 0.5.5 → 0.5.8

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,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{execSync as r}from"child_process";import f from"fs";import{setTimeout as x}from"timers/promises";var u=30,g=1,b=100,w=!1,v=!1,d=process.argv.slice(2);for(let e=0;e<d.length;e++)d[e]==="--json"?w=!0:d[e]==="--threshold"?u=parseInt(d[++e],10):d[e]==="--daemon"&&(v=!0);function _(e){try{if(e==="claude")return r("pgrep -x claude 2>/dev/null | head -1",{encoding:"utf8"}).trim()||null;if(e==="codex"){let t=r("pgrep -f 'node.*codex' 2>/dev/null",{encoding:"utf8"}).trim().split(`
3
- `);for(let n of t)if(n)try{let o=f.readFileSync(`/proc/${n}/cmdline`,"utf8").replace(/\0/g," ");if(/codex\.js|codex\/bin|@openai\/codex/.test(o))return n}catch{}}}catch{}return null}function T(){try{let e=r('find /home -name "*.jsonl" -path "*/.claude/projects/*" ! -path "*/subagents/*" -printf "%T@ %p\\n" 2>/dev/null | sort -rn | head -1',{encoding:"utf8"}).trim();if(e){let t=parseInt(e.split(" ")[0],10);return Math.floor(Date.now()/1e3)-t}}catch{}return-1}function E(){let e=-1,t=["/home/*/.codex/log/codex-tui.log","/root/.codex/log/codex-tui.log"];for(let n of t)try{let o=r(`ls ${n} 2>/dev/null`,{encoding:"utf8"}).trim().split(`
4
- `);for(let c of o)if(c)try{let a=f.statSync(c),l=Math.floor(Date.now()/1e3)-Math.floor(a.mtimeMs/1e3);(e===-1||l<e)&&(e=l)}catch{}}catch{}return e}function $(e){if(!e)return 0;let t=0;try{let n=r(`pgrep -P ${e} 2>/dev/null`,{encoding:"utf8"}).trim().split(`
5
- `);for(let o of n)if(o)try{if(f.readFileSync(`/proc/${o}/cmdline`,"utf8").replace(/\0/g," ").includes("check-claude-active"))continue;t++}catch{t++}}catch{}return t}function s(e){try{let n=f.readFileSync(`/proc/${e}/io`,"utf8").match(/^rchar:\s*(\d+)/m);return n?parseInt(n[1],10):0}catch{return 0}}async function A(e){if(!e)return 0;let t=s(e);return await x(g*1e3),s(e)-t}async function I(){let e=_("claude"),t=_("codex"),n=T(),o=E(),c=n>=0&&n<u||o>=0&&o<u,a=$(e),l=$(t),y=a>0||l>0,i=0;if(e&&t){let O=s(e),S=s(t);await x(g*1e3);let N=s(e),D=s(t);i=Math.max(N-O,D-S)}else e?i=await A(e):t&&(i=await A(t));let h=i>b,p=c||y||h,m=e&&t?"both":e?"claude":t?"codex":"none";return w||v?console.log(JSON.stringify({active:p,agent:m,claude_pid:e?parseInt(e):null,codex_pid:t?parseInt(t):null,claude_jsonl_age:n,codex_log_age:o,log_active:c,children:a+l,io_delta:i,io_active:h})):!e&&!t?console.log("Agents: not running"):p?(console.log(`Agent: ACTIVE (${m})`),c&&(n>=0&&n<u&&console.log(` Claude JSONL written ${n}s ago`),o>=0&&o<u&&console.log(` Codex log written ${o}s ago`)),y&&console.log(` ${a} claude + ${l} codex child process(es)`),h&&console.log(` I/O: ${i} bytes read in ${g}s`)):(console.log(`Agent: idle (${m} running, waiting for user)`),e&&console.log(` Claude JSONL last written ${n}s ago`),t&&console.log(` Codex log last written ${o}s ago`),console.log(" No child processes"),console.log(` I/O: ${i} bytes in ${g}s`)),p}function C(){try{return f.readFileSync("/run/e2b/.DEVBOX_NAME","utf8").trim()}catch{}return""}if(v){let e=process.env.E2B_HTTP_DOMAIN||"d.typhons.dev";for(console.log(`[agent-keepalive] domain=${e} interval=30s`);;){if(await I()){let n=C();if(n){let o=`https://${n}.${e}:49999/`;try{r(`curl -sf -o /dev/null -w '%{http_code}' --max-time 5 "${o}" 2>/dev/null`,{encoding:"utf8"})}catch{}}else console.log("[agent-keepalive] no devbox name available yet, skipping ping")}await x(29e3)}}else{let e=await I();process.exit(e?0:1)}
2
+ import{execSync as g}from"child_process";import f from"fs";import{setTimeout as D}from"timers/promises";import{execSync as b}from"child_process";function m(e){let t="";try{t=b(`pgrep -x ${e} 2>/dev/null`,{encoding:"utf8"})}catch{}return t.split(`
3
+ `).map(o=>o.trim()).filter(Boolean)}var a=30,x=1,T=100,M=!1,v=!1,r=process.argv.slice(2);for(let e=0;e<r.length;e++)r[e]==="--json"?M=!0:r[e]==="--threshold"?a=parseInt(r[++e],10):r[e]==="--daemon"&&(v=!0);function E(){try{let e=g('find /home -name "*.jsonl" -path "*/.claude/projects/*" ! -path "*/subagents/*" -printf "%T@ %p\\n" 2>/dev/null | sort -rn | head -1',{encoding:"utf8"}).trim();if(e){let t=parseInt(e.split(" ")[0],10);return Math.floor(Date.now()/1e3)-t}}catch{}return-1}function B(){let e=-1,t=["/home/*/.codex/log/codex-tui.log","/root/.codex/log/codex-tui.log"];for(let o of t)try{let n=g(`ls ${o} 2>/dev/null`,{encoding:"utf8"}).trim().split(`
4
+ `);for(let l of n)if(l)try{let i=f.statSync(l),s=Math.floor(Date.now()/1e3)-Math.floor(i.mtimeMs/1e3);(e===-1||s<e)&&(e=s)}catch{}}catch{}return e}function O(e){let t=0;for(let o of e)try{let n=g(`pgrep -P ${o} 2>/dev/null`,{encoding:"utf8"}).trim().split(`
5
+ `);for(let l of n)if(l)try{if(f.readFileSync(`/proc/${l}/cmdline`,"utf8").replace(/\0/g," ").includes("check-claude-active"))continue;t++}catch{t++}}catch{}return t}function w(e){try{let o=f.readFileSync(`/proc/${e}/io`,"utf8").match(/^rchar:\s*(\d+)/m);return o?parseInt(o[1],10):0}catch{return 0}}async function N(){let e=m("claude"),t=m("codex"),o=E(),n=B(),l=o>=0&&o<a||n>=0&&n<a,i=O(e),s=O(t),$=i>0||s>0,y=[...e,...t],_=y.map(w);await D(x*1e3);let A=y.map(w),I=0,S=0;for(let c=0;c<e.length;c++)I+=Math.max(0,A[c]-_[c]);for(let c=0;c<t.length;c++)S+=Math.max(0,A[e.length+c]-_[e.length+c]);let d=Math.max(I,S),u=d>T,h=l||$||u,p=e.length&&t.length?"both":e.length?"claude":t.length?"codex":"none";return M||v?console.log(JSON.stringify({active:h,agent:p,claude_pids:e.map(c=>parseInt(c,10)),codex_pids:t.map(c=>parseInt(c,10)),claude_jsonl_age:o,codex_log_age:n,log_active:l,children:i+s,io_delta:d,io_active:u})):!e.length&&!t.length?console.log("Agents: not running"):h?(console.log(`Agent: ACTIVE (${p}; ${e.length} claude + ${t.length} codex)`),l&&(o>=0&&o<a&&console.log(` Claude JSONL written ${o}s ago`),n>=0&&n<a&&console.log(` Codex log written ${n}s ago`)),$&&console.log(` ${i} claude + ${s} codex child process(es)`),u&&console.log(` I/O: ${d} bytes read in ${x}s`)):(console.log(`Agent: idle (${p}; ${e.length} claude + ${t.length} codex running, waiting for user)`),e.length&&console.log(` Claude JSONL last written ${o}s ago`),t.length&&console.log(` Codex log last written ${n}s ago`),console.log(" No child processes"),console.log(` I/O: ${d} bytes in ${x}s`)),h}function C(){try{return f.readFileSync("/run/e2b/.DEVBOX_NAME","utf8").trim()}catch{}return""}if(v){let e=process.env.E2B_HTTP_DOMAIN||"d.typhons.dev";for(console.log(`[agent-keepalive] domain=${e} interval=30s`);;){if(await N()){let o=C();if(o){let n=`https://${o}.${e}:49999/`;try{g(`curl -sf -o /dev/null -w '%{http_code}' --max-time 5 "${n}" 2>/dev/null`,{encoding:"utf8"})}catch{}}else console.log("[agent-keepalive] no devbox name available yet, skipping ping")}await D(29e3)}}else{let e=await N();process.exit(e?0:1)}
package/ipc.js CHANGED
@@ -1,5 +1 @@
1
- import d from"net";import o from"fs";var g=process.env.SESSION_PROXY_SOCK||"/tmp/session-proxy.sock";function x(t,r="process"){if(process.env.RELAY_CLIENT_SKIP_LOCK==="1")return{acquired:!0,skipped:!0};let e=process.pid;function n(){try{let c=o.openSync(t,o.constants.O_WRONLY|o.constants.O_CREAT|o.constants.O_EXCL,420);return o.writeSync(c,`${e}
2
- ${r}
3
- `),o.closeSync(c),{ok:!0}}catch(c){return c.code!=="EEXIST"?{ok:!1,error:c.message}:{ok:!1,exists:!0}}}let i=n();if(i.ok)return a(t,e),{acquired:!0,lockPath:t};let s;try{s=o.readFileSync(t,"utf8")}catch{}let u=parseInt((s||"").split(`
4
- `)[0],10);if(Number.isFinite(u)&&u!==e)try{return process.kill(u,0),{acquired:!1,heldBy:u,lockPath:t}}catch(c){if(c.code==="ESRCH"){try{o.unlinkSync(t)}catch{}if(i=n(),i.ok)return a(t,e),{acquired:!0,lockPath:t,reclaimed:!0}}}return{acquired:!1,heldBy:u||"unknown",lockPath:t}}function a(t,r){let e=()=>{try{let n=o.readFileSync(t,"utf8").split(`
5
- `)[0];parseInt(n,10)===r&&o.unlinkSync(t)}catch{}};process.on("exit",e),process.on("SIGINT",()=>{e(),process.exit(130)}),process.on("SIGTERM",()=>{e(),process.exit(143)})}var m=2*1024*1024,l=class{constructor(r=m){this.capacity=r,this.chunks=[],this.totalBytes=0,this.trimmedTo=0}append(r){let e=this.totalBytes;return this.chunks.push({offset:e,data:Buffer.from(r)}),this.totalBytes+=r.length,this._trim(),e}sliceFrom(r){if(r>=this.totalBytes)return Buffer.alloc(0);if(r<this.trimmedTo)throw new Error(`RING_OVERFLOW: requested offset ${r} already trimmed (trimmedTo=${this.trimmedTo})`);let e=[];for(let n of this.chunks){if(n.offset+n.data.length<=r)continue;let s=Math.max(0,r-n.offset);e.push(n.data.slice(s))}return Buffer.concat(e)}_trim(){for(;this.chunks.length>1&&!(this.totalBytes-this.chunks[0].offset<=this.capacity);){let e=this.chunks.shift();this.trimmedTo=e.offset+e.data.length}}},p=1,y=2;function E(t,r){if(!t||t.destroyed)return!1;let e=Buffer.alloc(5);return e[0]=p,e.writeUInt32LE(r.length,1),t.write(e),t.write(r),!0}function N(t,r){if(!t||t.destroyed)return!1;let e=Buffer.from(JSON.stringify(r)),n=Buffer.alloc(5);return n[0]=y,n.writeUInt32LE(e.length,1),t.write(n),t.write(e),!0}var h=class{constructor(r,e){this.onBinary=r,this.onJSON=e,this.buf=Buffer.alloc(0)}feed(r){for(this.buf=this.buf.length?Buffer.concat([this.buf,r]):r;this.buf.length>=5;){let e=this.buf[0],n=this.buf.readUInt32LE(1);if(this.buf.length<5+n)break;let i=this.buf.slice(5,5+n);if(this.buf=this.buf.slice(5+n),e===p)this.onBinary(i);else if(e===y)try{this.onJSON(JSON.parse(i.toString()))}catch(s){console.error(`[ipc] bad JSON: ${s.message}`)}}}};function T(t,r){try{o.unlinkSync(t)}catch{}let e=d.createServer(r);return e.listen(t,()=>{try{o.chmodSync(t,511)}catch{}}),e}function w(t,{onConnect:r,onClose:e,onError:n,retryMs:i=500}={}){let s=null,u=!1,c=null;function f(){u||(s=d.createConnection(t),s.on("connect",()=>{r&&r(s)}),s.on("close",()=>{s=null,e&&e(),u||(c=setTimeout(f,i))}),s.on("error",S=>{n&&n(S)}))}return f(),{getSocket(){return s},close(){u=!0,c&&clearTimeout(c),s&&s.destroy()}}}function _(t,r,e,n){let i=Buffer.alloc(17);return i.write(t,0,8,"ascii"),i[8]=r,i.writeBigUInt64BE(BigInt(e),9),Buffer.concat([i,n])}function O(t){return t.length<17?null:{sessionId:t.slice(0,8).toString(),direction:t[8],offset:Number(t.readBigUInt64BE(9)),payload:t.slice(17)}}export{h as FrameParser,m as MAX_RING,l as RingBuffer,g as SOCK_PATH,x as acquireSingleInstanceLock,w as connectIPC,T as createIPCServer,O as decodeDataFrame,_ as encodeDataFrame,E as sendBinary,N as sendJSON};
1
+ import l from"net";import a from"fs";var g=process.env.SESSION_PROXY_SOCK||"/tmp/session-proxy.sock",y=2*1024*1024,h=class{constructor(t=y){this.capacity=t,this.chunks=[],this.totalBytes=0,this.trimmedTo=0}append(t){let e=this.totalBytes;return this.chunks.push({offset:e,data:Buffer.from(t)}),this.totalBytes+=t.length,this._trim(),e}sliceFrom(t){if(t>=this.totalBytes)return Buffer.alloc(0);if(t<this.trimmedTo)throw new Error(`RING_OVERFLOW: requested offset ${t} already trimmed (trimmedTo=${this.trimmedTo})`);let e=[];for(let n of this.chunks){if(n.offset+n.data.length<=t)continue;let i=Math.max(0,t-n.offset);e.push(n.data.slice(i))}return Buffer.concat(e)}_trim(){for(;this.chunks.length>1&&!(this.totalBytes-this.chunks[0].offset<=this.capacity);){let e=this.chunks.shift();this.trimmedTo=e.offset+e.data.length}}},d=1,p=2;function x(r,t){if(!r||r.destroyed)return!1;let e=Buffer.alloc(5);return e[0]=d,e.writeUInt32LE(t.length,1),r.write(e),r.write(t),!0}function I(r,t){if(!r||r.destroyed)return!1;let e=Buffer.from(JSON.stringify(t)),n=Buffer.alloc(5);return n[0]=p,n.writeUInt32LE(e.length,1),r.write(n),r.write(e),!0}var u=class{constructor(t,e){this.onBinary=t,this.onJSON=e,this.buf=Buffer.alloc(0)}feed(t){for(this.buf=this.buf.length?Buffer.concat([this.buf,t]):t;this.buf.length>=5;){let e=this.buf[0],n=this.buf.readUInt32LE(1);if(this.buf.length<5+n)break;let s=this.buf.slice(5,5+n);if(this.buf=this.buf.slice(5+n),e===d)this.onBinary(s);else if(e===p)try{this.onJSON(JSON.parse(s.toString()))}catch(i){console.error(`[ipc] bad JSON: ${i.message}`)}}}};function w(r,t){try{a.unlinkSync(r)}catch{}let e=l.createServer(t);return e.listen(r,()=>{try{a.chmodSync(r,511)}catch{}}),e}function N(r,{onConnect:t,onClose:e,onError:n,retryMs:s=500}={}){let i=null,o=!1,c=null;function f(){o||(i=l.createConnection(r),i.on("connect",()=>{t&&t(i)}),i.on("close",()=>{i=null,e&&e(),o||(c=setTimeout(f,s))}),i.on("error",m=>{n&&n(m)}))}return f(),{getSocket(){return i},close(){o=!0,c&&clearTimeout(c),i&&i.destroy()}}}function E(r,t,e,n){let s=Buffer.alloc(17);return s.write(r,0,8,"ascii"),s[8]=t,s.writeBigUInt64BE(BigInt(e),9),Buffer.concat([s,n])}function T(r){return r.length<17?null:{sessionId:r.slice(0,8).toString(),direction:r[8],offset:Number(r.readBigUInt64BE(9)),payload:r.slice(17)}}export{u as FrameParser,y as MAX_RING,h as RingBuffer,g as SOCK_PATH,N as connectIPC,w as createIPCServer,T as decodeDataFrame,E as encodeDataFrame,x as sendBinary,I as sendJSON};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typhons/sandbox-tools",
3
- "version": "0.5.5",
3
+ "version": "0.5.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sandbox-relay-client": "./relay-client.js",
package/relay-client.js CHANGED
@@ -1,7 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import g from"ws";import z from"http";import y from"fs";import{spawn as Q,execSync as I,execFileSync as Z}from"child_process";import{dirname as ee}from"path";import{fileURLToPath as te}from"url";import q from"net";import d from"fs";var b=process.env.SESSION_PROXY_SOCK||"/tmp/session-proxy.sock";function D(e,r="process"){if(process.env.RELAY_CLIENT_SKIP_LOCK==="1")return{acquired:!0,skipped:!0};let t=process.pid;function n(){try{let s=d.openSync(e,d.constants.O_WRONLY|d.constants.O_CREAT|d.constants.O_EXCL,420);return d.writeSync(s,`${t}
3
- ${r}
4
- `),d.closeSync(s),{ok:!0}}catch(s){return s.code!=="EEXIST"?{ok:!1,error:s.message}:{ok:!1,exists:!0}}}let c=n();if(c.ok)return B(e,t),{acquired:!0,lockPath:e};let o;try{o=d.readFileSync(e,"utf8")}catch{}let l=parseInt((o||"").split(`
5
- `)[0],10);if(Number.isFinite(l)&&l!==t)try{return process.kill(l,0),{acquired:!1,heldBy:l,lockPath:e}}catch(s){if(s.code==="ESRCH"){try{d.unlinkSync(e)}catch{}if(c=n(),c.ok)return B(e,t),{acquired:!0,lockPath:e,reclaimed:!0}}}return{acquired:!1,heldBy:l||"unknown",lockPath:e}}function B(e,r){let t=()=>{try{let n=d.readFileSync(e,"utf8").split(`
6
- `)[0];parseInt(n,10)===r&&d.unlinkSync(e)}catch{}};process.on("exit",t),process.on("SIGINT",()=>{t(),process.exit(130)}),process.on("SIGTERM",()=>{t(),process.exit(143)})}var Ie=2*1024*1024;var k=1,P=2;function H(e,r){if(!e||e.destroyed)return!1;let t=Buffer.alloc(5);return t[0]=k,t.writeUInt32LE(r.length,1),e.write(t),e.write(r),!0}function U(e,r){if(!e||e.destroyed)return!1;let t=Buffer.from(JSON.stringify(r)),n=Buffer.alloc(5);return n[0]=P,n.writeUInt32LE(t.length,1),e.write(n),e.write(t),!0}var A=class{constructor(r,t){this.onBinary=r,this.onJSON=t,this.buf=Buffer.alloc(0)}feed(r){for(this.buf=this.buf.length?Buffer.concat([this.buf,r]):r;this.buf.length>=5;){let t=this.buf[0],n=this.buf.readUInt32LE(1);if(this.buf.length<5+n)break;let c=this.buf.slice(5,5+n);if(this.buf=this.buf.slice(5+n),t===k)this.onBinary(c);else if(t===P)try{this.onJSON(JSON.parse(c.toString()))}catch(o){console.error(`[ipc] bad JSON: ${o.message}`)}}}};function Y(e,{onConnect:r,onClose:t,onError:n,retryMs:c=500}={}){let o=null,l=!1,s=null;function f(){l||(o=q.createConnection(e),o.on("connect",()=>{r&&r(o)}),o.on("close",()=>{o=null,t&&t(),l||(s=setTimeout(f,c))}),o.on("error",C=>{n&&n(C)}))}return f(),{getSocket(){return o},close(){l=!0,s&&clearTimeout(s),o&&o.destroy()}}}var xe=ee(te(import.meta.url));process.on("uncaughtException",e=>{console.error(`[relay-client] UNCAUGHT: ${e.message}`),console.error(e.stack)});process.on("unhandledRejection",e=>{console.error(`[relay-client] UNHANDLED REJECTION: ${e.message||e}`)});var ne=process.env.RELAY_HOST||"ssh.typhons.dev",re=process.env.RELAY_SCHEME||"wss",T=process.env.E2B_RUN_DIR||"/run/e2b",oe=`${T}/.RELAY_TOKEN`,ie=`${T}/.E2B_SANDBOX_ID`,M=`${T}/.SAVED_SANDBOX_ID`,ce=`${T}/.DEVBOX_NAME`,se=parseInt(process.env.RELAY_CLIENT_RECONNECT_DELAY_MS||"1500",10),ae=3e3,F=15e3,le=6e4,K=parseInt(process.env.RELAY_CLIENT_EVICTION_RETRY_MS||"15000",10),J=parseInt(process.env.RELAY_CLIENT_EVICTION_GIVEUP_MS||String(30*60*1e3),10),ue=parseInt(process.env.RELAY_CLIENT_ID_RECHECK_MS||"10000",10),V="@typhons/sandbox-tools",de=process.env.RELAY_CLIENT_DISABLE_UPDATE==="1",R=parseInt(process.env.KEEPALIVE_PORT||"49999");function fe(){try{return y.readFileSync(oe,"utf8").trim()}catch{}return""}function v(){try{return y.readFileSync(ie,"utf8").trim()}catch{}return"unknown"}function i(e){console.log(`[relay-client ${new Date().toISOString().slice(11,19)}] ${e}`)}var h="unknown";function pe(){if(process.env.RELAY_URL)return process.env.RELAY_URL;h=v();try{let e=y.readFileSync(M,"utf8").trim();if(e&&e!==h){i(`Clone detected (${e} -> ${h}), interrupting agents...`);try{I("/opt/sandbox/bin/stop-agents",{timeout:1e4,stdio:"inherit"})}catch{}try{y.writeFileSync(M,h)}catch{}}}catch{}return`${re}://${ne}/tunnel/${h}`}var E=null,O=null;function he(){return Y(b,{onConnect(r){i("Connected to session-proxy via IPC"),E=r,O=new A(t=>{a&&a.readyState===g.OPEN&&a.send(t)},t=>{a&&a.readyState===g.OPEN&&a.send(JSON.stringify(t))}),r.on("data",t=>O.feed(t))},onClose(){i("IPC connection to session-proxy lost, will retry..."),E=null,O=null},onError(r){r.code!=="ENOENT"&&r.code!=="ECONNREFUSED"&&i(`IPC error: ${r.message}`)}})}var a=null,L=null,u=0,_=null,m=null;function x(e=se){if(a){try{a.terminate()}catch{}a=null}_&&(clearTimeout(_),_=null),_=setTimeout(()=>{_=null,W()},e)}function W(){let e=++u,r=pe();i(`Connecting to ${r}... (gen=${e}, sandbox=${h})`);let t={},n=fe();if(n&&(t.Authorization=`Bearer ${n}`),a){try{a.terminate()}catch{}a=null}let c=new g(r,{headers:t});a=c;let o=null,l=!0,s=setTimeout(()=>{if(e===u){i(`Handshake timeout after ${F}ms \u2014 aborting and reconnecting`);try{c.terminate()}catch{}f()}},F);function f(){s&&(clearTimeout(s),s=null),o&&(clearInterval(o),o=null),e===u&&x()}function C(){l=!0,o=setInterval(()=>{if(e!==u){clearInterval(o);return}if(!l){i("No pong received \u2014 connection dead, reconnecting"),f();return}l=!1;try{c.ping()}catch{f()}},ae)}c.on("open",()=>{if(e!==u){c.terminate();return}s&&(clearTimeout(s),s=null),i(`Connected${L?` (reconnect, port ${L})`:""} (gen=${e})`),C()}),c.on("pong",()=>{e===u&&(l=!0,m!==null&&(i(`Reclaimed slot after eviction (was evicted for ${Math.round((Date.now()-m)/1e3)}s)`),m=null))}),c.on("message",(p,S)=>{if(e===u){if(!S){try{ye(JSON.parse(p.toString()))}catch(N){i(`Bad control msg: ${N.message}`)}return}E&&!E.destroyed&&H(E,Buffer.from(p))}}),c.on("close",(p,S)=>{if(e!==u)return;let N=S?S.toString():"";if(p===4100){let w=Date.now();m===null&&(m=w);let $=w-m;$>=J&&(i(`Replaced by newer connection (code 4100) \u2014 gave up after ${Math.round($/1e3)}s, exiting`),process.exit(42)),i(`Replaced by newer connection (code 4100, reason=${N}) \u2014 retrying in ${K}ms (${Math.round($/1e3)}s evicted, giveup at ${Math.round(J/1e3)}s)`),s&&(clearTimeout(s),s=null),o&&(clearInterval(o),o=null),e===u&&x(K);return}i(`Data WS closed: code=${p}, reason=${N}`),f()}),c.on("error",p=>{e===u&&(i(`Data WS error: ${p.message} (code=${p.code||"none"})`),f())}),c.on("unexpected-response",(p,S)=>{e===u&&(i(`Data WS unexpected response: status=${S.statusCode}`),f())})}function ye(e){if(e.type==="port"){L=e.port,i(`Assigned port ${e.port}`);return}if(e.type==="name"){if(i(`Name assigned: ${e.name}`),e.name&&/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(e.name)){try{Z("hostname",[e.name],{timeout:5e3,stdio:"ignore"}),y.writeFileSync("/etc/hostname",e.name+`
7
- `)}catch{}try{y.writeFileSync(ce,e.name)}catch{}}return}E&&!E.destroyed&&U(E,e)}function G(){try{let e=I(`npm view ${V} version 2>/dev/null`,{timeout:15e3}).toString().trim();if(!e)return;let r="";try{r=JSON.parse(y.readFileSync("/opt/sandbox/lib/node_modules/@typhons/sandbox-tools/package.json","utf8")).version||""}catch{return}if(e!==r){i(`Update available: ${r} \u2192 ${e}, installing...`),I(`npm install -g --prefix /opt/sandbox ${V}@latest`,{timeout:12e4,stdio:"inherit"});try{I('pkill -f "check-claude-active.*--daemon" 2>/dev/null',{shell:!0,timeout:5e3})}catch{}try{let t=y.openSync("/tmp/claude-keepalive.log","a");Q("/opt/sandbox/bin/check-claude-active",["--daemon"],{detached:!0,stdio:["ignore",t,t]}).unref(),y.closeSync(t)}catch{}i("Update installed, exiting for restart..."),process.exit(0)}}catch{}}function Ee(){let e=z.createServer((t,n)=>{if(n.setHeader("Content-Type","application/json"),t.method==="GET"&&t.url==="/health"){n.end(JSON.stringify({ok:!0,sandboxId:v(),wsConnected:!!(a&&a.readyState===g.OPEN)}));return}if(t.method==="POST"&&t.url==="/_relay/verify"){let c="";t.on("data",o=>{c+=o,c.length>4096&&t.destroy()}),t.on("end",()=>{let o;try{o=JSON.parse(c).nonce}catch{n.writeHead(400),n.end('{"error":"bad json"}');return}if(typeof o!="string"||!o.length||o.length>256){n.writeHead(400),n.end('{"error":"missing or invalid nonce"}');return}if(!a||a.readyState!==g.OPEN){n.writeHead(503),n.end('{"error":"WS not connected"}');return}try{a.send(JSON.stringify({type:"verify-ack",nonce:o}))}catch(l){n.writeHead(500),n.end(`{"error":"${l.message}"}`);return}n.writeHead(200),n.end('{"ok":true}')}),t.on("error",()=>{try{n.writeHead(400),n.end('{"error":"req error"}')}catch{}});return}n.writeHead(404),n.end('{"error":"not found"}')}),r=!1;return e.on("error",t=>{if(t.code==="EADDRINUSE"&&!r){r=!0,i(`Port ${R} held by another process \u2014 killing it (likely a pre-0.5.5 session-proxy that owned this port; watchdog will restart it under new code which no longer binds it)`);try{I("pkill -f /opt/sandbox/lib/node_modules/@typhons/sandbox-tools/session-proxy.js",{timeout:3e3})}catch{}setTimeout(()=>{try{e.listen(R,"0.0.0.0")}catch{}},1500);return}i(`Keepalive HTTP server error: ${t.message}`)}),e.listen(R,"0.0.0.0",()=>{i(`Keepalive HTTP server on :${R}`)}),e}function Se(){if(process.env.RELAY_URL)return;let e=v();!e||e==="unknown"||e!==h&&(i(`Sandbox ID on disk changed (${h} -> ${e}), forcing reconnect`),h=e,x())}var j=process.env.RELAY_CLIENT_LOCK_PATH||`${T}/.relay-client.lock`,X=D(j,"relay-client");X.acquired||(i(`Another relay-client is already running (pid=${X.heldBy}, lock=${j}). Refusing to start. Set RELAY_CLIENT_SKIP_LOCK=1 to override.`),process.exit(2));var we=he();Ee();W();setInterval(Se,ue);de||(setTimeout(G,1e4),setInterval(G,le));i(`Relay client started (sandbox=${v()})`);
2
+ import g from"ws";import J from"http";import h from"fs";import{spawn as K,execSync as _,execFileSync as M}from"child_process";import{dirname as V}from"path";import{fileURLToPath as X}from"url";import F from"net";var O=process.env.SESSION_PROXY_SOCK||"/tmp/session-proxy.sock",ae=2*1024*1024;var B=1,x=2;function b(e,r){if(!e||e.destroyed)return!1;let t=Buffer.alloc(5);return t[0]=B,t.writeUInt32LE(r.length,1),e.write(t),e.write(r),!0}function w(e,r){if(!e||e.destroyed)return!1;let t=Buffer.from(JSON.stringify(r)),n=Buffer.alloc(5);return n[0]=x,n.writeUInt32LE(t.length,1),e.write(n),e.write(t),!0}var T=class{constructor(r,t){this.onBinary=r,this.onJSON=t,this.buf=Buffer.alloc(0)}feed(r){for(this.buf=this.buf.length?Buffer.concat([this.buf,r]):r;this.buf.length>=5;){let t=this.buf[0],n=this.buf.readUInt32LE(1);if(this.buf.length<5+n)break;let c=this.buf.slice(5,5+n);if(this.buf=this.buf.slice(5+n),t===B)this.onBinary(c);else if(t===x)try{this.onJSON(JSON.parse(c.toString()))}catch(o){console.error(`[ipc] bad JSON: ${o.message}`)}}}};function C(e,{onConnect:r,onClose:t,onError:n,retryMs:c=500}={}){let o=null,u=!1,a=null;function f(){u||(o=F.createConnection(e),o.on("connect",()=>{r&&r(o)}),o.on("close",()=>{o=null,t&&t(),u||(a=setTimeout(f,c))}),o.on("error",A=>{n&&n(A)}))}return f(),{getSocket(){return o},close(){u=!0,a&&clearTimeout(a),o&&o.destroy()}}}var _e=V(X(import.meta.url));process.on("uncaughtException",e=>{console.error(`[relay-client] UNCAUGHT: ${e.message}`),console.error(e.stack)});process.on("unhandledRejection",e=>{console.error(`[relay-client] UNHANDLED REJECTION: ${e.message||e}`)});var G=process.env.RELAY_HOST||"ssh.typhons.dev",j=process.env.RELAY_SCHEME||"wss",I=process.env.E2B_RUN_DIR||"/run/e2b",W=`${I}/.RELAY_TOKEN`,z=`${I}/.E2B_SANDBOX_ID`,D=`${I}/.SAVED_SANDBOX_ID`,Q=`${I}/.DEVBOX_NAME`,Z=parseInt(process.env.RELAY_CLIENT_RECONNECT_DELAY_MS||"1500",10),q=3e3,P=15e3,ee=6e4,k=parseInt(process.env.RELAY_CLIENT_VERIFY_REJECT_RETRY_MS||"10000",10),L="@typhons/sandbox-tools",te=process.env.RELAY_CLIENT_DISABLE_UPDATE==="1",N=parseInt(process.env.KEEPALIVE_PORT||"49999");function ne(){try{return h.readFileSync(W,"utf8").trim()}catch{}return""}function $(){try{return h.readFileSync(z,"utf8").trim()}catch{}return"unknown"}function i(e){console.log(`[relay-client ${new Date().toISOString().slice(11,19)}] ${e}`)}var m="unknown";function re(){if(process.env.RELAY_URL)return process.env.RELAY_URL;m=$();try{let e=h.readFileSync(D,"utf8").trim();if(e&&e!==m){i(`Clone detected (${e} -> ${m}), interrupting agents...`);try{_("/opt/sandbox/bin/stop-agents",{timeout:1e4,stdio:"inherit"})}catch{}try{h.writeFileSync(D,m)}catch{}}}catch{}return`${j}://${G}/tunnel/${m}`}var p=null,R=null;function oe(){return C(O,{onConnect(r){i("Connected to session-proxy via IPC"),p=r,R=new T(t=>{s&&s.readyState===g.OPEN&&s.send(t)},t=>{s&&s.readyState===g.OPEN&&s.send(JSON.stringify(t))}),r.on("data",t=>R.feed(t))},onClose(){i("IPC connection to session-proxy lost, will retry..."),p=null,R=null},onError(r){r.code!=="ENOENT"&&r.code!=="ECONNREFUSED"&&i(`IPC error: ${r.message}`)}})}var s=null,v=null,l=0,S=null;function H(e=Z){if(s){try{s.terminate()}catch{}s=null}S&&(clearTimeout(S),S=null),S=setTimeout(()=>{S=null,Y()},e)}function Y(){let e=++l,r=re();i(`Connecting to ${r}... (gen=${e}, sandbox=${m})`);let t={},n=ne();if(n&&(t.Authorization=`Bearer ${n}`),s){try{s.terminate()}catch{}s=null}let c=new g(r,{headers:t});s=c;let o=null,u=!0,a=setTimeout(()=>{if(e===l){i(`Handshake timeout after ${P}ms \u2014 aborting and reconnecting`);try{c.terminate()}catch{}f()}},P);function f(){a&&(clearTimeout(a),a=null),o&&(clearInterval(o),o=null),e===l&&H()}function A(){u=!0,o=setInterval(()=>{if(e!==l){clearInterval(o);return}if(!u){i("No pong received \u2014 connection dead, reconnecting"),f();return}u=!1;try{c.ping()}catch{f()}},q)}c.on("open",()=>{if(e!==l){c.terminate();return}a&&(clearTimeout(a),a=null),i(`Connected${v?` (reconnect, port ${v})`:""} (gen=${e})`),A()}),c.on("pong",()=>{e===l&&(u=!0)}),c.on("message",(d,y)=>{if(e===l){if(!y){try{ie(JSON.parse(d.toString()))}catch(E){i(`Bad control msg: ${E.message}`)}return}p&&!p.destroyed&&b(p,Buffer.from(d))}}),c.on("close",(d,y)=>{if(e!==l)return;let E=y?y.toString():"";if(d===4100&&(i(`Replaced by newer connection (code 4100, reason=${E}) \u2014 exiting`),process.exit(42)),d===4101){i(`Identity verification rejected (code 4101, reason=${E}) \u2014 retrying in ${k}ms (likely waiting for envd to update /run/e2b/.E2B_SANDBOX_ID)`),a&&(clearTimeout(a),a=null),o&&(clearInterval(o),o=null),e===l&&H(k);return}i(`Data WS closed: code=${d}, reason=${E}`),f()}),c.on("error",d=>{e===l&&(i(`Data WS error: ${d.message} (code=${d.code||"none"})`),f())}),c.on("unexpected-response",(d,y)=>{e===l&&(i(`Data WS unexpected response: status=${y.statusCode}`),f())})}function ie(e){if(e.type==="port"){v=e.port,i(`Assigned port ${e.port}`);return}if(e.type==="name"){if(i(`Name assigned: ${e.name}`),e.name&&/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(e.name)){try{M("hostname",[e.name],{timeout:5e3,stdio:"ignore"}),h.writeFileSync("/etc/hostname",e.name+`
3
+ `)}catch{}try{h.writeFileSync(Q,e.name)}catch{}}return}p&&!p.destroyed&&w(p,e)}function U(){try{let e=_(`npm view ${L} version 2>/dev/null`,{timeout:15e3}).toString().trim();if(!e)return;let r="";try{r=JSON.parse(h.readFileSync("/opt/sandbox/lib/node_modules/@typhons/sandbox-tools/package.json","utf8")).version||""}catch{return}if(e!==r){i(`Update available: ${r} \u2192 ${e}, installing...`),_(`npm install -g --prefix /opt/sandbox ${L}@latest`,{timeout:12e4,stdio:"inherit"});try{_('pkill -f "check-claude-active.*--daemon" 2>/dev/null',{shell:!0,timeout:5e3})}catch{}try{let t=h.openSync("/tmp/claude-keepalive.log","a");K("/opt/sandbox/bin/check-claude-active",["--daemon"],{detached:!0,stdio:["ignore",t,t]}).unref(),h.closeSync(t)}catch{}i("Update installed, exiting for restart..."),process.exit(0)}}catch{}}function ce(){let e=J.createServer((t,n)=>{if(n.setHeader("Content-Type","application/json"),t.method==="GET"&&t.url==="/health"){n.end(JSON.stringify({ok:!0,sandboxId:$(),wsConnected:!!(s&&s.readyState===g.OPEN)}));return}if(t.method==="POST"&&t.url==="/_relay/verify"){let c="";t.on("data",o=>{c+=o,c.length>4096&&t.destroy()}),t.on("end",()=>{let o;try{o=JSON.parse(c).nonce}catch{n.writeHead(400),n.end('{"error":"bad json"}');return}if(typeof o!="string"||!o.length||o.length>256){n.writeHead(400),n.end('{"error":"missing or invalid nonce"}');return}if(!s||s.readyState!==g.OPEN){n.writeHead(503),n.end('{"error":"WS not connected"}');return}try{s.send(JSON.stringify({type:"verify-ack",nonce:o}))}catch(u){n.writeHead(500),n.end(`{"error":"${u.message}"}`);return}n.writeHead(200),n.end('{"ok":true}')}),t.on("error",()=>{try{n.writeHead(400),n.end('{"error":"req error"}')}catch{}});return}n.writeHead(404),n.end('{"error":"not found"}')}),r=!1;return e.on("error",t=>{if(t.code==="EADDRINUSE"&&!r){r=!0,i(`Port ${N} held by another process \u2014 killing it (likely a pre-0.5.5 session-proxy that owned this port; watchdog will restart it under new code which no longer binds it)`);try{_("pkill -f /opt/sandbox/lib/node_modules/@typhons/sandbox-tools/session-proxy.js",{timeout:3e3})}catch{}setTimeout(()=>{try{e.listen(N,"0.0.0.0")}catch{}},1500);return}i(`Keepalive HTTP server error: ${t.message}`)}),e.listen(N,"0.0.0.0",()=>{i(`Keepalive HTTP server on :${N}`)}),e}var ge=oe();ce();Y();te||(setTimeout(U,1e4),setInterval(U,ee));i(`Relay client started (sandbox=${$()})`);
@@ -1,13 +1,5 @@
1
1
  #!/bin/bash
2
2
  # Watchdog: keeps both session-proxy and relay-client running.
3
- #
4
- # Exit codes from the children that we treat specially:
5
- # 42 — relay-client gave up after EVICTION_GIVEUP_MS of consistent 4100s.
6
- # Means another machine durably owns this sandbox-id slot. Stop.
7
- # 2 — child refused to start because another instance already holds the
8
- # single-instance lock. Keep retrying on a slower cadence (5s) so
9
- # we take over cleanly if the lock-holder eventually exits, without
10
- # busy-looping spawn-and-exit while it's alive.
11
3
 
12
4
  BIN="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
13
5
  export SSH_PORT="${SSH_PORT:-2222}"
@@ -19,11 +11,6 @@ log() { echo "[watchdog $(date +%H:%M:%S)] $*"; }
19
11
  while true; do
20
12
  node "$BIN/session-proxy.js" 2>&1
21
13
  EXIT_CODE=$?
22
- if [ "$EXIT_CODE" = "2" ]; then
23
- log "session-proxy refused to start (lock held), retrying in 5s..."
24
- sleep 5
25
- continue
26
- fi
27
14
  log "session-proxy exited (code $EXIT_CODE), restarting in 2s..."
28
15
  sleep 2
29
16
  done
@@ -36,15 +23,10 @@ while true; do
36
23
  node "$BIN/relay-client.js" 2>&1
37
24
  EXIT_CODE=$?
38
25
  if [ "$EXIT_CODE" = "42" ]; then
39
- log "relay-client gave up after sustained eviction (exit 42), stopping all"
26
+ log "relay-client replaced by newer connection (exit 42), stopping all"
40
27
  kill $SESSION_PROXY_PID 2>/dev/null
41
28
  exit 0
42
29
  fi
43
- if [ "$EXIT_CODE" = "2" ]; then
44
- log "relay-client refused to start (lock held), retrying in 5s..."
45
- sleep 5
46
- continue
47
- fi
48
30
  log "relay-client exited (code $EXIT_CODE), restarting in 2s..."
49
31
  sleep 2
50
32
  done
package/session-proxy.js CHANGED
@@ -1,6 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import M from"net";import K from"fs";import{execFileSync as G}from"child_process";import z from"net";import a from"fs";var k=process.env.SESSION_PROXY_SOCK||"/tmp/session-proxy.sock";function L(e,s="process"){if(process.env.RELAY_CLIENT_SKIP_LOCK==="1")return{acquired:!0,skipped:!0};let t=process.pid;function r(){try{let u=a.openSync(e,a.constants.O_WRONLY|a.constants.O_CREAT|a.constants.O_EXCL,420);return a.writeSync(u,`${t}
3
- ${s}
4
- `),a.closeSync(u),{ok:!0}}catch(u){return u.code!=="EEXIST"?{ok:!1,error:u.message}:{ok:!1,exists:!0}}}let n=r();if(n.ok)return b(e,t),{acquired:!0,lockPath:e};let o;try{o=a.readFileSync(e,"utf8")}catch{}let i=parseInt((o||"").split(`
5
- `)[0],10);if(Number.isFinite(i)&&i!==t)try{return process.kill(i,0),{acquired:!1,heldBy:i,lockPath:e}}catch(u){if(u.code==="ESRCH"){try{a.unlinkSync(e)}catch{}if(n=r(),n.ok)return b(e,t),{acquired:!0,lockPath:e,reclaimed:!0}}}return{acquired:!1,heldBy:i||"unknown",lockPath:e}}function b(e,s){let t=()=>{try{let r=a.readFileSync(e,"utf8").split(`
6
- `)[0];parseInt(r,10)===s&&a.unlinkSync(e)}catch{}};process.on("exit",t),process.on("SIGINT",()=>{t(),process.exit(130)}),process.on("SIGTERM",()=>{t(),process.exit(143)})}var J=2*1024*1024,$=class{constructor(s=J){this.capacity=s,this.chunks=[],this.totalBytes=0,this.trimmedTo=0}append(s){let t=this.totalBytes;return this.chunks.push({offset:t,data:Buffer.from(s)}),this.totalBytes+=s.length,this._trim(),t}sliceFrom(s){if(s>=this.totalBytes)return Buffer.alloc(0);if(s<this.trimmedTo)throw new Error(`RING_OVERFLOW: requested offset ${s} already trimmed (trimmedTo=${this.trimmedTo})`);let t=[];for(let r of this.chunks){if(r.offset+r.data.length<=s)continue;let o=Math.max(0,s-r.offset);t.push(r.data.slice(o))}return Buffer.concat(t)}_trim(){for(;this.chunks.length>1&&!(this.totalBytes-this.chunks[0].offset<=this.capacity);){let t=this.chunks.shift();this.trimmedTo=t.offset+t.data.length}}},R=1,q=2;function A(e,s){if(!e||e.destroyed)return!1;let t=Buffer.alloc(5);return t[0]=R,t.writeUInt32LE(s.length,1),e.write(t),e.write(s),!0}function O(e,s){if(!e||e.destroyed)return!1;let t=Buffer.from(JSON.stringify(s)),r=Buffer.alloc(5);return r[0]=q,r.writeUInt32LE(t.length,1),e.write(r),e.write(t),!0}var w=class{constructor(s,t){this.onBinary=s,this.onJSON=t,this.buf=Buffer.alloc(0)}feed(s){for(this.buf=this.buf.length?Buffer.concat([this.buf,s]):s;this.buf.length>=5;){let t=this.buf[0],r=this.buf.readUInt32LE(1);if(this.buf.length<5+r)break;let n=this.buf.slice(5,5+r);if(this.buf=this.buf.slice(5+r),t===R)this.onBinary(n);else if(t===q)try{this.onJSON(JSON.parse(n.toString()))}catch(o){console.error(`[ipc] bad JSON: ${o.message}`)}}}};function F(e,s){try{a.unlinkSync(e)}catch{}let t=z.createServer(s);return t.listen(e,()=>{try{a.chmodSync(e,511)}catch{}}),t}function H(e,s,t,r){let n=Buffer.alloc(17);return n.write(e,0,8,"ascii"),n[8]=s,n.writeBigUInt64BE(BigInt(t),9),Buffer.concat([n,r])}function I(e){return e.length<17?null:{sessionId:e.slice(0,8).toString(),direction:e[8],offset:Number(e.readBigUInt64BE(9)),payload:e.slice(17)}}var B;try{B=(await import("@homebridge/node-pty-prebuilt-multiarch")).default}catch{try{B=(await import("node-pty")).default}catch{}}var Y=parseInt(process.env.SSH_PORT||"22");function c(e){console.log(`[session-proxy ${new Date().toISOString().slice(11,19)}] ${e}`)}process.on("uncaughtException",e=>{console.error(`[session-proxy] UNCAUGHT: ${e.message}`),console.error(e.stack)});process.on("unhandledRejection",e=>{console.error(`[session-proxy] UNHANDLED REJECTION: ${e.message||e}`)});var l=new Map,d=null;function v(e,s,t,r){if(!d||d.destroyed)return!1;let n=H(e,s,t,r);return A(d,n)}function f(e){!d||d.destroyed||O(d,e)}function y(e,s){let t=l.get(e);if(t){if(c(`session ${e}: closing \u2014 ${s}`),t.socket&&!t.socket.destroyed&&t.socket.destroy(),t.ptyProcess){t.closed=!0;try{t.ptyProcess.kill()}catch{}}f({type:"close",session:e}),l.delete(e)}}function X(e,s,t){let r=l.get(e);if(!r||!(r.ptyProcess||r.socket)||r.socket&&r.socket.destroyed||r.closed)return;let o=r.user2sshd.received;if(s+t.length<=o)return;let i=t;if(s<o&&(i=t.slice(o-s)),s>o){y(e,`user2sshd stream gap: expected offset ${o}, got ${s}`);return}r.ptyProcess?r.ptyProcess.write(i):r.socket.write(i),r.user2sshd.received=Math.max(o,s+t.length),f({type:"ack",session:e,direction:"user2sshd",offset:r.user2sshd.received})}function D(e,s){let t=l.get(e);if(!t)return;let r;try{r=t.sshd2user.ring.sliceFrom(s)}catch(n){y(e,`ring buffer overflow on resend: ${n.message}`);return}r.length>0?(c(`session ${e}: re-sending ${r.length}b sshd\u2192user from offset ${s}`),v(e,0,s,r)):c(`session ${e}: relay has all ${s}b sshd\u2192user, nothing to re-send`)}function j(e){if(e.type==="open"){e.mode==="pty"?B?W(e.session,e.sshd2userOffset||0,e.user2sshdOffset||0,e):(c(`session ${e.session}: PTY mode requested but node-pty not available \u2014 rejecting`),f({type:"close",session:e.session})):V(e.session,e.sshd2userOffset||0,e.user2sshdOffset||0);return}if(e.type==="resize"){let s=l.get(e.session);if(s?.ptyProcess)try{s.ptyProcess.resize(e.cols,e.rows)}catch{}return}if(e.type==="close"){y(e.session,"relay requested close");return}if(e.type==="ack"){let s=l.get(e.session);if(!s)return;e.direction==="sshd2user"&&typeof e.offset=="number"&&(s.sshd2user.acked=Math.max(s.sshd2user.acked,e.offset));return}if(e.type==="get-authorized-keys"){let s=e.user||"root";if(s!=="root"&&!/^[a-z_][a-z0-9_-]{0,31}$/.test(s)){f({type:"authorized-keys",requestId:e.requestId,keys:null});return}let t=s==="root"?"/root":`/home/${s}`,r=null;try{r=K.readFileSync(`${t}/.ssh/authorized_keys`,"utf8")}catch{}f({type:"authorized-keys",requestId:e.requestId,keys:r});return}}function W(e,s,t,r){let n=l.get(e);if(n&&n.ptyProcess&&!n.closed){c(`session ${e}: re-attaching PTY (relay sshd2user=${s}, user2sshd=${t}, our sshd2user=${n.sshd2user.ring.totalBytes}, user2sshd=${n.user2sshd.received})`),n.sshd2user.acked=s,f({type:"ready",session:e,sshd2userOffset:n.sshd2user.ring.totalBytes,user2sshdOffset:n.user2sshd.received}),D(e,s);return}let o=r.user||"root",i=r.shell||null,u=process.env.SHELL||"/bin/bash",T=r.cols||80,P=r.rows||24,_=r.term||"xterm-256color",g,E,m;try{let h=G("getent",["passwd",o],{timeout:2e3}).toString().trim().split(":");g=parseInt(h[2]),E=parseInt(h[3]),m=h[5]||(o==="root"?"/root":`/home/${o}`)}catch{g=o==="root"?0:void 0,E=void 0,m=o==="root"?"/root":`/home/${o}`}let C=u,N=i?["-c",i]:["-l"];c(`session ${e}: spawning PTY ${C} ${N.join(" ")} as ${o} (uid=${g}, ${T}x${P})`);let x={socket:null,ptyProcess:null,closed:!1,sshd2user:{ring:new $,acked:0},user2sshd:{received:0}};l.set(e,x);try{let p=B.spawn(C,N,{name:_,cols:T,rows:P,cwd:m,env:{...process.env,TERM:_,HOME:m,USER:o,LOGNAME:o,SHELL:u,PATH:process.env.PATH},uid:g,gid:E});x.ptyProcess=p,p.onData(h=>{let S=Buffer.from(h),U=x.sshd2user.ring.append(S);v(e,0,U,S)}),p.onExit(({exitCode:h,signal:S})=>{c(`session ${e}: PTY exited (code=${h}, signal=${S})`),x.closed=!0,f({type:"exit",session:e,code:h,signal:S||void 0}),y(e,"pty exited")}),f({type:"ready",session:e,sshd2userOffset:0,user2sshdOffset:0})}catch(p){c(`session ${e}: PTY spawn failed: ${p.message}`),y(e,`pty spawn error: ${p.message}`)}}function V(e,s,t){let r=l.get(e);if(r&&r.socket&&!r.socket.destroyed){c(`session ${e}: re-attaching (relay sshd2user=${s}, user2sshd=${t}, our sshd2user=${r.sshd2user.ring.totalBytes}, user2sshd=${r.user2sshd.received})`),r.sshd2user.acked=s,f({type:"ready",session:e,sshd2userOffset:r.sshd2user.ring.totalBytes,user2sshdOffset:r.user2sshd.received}),D(e,s);return}c(`session ${e}: connecting to sshd :${Y}`);let n={socket:null,sshd2user:{ring:new $,acked:0},user2sshd:{received:0}};l.set(e,n);let o=M.createConnection({host:"127.0.0.1",port:Y},()=>{c(`session ${e}: sshd connected`),o.setNoDelay(!0),n.socket=o,f({type:"ready",session:e,sshd2userOffset:0,user2sshdOffset:0})});n.socket=o,o.on("data",i=>{let u=n.sshd2user.ring.append(i);v(e,0,u,i)}),o.on("close",()=>y(e,"sshd closed")),o.on("error",i=>y(e,`sshd error: ${i.message}`))}{let e=process.env.SESSION_PROXY_LOCK_PATH||`${k}.lock`,s=L(e,"session-proxy");s.acquired||(c(`Another session-proxy is already running (pid=${s.heldBy}, lock=${e}). Refusing to start. Set RELAY_CLIENT_SKIP_LOCK=1 to override.`),process.exit(2))}var oe=F(k,e=>{if(c("Relay-client connected via IPC"),d&&!d.destroyed&&(c("Replacing previous relay-client IPC connection"),d.destroy()),d=e,l.size>0){let t=[];for(let[r,n]of l)t.push({id:r,sshd2userOffset:n.sshd2user.ring.totalBytes,user2sshdOffset:n.user2sshd.received,mode:n.ptyProcess?"pty":"sshd"});O(e,{type:"existing-sessions",sessions:t})}let s=new w(t=>{let r=I(t);r&&r.direction===1&&X(r.sessionId,r.offset,r.payload)},t=>{j(t)});e.on("data",t=>s.feed(t)),e.on("close",()=>{c("Relay-client IPC disconnected"),d===e&&(d=null)}),e.on("error",t=>{c(`IPC error: ${t.message}`)})});c(`IPC server listening on ${k}`);
2
+ import M from"net";import q from"fs";import{execFileSync as G}from"child_process";import J from"net";import C from"fs";var P=process.env.SESSION_PROXY_SOCK||"/tmp/session-proxy.sock",Y=2*1024*1024,y=class{constructor(s=Y){this.capacity=s,this.chunks=[],this.totalBytes=0,this.trimmedTo=0}append(s){let t=this.totalBytes;return this.chunks.push({offset:t,data:Buffer.from(s)}),this.totalBytes+=s.length,this._trim(),t}sliceFrom(s){if(s>=this.totalBytes)return Buffer.alloc(0);if(s<this.trimmedTo)throw new Error(`RING_OVERFLOW: requested offset ${s} already trimmed (trimmedTo=${this.trimmedTo})`);let t=[];for(let r of this.chunks){if(r.offset+r.data.length<=s)continue;let n=Math.max(0,s-r.offset);t.push(r.data.slice(n))}return Buffer.concat(t)}_trim(){for(;this.chunks.length>1&&!(this.totalBytes-this.chunks[0].offset<=this.capacity);){let t=this.chunks.shift();this.trimmedTo=t.offset+t.data.length}}},_=1,R=2;function L(e,s){if(!e||e.destroyed)return!1;let t=Buffer.alloc(5);return t[0]=_,t.writeUInt32LE(s.length,1),e.write(t),e.write(s),!0}function k(e,s){if(!e||e.destroyed)return!1;let t=Buffer.from(JSON.stringify(s)),r=Buffer.alloc(5);return r[0]=R,r.writeUInt32LE(t.length,1),e.write(r),e.write(t),!0}var w=class{constructor(s,t){this.onBinary=s,this.onJSON=t,this.buf=Buffer.alloc(0)}feed(s){for(this.buf=this.buf.length?Buffer.concat([this.buf,s]):s;this.buf.length>=5;){let t=this.buf[0],r=this.buf.readUInt32LE(1);if(this.buf.length<5+r)break;let o=this.buf.slice(5,5+r);if(this.buf=this.buf.slice(5+r),t===_)this.onBinary(o);else if(t===R)try{this.onJSON(JSON.parse(o.toString()))}catch(n){console.error(`[ipc] bad JSON: ${n.message}`)}}}};function D(e,s){try{C.unlinkSync(e)}catch{}let t=J.createServer(s);return t.listen(e,()=>{try{C.chmodSync(e,511)}catch{}}),t}function F(e,s,t,r){let o=Buffer.alloc(17);return o.write(e,0,8,"ascii"),o[8]=s,o.writeBigUInt64BE(BigInt(t),9),Buffer.concat([o,r])}function H(e){return e.length<17?null:{sessionId:e.slice(0,8).toString(),direction:e[8],offset:Number(e.readBigUInt64BE(9)),payload:e.slice(17)}}var x;try{x=(await import("@homebridge/node-pty-prebuilt-multiarch")).default}catch{try{x=(await import("node-pty")).default}catch{}}var U=parseInt(process.env.SSH_PORT||"22");function i(e){console.log(`[session-proxy ${new Date().toISOString().slice(11,19)}] ${e}`)}process.on("uncaughtException",e=>{console.error(`[session-proxy] UNCAUGHT: ${e.message}`),console.error(e.stack)});process.on("unhandledRejection",e=>{console.error(`[session-proxy] UNHANDLED REJECTION: ${e.message||e}`)});var u=new Map,c=null;function v(e,s,t,r){if(!c||c.destroyed)return!1;let o=F(e,s,t,r);return L(c,o)}function d(e){!c||c.destroyed||k(c,e)}function h(e,s){let t=u.get(e);if(t){if(i(`session ${e}: closing \u2014 ${s}`),t.socket&&!t.socket.destroyed&&t.socket.destroy(),t.ptyProcess){t.closed=!0;try{t.ptyProcess.kill()}catch{}}d({type:"close",session:e}),u.delete(e)}}function j(e,s,t){let r=u.get(e);if(!r||!(r.ptyProcess||r.socket)||r.socket&&r.socket.destroyed||r.closed)return;let n=r.user2sshd.received;if(s+t.length<=n)return;let a=t;if(s<n&&(a=t.slice(n-s)),s>n){h(e,`user2sshd stream gap: expected offset ${n}, got ${s}`);return}r.ptyProcess?r.ptyProcess.write(a):r.socket.write(a),r.user2sshd.received=Math.max(n,s+t.length),d({type:"ack",session:e,direction:"user2sshd",offset:r.user2sshd.received})}function A(e,s){let t=u.get(e);if(!t)return;let r;try{r=t.sshd2user.ring.sliceFrom(s)}catch(o){h(e,`ring buffer overflow on resend: ${o.message}`);return}r.length>0?(i(`session ${e}: re-sending ${r.length}b sshd\u2192user from offset ${s}`),v(e,0,s,r)):i(`session ${e}: relay has all ${s}b sshd\u2192user, nothing to re-send`)}function K(e){if(e.type==="open"){e.mode==="pty"?x?W(e.session,e.sshd2userOffset||0,e.user2sshdOffset||0,e):(i(`session ${e.session}: PTY mode requested but node-pty not available \u2014 rejecting`),d({type:"close",session:e.session})):X(e.session,e.sshd2userOffset||0,e.user2sshdOffset||0);return}if(e.type==="resize"){let s=u.get(e.session);if(s?.ptyProcess)try{s.ptyProcess.resize(e.cols,e.rows)}catch{}return}if(e.type==="close"){h(e.session,"relay requested close");return}if(e.type==="ack"){let s=u.get(e.session);if(!s)return;e.direction==="sshd2user"&&typeof e.offset=="number"&&(s.sshd2user.acked=Math.max(s.sshd2user.acked,e.offset));return}if(e.type==="get-authorized-keys"){let s=e.user||"root";if(s!=="root"&&!/^[a-z_][a-z0-9_-]{0,31}$/.test(s)){d({type:"authorized-keys",requestId:e.requestId,keys:null});return}let t=s==="root"?"/root":`/home/${s}`,r=null;try{r=q.readFileSync(`${t}/.ssh/authorized_keys`,"utf8")}catch{}d({type:"authorized-keys",requestId:e.requestId,keys:r});return}}function W(e,s,t,r){let o=u.get(e);if(o&&o.ptyProcess&&!o.closed){i(`session ${e}: re-attaching PTY (relay sshd2user=${s}, user2sshd=${t}, our sshd2user=${o.sshd2user.ring.totalBytes}, user2sshd=${o.user2sshd.received})`),o.sshd2user.acked=s,d({type:"ready",session:e,sshd2userOffset:o.sshd2user.ring.totalBytes,user2sshdOffset:o.user2sshd.received}),A(e,s);return}let n=r.user||"root",a=r.shell||null,$=process.env.SHELL||"/bin/bash",E=r.cols||80,O=r.rows||24,T=r.term||"xterm-256color",m,B,g;try{let l=G("getent",["passwd",n],{timeout:2e3}).toString().trim().split(":");m=parseInt(l[2]),B=parseInt(l[3]),g=l[5]||(n==="root"?"/root":`/home/${n}`)}catch{m=n==="root"?0:void 0,B=void 0,g=n==="root"?"/root":`/home/${n}`}let b=$,N=a?["-c",a]:["-l"];i(`session ${e}: spawning PTY ${b} ${N.join(" ")} as ${n} (uid=${m}, ${E}x${O})`);let S={socket:null,ptyProcess:null,closed:!1,sshd2user:{ring:new y,acked:0},user2sshd:{received:0}};u.set(e,S);try{let f=x.spawn(b,N,{name:T,cols:E,rows:O,cwd:g,env:{...process.env,TERM:T,HOME:g,USER:n,LOGNAME:n,SHELL:$,PATH:process.env.PATH},uid:m,gid:B});S.ptyProcess=f,f.onData(l=>{let p=Buffer.from(l),z=S.sshd2user.ring.append(p);v(e,0,z,p)}),f.onExit(({exitCode:l,signal:p})=>{i(`session ${e}: PTY exited (code=${l}, signal=${p})`),S.closed=!0,d({type:"exit",session:e,code:l,signal:p||void 0}),h(e,"pty exited")}),d({type:"ready",session:e,sshd2userOffset:0,user2sshdOffset:0})}catch(f){i(`session ${e}: PTY spawn failed: ${f.message}`),h(e,`pty spawn error: ${f.message}`)}}function X(e,s,t){let r=u.get(e);if(r&&r.socket&&!r.socket.destroyed){i(`session ${e}: re-attaching (relay sshd2user=${s}, user2sshd=${t}, our sshd2user=${r.sshd2user.ring.totalBytes}, user2sshd=${r.user2sshd.received})`),r.sshd2user.acked=s,d({type:"ready",session:e,sshd2userOffset:r.sshd2user.ring.totalBytes,user2sshdOffset:r.user2sshd.received}),A(e,s);return}i(`session ${e}: connecting to sshd :${U}`);let o={socket:null,sshd2user:{ring:new y,acked:0},user2sshd:{received:0}};u.set(e,o);let n=M.createConnection({host:"127.0.0.1",port:U},()=>{i(`session ${e}: sshd connected`),n.setNoDelay(!0),o.socket=n,d({type:"ready",session:e,sshd2userOffset:0,user2sshdOffset:0})});o.socket=n,n.on("data",a=>{let $=o.sshd2user.ring.append(a);v(e,0,$,a)}),n.on("close",()=>h(e,"sshd closed")),n.on("error",a=>h(e,`sshd error: ${a.message}`))}var re=D(P,e=>{if(i("Relay-client connected via IPC"),c&&!c.destroyed&&(i("Replacing previous relay-client IPC connection"),c.destroy()),c=e,u.size>0){let t=[];for(let[r,o]of u)t.push({id:r,sshd2userOffset:o.sshd2user.ring.totalBytes,user2sshdOffset:o.user2sshd.received,mode:o.ptyProcess?"pty":"sshd"});k(e,{type:"existing-sessions",sessions:t})}let s=new w(t=>{let r=H(t);r&&r.direction===1&&j(r.sessionId,r.offset,r.payload)},t=>{K(t)});e.on("data",t=>s.feed(t)),e.on("close",()=>{i("Relay-client IPC disconnected"),c===e&&(c=null)}),e.on("error",t=>{i(`IPC error: ${t.message}`)})});i(`IPC server listening on ${P}`);
package/stop-agents.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import{execSync as e}from"child_process";import l from"fs";function n(t){console.log(`[stop-agents] ${t}`)}function r(t,d){let o;try{o=e(`pgrep -f "${d}" 2>/dev/null | head -1`,{encoding:"utf8"}).trim()}catch{}if(!o){n(`No ${t} process found`);return}let c;try{c=l.readlinkSync(`/proc/${o}/fd/0`)}catch{}if(!c||!c.startsWith("/dev/pts/")){n(`${t} (pid ${o}) has no PTY (fd/0 -> ${c}), skipping`);return}n(`Injecting Escape into ${t} (pid ${o}, tty ${c})`);try{e(`sudo python3 -c "
2
+ import{execSync as s}from"child_process";import p from"fs";import{execSync as d}from"child_process";function r(n){let o="";try{o=d(`pgrep -x ${n} 2>/dev/null`,{encoding:"utf8"})}catch{}return o.split(`
3
+ `).map(t=>t.trim()).filter(Boolean)}function c(n){console.log(`[stop-agents] ${n}`)}function l(n,o){let t;try{t=p.readlinkSync(`/proc/${o}/fd/0`)}catch{}if(!t||!t.startsWith("/dev/pts/")){c(`${n} (pid ${o}) has no PTY (fd/0 -> ${t}), skipping`);return}c(`Injecting Escape into ${n} (pid ${o}, tty ${t})`);try{s(`sudo python3 -c "
3
4
  import fcntl, os, struct
4
- fd = os.open('${c}', os.O_WRONLY)
5
+ fd = os.open('${t}', os.O_WRONLY)
5
6
  fcntl.ioctl(fd, 0x5412, struct.pack('b', 0x1b))
6
7
  os.close(fd)
7
- "`,{stdio:"ignore"}),n(`Escape injected for ${t}`)}catch{n(`TIOCSTI failed for ${t}, falling back to killing children`);try{let s=e(`pgrep -P ${o} 2>/dev/null`,{encoding:"utf8"}).trim().split(`
8
- `);for(let i of s)if(i)try{process.kill(parseInt(i),"SIGINT")}catch{}}catch{}}}r("Claude Code","^claude$");r("Codex","@openai/codex");n("Done");
8
+ "`,{stdio:"ignore"}),c(`Escape injected for ${n} (pid ${o})`)}catch{c(`TIOCSTI failed for ${n} (pid ${o}), falling back to SIGINT-ing children`);try{let e=s(`pgrep -P ${o} 2>/dev/null`,{encoding:"utf8"}).trim().split(`
9
+ `);for(let i of e)if(i)try{process.kill(parseInt(i),"SIGINT")}catch{}}catch{}}}function f(n,o){let t=r(o);if(t.length===0){c(`No ${n} process found`);return}c(`Found ${t.length} ${n} instance${t.length===1?"":"s"}: ${t.join(", ")}`);for(let e of t)l(n,e)}f("Claude Code","claude");f("Codex","codex");c("Done");