@typhons/sandbox-tools 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ipc.js CHANGED
@@ -1 +1,5 @@
1
- import l from"net";import a from"fs";var S="/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,m=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 w(r,t){if(!r||r.destroyed)return!1;let e=Buffer.from(JSON.stringify(t)),n=Buffer.alloc(5);return n[0]=m,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===m)try{this.onJSON(JSON.parse(s.toString()))}catch(i){console.error(`[ipc] bad JSON: ${i.message}`)}}}};function I(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",p=>{n&&n(p)}))}return f(),{getSocket(){return i},close(){o=!0,c&&clearTimeout(c),i&&i.destroy()}}}function T(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 b(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,S as SOCK_PATH,N as connectIPC,I as createIPCServer,b as decodeDataFrame,T as encodeDataFrame,x as sendBinary,w as sendJSON};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typhons/sandbox-tools",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sandbox-relay-client": "./relay-client.js",
package/relay-client.js CHANGED
@@ -1,3 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import b from"ws";import d from"fs";import{spawn as P,execSync as N,execFileSync as F}from"child_process";import{dirname as L}from"path";import{fileURLToPath as J}from"url";import U from"net";var $="/tmp/session-proxy.sock",Q=2*1024*1024;var B=1,I=2;function A(e,n){if(!e||e.destroyed)return!1;let t=Buffer.alloc(5);return t[0]=B,t.writeUInt32LE(n.length,1),e.write(t),e.write(n),!0}function O(e,n){if(!e||e.destroyed)return!1;let t=Buffer.from(JSON.stringify(n)),c=Buffer.alloc(5);return c[0]=I,c.writeUInt32LE(t.length,1),e.write(c),e.write(t),!0}var E=class{constructor(n,t){this.onBinary=n,this.onJSON=t,this.buf=Buffer.alloc(0)}feed(n){for(this.buf=this.buf.length?Buffer.concat([this.buf,n]):n;this.buf.length>=5;){let t=this.buf[0],c=this.buf.readUInt32LE(1);if(this.buf.length<5+c)break;let i=this.buf.slice(5,5+c);if(this.buf=this.buf.slice(5+c),t===B)this.onBinary(i);else if(t===I)try{this.onJSON(JSON.parse(i.toString()))}catch(o){console.error(`[ipc] bad JSON: ${o.message}`)}}}};function w(e,{onConnect:n,onClose:t,onError:c,retryMs:i=500}={}){let o=null,h=!1,a=null;function u(){h||(o=U.createConnection(e),o.on("connect",()=>{n&&n(o)}),o.on("close",()=>{o=null,t&&t(),h||(a=setTimeout(u,i))}),o.on("error",T=>{c&&c(T)}))}return u(),{getSocket(){return o},close(){h=!0,a&&clearTimeout(a),o&&o.destroy()}}}var ae=L(J(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 H=process.env.RELAY_HOST||"ssh.typhons.dev",Y="/run/e2b/.RELAY_TOKEN",K=500,G=3e3,v=15e3,M=6e4,C="@typhons/sandbox-tools";function V(){try{return d.readFileSync(Y,"utf8").trim()}catch{}return""}function R(){try{return d.readFileSync("/run/e2b/.E2B_SANDBOX_ID","utf8").trim()}catch{}return"unknown"}function r(e){console.log(`[relay-client ${new Date().toISOString().slice(11,19)}] ${e}`)}var y="unknown";function W(){if(process.env.RELAY_URL)return process.env.RELAY_URL;y=R();try{let e=d.readFileSync("/run/e2b/.SAVED_SANDBOX_ID","utf8").trim();if(e&&e!==y){r(`Clone detected (${e} -> ${y}), interrupting agents...`);try{N("/opt/sandbox/bin/stop-agents",{timeout:1e4,stdio:"inherit"})}catch{}try{d.writeFileSync("/run/e2b/.SAVED_SANDBOX_ID",y)}catch{}}}catch{}return`wss://${H}/tunnel/${y}`}var p=null,x=null;function X(){return w($,{onConnect(n){r("Connected to session-proxy via IPC"),p=n,x=new E(t=>{s&&s.readyState===b.OPEN&&s.send(t)},t=>{s&&s.readyState===b.OPEN&&s.send(JSON.stringify(t))}),n.on("data",t=>x.feed(t))},onClose(){r("IPC connection to session-proxy lost, will retry..."),p=null,x=null},onError(n){n.code!=="ENOENT"&&n.code!=="ECONNREFUSED"&&r(`IPC error: ${n.message}`)}})}var s=null,_=null,l=0,S=null;function z(){if(s){try{s.terminate()}catch{}s=null}S&&(clearTimeout(S),S=null),S=setTimeout(()=>{S=null,k()},K)}function k(){let e=++l,n=W();r(`Connecting to ${n}... (gen=${e}, sandbox=${y})`);let t={},c=V();if(c&&(t.Authorization=`Bearer ${c}`),s){try{s.terminate()}catch{}s=null}let i=new b(n,{headers:t});s=i;let o=null,h=!0,a=setTimeout(()=>{if(e===l){r(`Handshake timeout after ${v}ms \u2014 aborting and reconnecting`);try{i.terminate()}catch{}u()}},v);function u(){a&&(clearTimeout(a),a=null),o&&(clearInterval(o),o=null),e===l&&z()}function T(){h=!0,o=setInterval(()=>{if(e!==l){clearInterval(o);return}if(!h){r("No pong received \u2014 connection dead, reconnecting"),u();return}h=!1;try{i.ping()}catch{u()}},G)}i.on("open",()=>{if(e!==l){i.terminate();return}a&&(clearTimeout(a),a=null),r(`Connected${_?` (reconnect, port ${_})`:""} (gen=${e})`),T()}),i.on("pong",()=>{e===l&&(h=!0)}),i.on("message",(f,m)=>{if(e===l){if(!m){try{j(JSON.parse(f.toString()))}catch(g){r(`Bad control msg: ${g.message}`)}return}p&&!p.destroyed&&A(p,Buffer.from(f))}}),i.on("close",(f,m)=>{if(e!==l)return;let g=m?m.toString():"";f===4100&&(r(`Replaced by newer connection (code 4100, reason=${g}) \u2014 exiting`),process.exit(42)),r(`Data WS closed: code=${f}, reason=${g}`),u()}),i.on("error",f=>{e===l&&(r(`Data WS error: ${f.message} (code=${f.code||"none"})`),u())}),i.on("unexpected-response",(f,m)=>{e===l&&(r(`Data WS unexpected response: status=${m.statusCode}`),u())})}function j(e){if(e.type==="port"){_=e.port,r(`Assigned port ${e.port}`);return}if(e.type==="name"){if(r(`Name assigned: ${e.name}`),e.name&&/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(e.name)){try{F("hostname",[e.name],{timeout:5e3,stdio:"ignore"}),d.writeFileSync("/etc/hostname",e.name+`
3
- `)}catch{}try{d.writeFileSync("/run/e2b/.DEVBOX_NAME",e.name)}catch{}}return}p&&!p.destroyed&&O(p,e)}function D(){try{let e=N(`npm view ${C} version 2>/dev/null`,{timeout:15e3}).toString().trim();if(!e)return;let n="";try{n=JSON.parse(d.readFileSync("/opt/sandbox/lib/node_modules/@typhons/sandbox-tools/package.json","utf8")).version||""}catch{return}if(e!==n){r(`Update available: ${n} \u2192 ${e}, installing...`),N(`npm install -g --prefix /opt/sandbox ${C}@latest`,{timeout:12e4,stdio:"inherit"});try{N('pkill -f "check-claude-active.*--daemon" 2>/dev/null',{shell:!0,timeout:5e3})}catch{}try{let t=d.openSync("/tmp/claude-keepalive.log","a");P("/opt/sandbox/bin/check-claude-active",["--daemon"],{detached:!0,stdio:["ignore",t,t]}).unref(),d.closeSync(t)}catch{}r("Update installed, exiting for restart..."),process.exit(0)}}catch{}}var le=X();k();setTimeout(D,1e4);setInterval(D,M);r(`Relay client started (sandbox=${R()})`);
2
+ import $ from"ws";import y from"fs";import{spawn as z,execSync as N,execFileSync as j}from"child_process";import{dirname as Q}from"path";import{fileURLToPath as Z}from"url";import W from"net";import d from"fs";var D=process.env.SESSION_PROXY_SOCK||"/tmp/session-proxy.sock";function b(e,n="process"){if(process.env.RELAY_CLIENT_SKIP_LOCK==="1")return{acquired:!0,skipped:!0};let t=process.pid;function i(){try{let c=d.openSync(e,d.constants.O_WRONLY|d.constants.O_CREAT|d.constants.O_EXCL,420);return d.writeSync(c,`${t}
3
+ ${n}
4
+ `),d.closeSync(c),{ok:!0}}catch(c){return c.code!=="EEXIST"?{ok:!1,error:c.message}:{ok:!1,exists:!0}}}let s=i();if(s.ok)return x(e,t),{acquired:!0,lockPath:e};let r;try{r=d.readFileSync(e,"utf8")}catch{}let a=parseInt((r||"").split(`
5
+ `)[0],10);if(Number.isFinite(a)&&a!==t)try{return process.kill(a,0),{acquired:!1,heldBy:a,lockPath:e}}catch(c){if(c.code==="ESRCH"){try{d.unlinkSync(e)}catch{}if(s=i(),s.ok)return x(e,t),{acquired:!0,lockPath:e,reclaimed:!0}}}return{acquired:!1,heldBy:a||"unknown",lockPath:e}}function x(e,n){let t=()=>{try{let i=d.readFileSync(e,"utf8").split(`
6
+ `)[0];parseInt(i,10)===n&&d.unlinkSync(e)}catch{}};process.on("exit",t),process.on("SIGINT",()=>{t(),process.exit(130)}),process.on("SIGTERM",()=>{t(),process.exit(143)})}var Se=2*1024*1024;var k=1,w=2;function U(e,n){if(!e||e.destroyed)return!1;let t=Buffer.alloc(5);return t[0]=k,t.writeUInt32LE(n.length,1),e.write(t),e.write(n),!0}function Y(e,n){if(!e||e.destroyed)return!1;let t=Buffer.from(JSON.stringify(n)),i=Buffer.alloc(5);return i[0]=w,i.writeUInt32LE(t.length,1),e.write(i),e.write(t),!0}var T=class{constructor(n,t){this.onBinary=n,this.onJSON=t,this.buf=Buffer.alloc(0)}feed(n){for(this.buf=this.buf.length?Buffer.concat([this.buf,n]):n;this.buf.length>=5;){let t=this.buf[0],i=this.buf.readUInt32LE(1);if(this.buf.length<5+i)break;let s=this.buf.slice(5,5+i);if(this.buf=this.buf.slice(5+i),t===k)this.onBinary(s);else if(t===w)try{this.onJSON(JSON.parse(s.toString()))}catch(r){console.error(`[ipc] bad JSON: ${r.message}`)}}}};function M(e,{onConnect:n,onClose:t,onError:i,retryMs:s=500}={}){let r=null,a=!1,c=null;function f(){a||(r=W.createConnection(e),r.on("connect",()=>{n&&n(r)}),r.on("close",()=>{r=null,t&&t(),a||(c=setTimeout(f,s))}),r.on("error",R=>{i&&i(R)}))}return f(),{getSocket(){return r},close(){a=!0,c&&clearTimeout(c),r&&r.destroy()}}}var $e=Q(Z(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 ee=process.env.RELAY_HOST||"ssh.typhons.dev",te=process.env.RELAY_SCHEME||"wss",I=process.env.E2B_RUN_DIR||"/run/e2b",ne=`${I}/.RELAY_TOKEN`,re=`${I}/.E2B_SANDBOX_ID`,P=`${I}/.SAVED_SANDBOX_ID`,oe=`${I}/.DEVBOX_NAME`,ce=parseInt(process.env.RELAY_CLIENT_RECONNECT_DELAY_MS||"1500",10),ie=3e3,F=15e3,se=6e4,H=parseInt(process.env.RELAY_CLIENT_EVICTION_RETRY_MS||"15000",10),K=parseInt(process.env.RELAY_CLIENT_EVICTION_GIVEUP_MS||String(30*60*1e3),10),ae=parseInt(process.env.RELAY_CLIENT_ID_RECHECK_MS||"10000",10),J="@typhons/sandbox-tools",le=process.env.RELAY_CLIENT_DISABLE_UPDATE==="1";function ue(){try{return y.readFileSync(ne,"utf8").trim()}catch{}return""}function B(){try{return y.readFileSync(re,"utf8").trim()}catch{}return"unknown"}function o(e){console.log(`[relay-client ${new Date().toISOString().slice(11,19)}] ${e}`)}var h="unknown";function de(){if(process.env.RELAY_URL)return process.env.RELAY_URL;h=B();try{let e=y.readFileSync(P,"utf8").trim();if(e&&e!==h){o(`Clone detected (${e} -> ${h}), interrupting agents...`);try{N("/opt/sandbox/bin/stop-agents",{timeout:1e4,stdio:"inherit"})}catch{}try{y.writeFileSync(P,h)}catch{}}}catch{}return`${te}://${ee}/tunnel/${h}`}var E=null,C=null;function fe(){return M(D,{onConnect(n){o("Connected to session-proxy via IPC"),E=n,C=new T(t=>{l&&l.readyState===$.OPEN&&l.send(t)},t=>{l&&l.readyState===$.OPEN&&l.send(JSON.stringify(t))}),n.on("data",t=>C.feed(t))},onClose(){o("IPC connection to session-proxy lost, will retry..."),E=null,C=null},onError(n){n.code!=="ENOENT"&&n.code!=="ECONNREFUSED"&&o(`IPC error: ${n.message}`)}})}var l=null,L=null,u=0,_=null,m=null;function v(e=ce){if(l){try{l.terminate()}catch{}l=null}_&&(clearTimeout(_),_=null),_=setTimeout(()=>{_=null,q()},e)}function q(){let e=++u,n=de();o(`Connecting to ${n}... (gen=${e}, sandbox=${h})`);let t={},i=ue();if(i&&(t.Authorization=`Bearer ${i}`),l){try{l.terminate()}catch{}l=null}let s=new $(n,{headers:t});l=s;let r=null,a=!0,c=setTimeout(()=>{if(e===u){o(`Handshake timeout after ${F}ms \u2014 aborting and reconnecting`);try{s.terminate()}catch{}f()}},F);function f(){c&&(clearTimeout(c),c=null),r&&(clearInterval(r),r=null),e===u&&v()}function R(){a=!0,r=setInterval(()=>{if(e!==u){clearInterval(r);return}if(!a){o("No pong received \u2014 connection dead, reconnecting"),f();return}a=!1;try{s.ping()}catch{f()}},ie)}s.on("open",()=>{if(e!==u){s.terminate();return}c&&(clearTimeout(c),c=null),o(`Connected${L?` (reconnect, port ${L})`:""} (gen=${e})`),R()}),s.on("pong",()=>{e===u&&(a=!0,m!==null&&(o(`Reclaimed slot after eviction (was evicted for ${Math.round((Date.now()-m)/1e3)}s)`),m=null))}),s.on("message",(p,S)=>{if(e===u){if(!S){try{pe(JSON.parse(p.toString()))}catch(g){o(`Bad control msg: ${g.message}`)}return}E&&!E.destroyed&&U(E,Buffer.from(p))}}),s.on("close",(p,S)=>{if(e!==u)return;let g=S?S.toString():"";if(p===4100){let O=Date.now();m===null&&(m=O);let A=O-m;A>=K&&(o(`Replaced by newer connection (code 4100) \u2014 gave up after ${Math.round(A/1e3)}s, exiting`),process.exit(42)),o(`Replaced by newer connection (code 4100, reason=${g}) \u2014 retrying in ${H}ms (${Math.round(A/1e3)}s evicted, giveup at ${Math.round(K/1e3)}s)`),c&&(clearTimeout(c),c=null),r&&(clearInterval(r),r=null),e===u&&v(H);return}o(`Data WS closed: code=${p}, reason=${g}`),f()}),s.on("error",p=>{e===u&&(o(`Data WS error: ${p.message} (code=${p.code||"none"})`),f())}),s.on("unexpected-response",(p,S)=>{e===u&&(o(`Data WS unexpected response: status=${S.statusCode}`),f())})}function pe(e){if(e.type==="port"){L=e.port,o(`Assigned port ${e.port}`);return}if(e.type==="name"){if(o(`Name assigned: ${e.name}`),e.name&&/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(e.name)){try{j("hostname",[e.name],{timeout:5e3,stdio:"ignore"}),y.writeFileSync("/etc/hostname",e.name+`
7
+ `)}catch{}try{y.writeFileSync(oe,e.name)}catch{}}return}E&&!E.destroyed&&Y(E,e)}function V(){try{let e=N(`npm view ${J} version 2>/dev/null`,{timeout:15e3}).toString().trim();if(!e)return;let n="";try{n=JSON.parse(y.readFileSync("/opt/sandbox/lib/node_modules/@typhons/sandbox-tools/package.json","utf8")).version||""}catch{return}if(e!==n){o(`Update available: ${n} \u2192 ${e}, installing...`),N(`npm install -g --prefix /opt/sandbox ${J}@latest`,{timeout:12e4,stdio:"inherit"});try{N('pkill -f "check-claude-active.*--daemon" 2>/dev/null',{shell:!0,timeout:5e3})}catch{}try{let t=y.openSync("/tmp/claude-keepalive.log","a");z("/opt/sandbox/bin/check-claude-active",["--daemon"],{detached:!0,stdio:["ignore",t,t]}).unref(),y.closeSync(t)}catch{}o("Update installed, exiting for restart..."),process.exit(0)}}catch{}}function he(){if(process.env.RELAY_URL)return;let e=B();!e||e==="unknown"||e!==h&&(o(`Sandbox ID on disk changed (${h} -> ${e}), forcing reconnect`),h=e,v())}var G=process.env.RELAY_CLIENT_LOCK_PATH||`${I}/.relay-client.lock`,X=b(G,"relay-client");X.acquired||(o(`Another relay-client is already running (pid=${X.heldBy}, lock=${G}). Refusing to start. Set RELAY_CLIENT_SKIP_LOCK=1 to override.`),process.exit(2));var Le=fe();q();setInterval(he,ae);le||(setTimeout(V,1e4),setInterval(V,se));o(`Relay client started (sandbox=${B()})`);
@@ -1,5 +1,13 @@
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.
3
11
 
4
12
  BIN="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
5
13
  export SSH_PORT="${SSH_PORT:-2222}"
@@ -11,6 +19,11 @@ log() { echo "[watchdog $(date +%H:%M:%S)] $*"; }
11
19
  while true; do
12
20
  node "$BIN/session-proxy.js" 2>&1
13
21
  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
14
27
  log "session-proxy exited (code $EXIT_CODE), restarting in 2s..."
15
28
  sleep 2
16
29
  done
@@ -23,10 +36,15 @@ while true; do
23
36
  node "$BIN/relay-client.js" 2>&1
24
37
  EXIT_CODE=$?
25
38
  if [ "$EXIT_CODE" = "42" ]; then
26
- log "relay-client replaced by newer connection (exit 42), stopping all"
39
+ log "relay-client gave up after sustained eviction (exit 42), stopping all"
27
40
  kill $SESSION_PROXY_PID 2>/dev/null
28
41
  exit 0
29
42
  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
30
48
  log "relay-client exited (code $EXIT_CODE), restarting in 2s..."
31
49
  sleep 2
32
50
  done
package/session-proxy.js CHANGED
@@ -1,2 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import K from"net";import j from"http";import{WebSocketServer as G}from"ws";import z from"fs";import{execFileSync as I}from"child_process";import Y from"net";import C from"fs";var x="/tmp/session-proxy.sock",q=2*1024*1024,y=class{constructor(t=q){this.capacity=t,this.chunks=[],this.totalBytes=0,this.trimmedTo=0}append(t){let s=this.totalBytes;return this.chunks.push({offset:s,data:Buffer.from(t)}),this.totalBytes+=t.length,this._trim(),s}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 s=[];for(let r of this.chunks){if(r.offset+r.data.length<=t)continue;let o=Math.max(0,t-r.offset);s.push(r.data.slice(o))}return Buffer.concat(s)}_trim(){for(;this.chunks.length>1&&!(this.totalBytes-this.chunks[0].offset<=this.capacity);){let s=this.chunks.shift();this.trimmedTo=s.offset+s.data.length}}},_=1,R=2;function D(e,t){if(!e||e.destroyed)return!1;let s=Buffer.alloc(5);return s[0]=_,s.writeUInt32LE(t.length,1),e.write(s),e.write(t),!0}function B(e,t){if(!e||e.destroyed)return!1;let s=Buffer.from(JSON.stringify(t)),r=Buffer.alloc(5);return r[0]=R,r.writeUInt32LE(s.length,1),e.write(r),e.write(s),!0}var w=class{constructor(t,s){this.onBinary=t,this.onJSON=s,this.buf=Buffer.alloc(0)}feed(t){for(this.buf=this.buf.length?Buffer.concat([this.buf,t]):t;this.buf.length>=5;){let s=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),s===_)this.onBinary(n);else if(s===R)try{this.onJSON(JSON.parse(n.toString()))}catch(o){console.error(`[ipc] bad JSON: ${o.message}`)}}}};function H(e,t){try{C.unlinkSync(e)}catch{}let s=Y.createServer(t);return s.listen(e,()=>{try{C.chmodSync(e,511)}catch{}}),s}function F(e,t,s,r){let n=Buffer.alloc(17);return n.write(e,0,8,"ascii"),n[8]=t,n.writeBigUInt64BE(BigInt(s),9),Buffer.concat([n,r])}function L(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 v;try{v=(await import("@homebridge/node-pty-prebuilt-multiarch")).default}catch{try{v=(await import("node-pty")).default}catch{}}var A=parseInt(process.env.SSH_PORT||"22"),U=parseInt(process.env.WS_PORT||"49999");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 a=new Map,c=null;function P(e,t,s,r){if(!c||c.destroyed)return!1;let n=F(e,t,s,r);return D(c,n)}function d(e){!c||c.destroyed||B(c,e)}function h(e,t){let s=a.get(e);if(s){if(i(`session ${e}: closing \u2014 ${t}`),s.socket&&!s.socket.destroyed&&s.socket.destroy(),s.ptyProcess){s.closed=!0;try{s.ptyProcess.kill()}catch{}}d({type:"close",session:e}),a.delete(e)}}function X(e,t,s){let r=a.get(e);if(!r||!(r.ptyProcess||r.socket)||r.socket&&r.socket.destroyed||r.closed)return;let o=r.user2sshd.received;if(t+s.length<=o)return;let u=s;if(t<o&&(u=s.slice(o-t)),t>o){h(e,`user2sshd stream gap: expected offset ${o}, got ${t}`);return}r.ptyProcess?r.ptyProcess.write(u):r.socket.write(u),r.user2sshd.received=Math.max(o,t+s.length),d({type:"ack",session:e,direction:"user2sshd",offset:r.user2sshd.received})}function J(e,t){let s=a.get(e);if(!s)return;let r;try{r=s.sshd2user.ring.sliceFrom(t)}catch(n){h(e,`ring buffer overflow on resend: ${n.message}`);return}r.length>0?(i(`session ${e}: re-sending ${r.length}b sshd\u2192user from offset ${t}`),P(e,0,t,r)):i(`session ${e}: relay has all ${t}b sshd\u2192user, nothing to re-send`)}function V(e){if(e.type==="open"){e.mode==="pty"?v?Q(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})):Z(e.session,e.sshd2userOffset||0,e.user2sshdOffset||0);return}if(e.type==="resize"){let t=a.get(e.session);if(t?.ptyProcess)try{t.ptyProcess.resize(e.cols,e.rows)}catch{}return}if(e.type==="close"){h(e.session,"relay requested close");return}if(e.type==="ack"){let t=a.get(e.session);if(!t)return;e.direction==="sshd2user"&&typeof e.offset=="number"&&(t.sshd2user.acked=Math.max(t.sshd2user.acked,e.offset));return}if(e.type==="get-authorized-keys"){let t=e.user||"root";if(t!=="root"&&!/^[a-z_][a-z0-9_-]{0,31}$/.test(t)){d({type:"authorized-keys",requestId:e.requestId,keys:null});return}let s=t==="root"?"/root":`/home/${t}`,r=null;try{r=z.readFileSync(`${s}/.ssh/authorized_keys`,"utf8")}catch{}d({type:"authorized-keys",requestId:e.requestId,keys:r});return}}function Q(e,t,s,r){let n=a.get(e);if(n&&n.ptyProcess&&!n.closed){i(`session ${e}: re-attaching PTY (relay sshd2user=${t}, user2sshd=${s}, our sshd2user=${n.sshd2user.ring.totalBytes}, user2sshd=${n.user2sshd.received})`),n.sshd2user.acked=t,d({type:"ready",session:e,sshd2userOffset:n.sshd2user.ring.totalBytes,user2sshdOffset:n.user2sshd.received}),J(e,t);return}let o=r.user||"root",u=r.shell||null,$=process.env.SHELL||"/bin/bash",T=r.cols||80,b=r.rows||24,O=r.term||"xterm-256color",m,k,S;try{let l=I("getent",["passwd",o],{timeout:2e3}).toString().trim().split(":");m=parseInt(l[2]),k=parseInt(l[3]),S=l[5]||(o==="root"?"/root":`/home/${o}`)}catch{m=o==="root"?0:void 0,k=void 0,S=o==="root"?"/root":`/home/${o}`}let E=$,N=u?["-c",u]:["-l"];i(`session ${e}: spawning PTY ${E} ${N.join(" ")} as ${o} (uid=${m}, ${T}x${b})`);let g={socket:null,ptyProcess:null,closed:!1,sshd2user:{ring:new y,acked:0},user2sshd:{received:0}};a.set(e,g);try{let f=v.spawn(E,N,{name:O,cols:T,rows:b,cwd:S,env:{...process.env,TERM:O,HOME:S,USER:o,LOGNAME:o,SHELL:$,PATH:process.env.PATH},uid:m,gid:k});g.ptyProcess=f,f.onData(l=>{let p=Buffer.from(l),M=g.sshd2user.ring.append(p);P(e,0,M,p)}),f.onExit(({exitCode:l,signal:p})=>{i(`session ${e}: PTY exited (code=${l}, signal=${p})`),g.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 Z(e,t,s){let r=a.get(e);if(r&&r.socket&&!r.socket.destroyed){i(`session ${e}: re-attaching (relay sshd2user=${t}, user2sshd=${s}, our sshd2user=${r.sshd2user.ring.totalBytes}, user2sshd=${r.user2sshd.received})`),r.sshd2user.acked=t,d({type:"ready",session:e,sshd2userOffset:r.sshd2user.ring.totalBytes,user2sshdOffset:r.user2sshd.received}),J(e,t);return}i(`session ${e}: connecting to sshd :${A}`);let n={socket:null,sshd2user:{ring:new y,acked:0},user2sshd:{received:0}};a.set(e,n);let o=K.createConnection({host:"127.0.0.1",port:A},()=>{i(`session ${e}: sshd connected`),o.setNoDelay(!0),n.socket=o,d({type:"ready",session:e,sshd2userOffset:0,user2sshdOffset:0})});n.socket=o,o.on("data",u=>{let $=n.sshd2user.ring.append(u);P(e,0,$,u)}),o.on("close",()=>h(e,"sshd closed")),o.on("error",u=>h(e,`sshd error: ${u.message}`))}var le=H(x,e=>{if(i("Relay-client connected via IPC"),c&&!c.destroyed&&(i("Replacing previous relay-client IPC connection"),c.destroy()),c=e,a.size>0){let s=[];for(let[r,n]of a)s.push({id:r,sshd2userOffset:n.sshd2user.ring.totalBytes,user2sshdOffset:n.user2sshd.received,mode:n.ptyProcess?"pty":"sshd"});B(e,{type:"existing-sessions",sessions:s})}let t=new w(s=>{let r=L(s);r&&r.direction===1&&X(r.sessionId,r.offset,r.payload)},s=>{V(s)});e.on("data",s=>t.feed(s)),e.on("close",()=>{i("Relay-client IPC disconnected"),c===e&&(c=null)}),e.on("error",s=>{i(`IPC error: ${s.message}`)})});i(`IPC server listening on ${x}`);function ee(){try{return z.readFileSync("/run/e2b/.E2B_SANDBOX_ID","utf8").trim()}catch{}return"unknown"}var W=j.createServer((e,t)=>{t.setHeader("Content-Type","application/json"),e.url==="/health"?t.end(JSON.stringify({ok:!0,sandboxId:ee(),sessions:a.size,ipcConnected:!!(c&&!c.destroyed)})):(t.writeHead(404),t.end('{"error":"not found"}'))}),te=new G({server:W});te.on("connection",e=>{i("Keepalive WS connected from relay"),e.on("close",(t,s)=>i(`Keepalive WS closed: code=${t}, reason=${s?s.toString():""}`)),e.on("error",t=>i(`Keepalive WS error: ${t.message}`))});W.listen(U,"0.0.0.0",()=>{i(`Keepalive WS server on :${U}`)});
2
+ import G from"net";import X from"http";import{WebSocketServer as j}from"ws";import K from"fs";import{execFileSync as V}from"child_process";import W from"net";import l from"fs";var k=process.env.SESSION_PROXY_SOCK||"/tmp/session-proxy.sock";function R(e,t="process"){if(process.env.RELAY_CLIENT_SKIP_LOCK==="1")return{acquired:!0,skipped:!0};let s=process.pid;function r(){try{let d=l.openSync(e,l.constants.O_WRONLY|l.constants.O_CREAT|l.constants.O_EXCL,420);return l.writeSync(d,`${s}
3
+ ${t}
4
+ `),l.closeSync(d),{ok:!0}}catch(d){return d.code!=="EEXIST"?{ok:!1,error:d.message}:{ok:!1,exists:!0}}}let n=r();if(n.ok)return N(e,s),{acquired:!0,lockPath:e};let o;try{o=l.readFileSync(e,"utf8")}catch{}let c=parseInt((o||"").split(`
5
+ `)[0],10);if(Number.isFinite(c)&&c!==s)try{return process.kill(c,0),{acquired:!1,heldBy:c,lockPath:e}}catch(d){if(d.code==="ESRCH"){try{l.unlinkSync(e)}catch{}if(n=r(),n.ok)return N(e,s),{acquired:!0,lockPath:e,reclaimed:!0}}}return{acquired:!1,heldBy:c||"unknown",lockPath:e}}function N(e,t){let s=()=>{try{let r=l.readFileSync(e,"utf8").split(`
6
+ `)[0];parseInt(r,10)===t&&l.unlinkSync(e)}catch{}};process.on("exit",s),process.on("SIGINT",()=>{s(),process.exit(130)}),process.on("SIGTERM",()=>{s(),process.exit(143)})}var M=2*1024*1024,$=class{constructor(t=M){this.capacity=t,this.chunks=[],this.totalBytes=0,this.trimmedTo=0}append(t){let s=this.totalBytes;return this.chunks.push({offset:s,data:Buffer.from(t)}),this.totalBytes+=t.length,this._trim(),s}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 s=[];for(let r of this.chunks){if(r.offset+r.data.length<=t)continue;let o=Math.max(0,t-r.offset);s.push(r.data.slice(o))}return Buffer.concat(s)}_trim(){for(;this.chunks.length>1&&!(this.totalBytes-this.chunks[0].offset<=this.capacity);){let s=this.chunks.shift();this.trimmedTo=s.offset+s.data.length}}},L=1,I=2;function A(e,t){if(!e||e.destroyed)return!1;let s=Buffer.alloc(5);return s[0]=L,s.writeUInt32LE(t.length,1),e.write(s),e.write(t),!0}function O(e,t){if(!e||e.destroyed)return!1;let s=Buffer.from(JSON.stringify(t)),r=Buffer.alloc(5);return r[0]=I,r.writeUInt32LE(s.length,1),e.write(r),e.write(s),!0}var v=class{constructor(t,s){this.onBinary=t,this.onJSON=s,this.buf=Buffer.alloc(0)}feed(t){for(this.buf=this.buf.length?Buffer.concat([this.buf,t]):t;this.buf.length>=5;){let s=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),s===L)this.onBinary(n);else if(s===I)try{this.onJSON(JSON.parse(n.toString()))}catch(o){console.error(`[ipc] bad JSON: ${o.message}`)}}}};function q(e,t){try{l.unlinkSync(e)}catch{}let s=W.createServer(t);return s.listen(e,()=>{try{l.chmodSync(e,511)}catch{}}),s}function F(e,t,s,r){let n=Buffer.alloc(17);return n.write(e,0,8,"ascii"),n[8]=t,n.writeBigUInt64BE(BigInt(s),9),Buffer.concat([n,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 w;try{w=(await import("@homebridge/node-pty-prebuilt-multiarch")).default}catch{try{w=(await import("node-pty")).default}catch{}}var D=parseInt(process.env.SSH_PORT||"22"),Y=parseInt(process.env.WS_PORT||"49999");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 a=new Map,u=null;function E(e,t,s,r){if(!u||u.destroyed)return!1;let n=F(e,t,s,r);return A(u,n)}function f(e){!u||u.destroyed||O(u,e)}function y(e,t){let s=a.get(e);if(s){if(i(`session ${e}: closing \u2014 ${t}`),s.socket&&!s.socket.destroyed&&s.socket.destroy(),s.ptyProcess){s.closed=!0;try{s.ptyProcess.kill()}catch{}}f({type:"close",session:e}),a.delete(e)}}function Q(e,t,s){let r=a.get(e);if(!r||!(r.ptyProcess||r.socket)||r.socket&&r.socket.destroyed||r.closed)return;let o=r.user2sshd.received;if(t+s.length<=o)return;let c=s;if(t<o&&(c=s.slice(o-t)),t>o){y(e,`user2sshd stream gap: expected offset ${o}, got ${t}`);return}r.ptyProcess?r.ptyProcess.write(c):r.socket.write(c),r.user2sshd.received=Math.max(o,t+s.length),f({type:"ack",session:e,direction:"user2sshd",offset:r.user2sshd.received})}function U(e,t){let s=a.get(e);if(!s)return;let r;try{r=s.sshd2user.ring.sliceFrom(t)}catch(n){y(e,`ring buffer overflow on resend: ${n.message}`);return}r.length>0?(i(`session ${e}: re-sending ${r.length}b sshd\u2192user from offset ${t}`),E(e,0,t,r)):i(`session ${e}: relay has all ${t}b sshd\u2192user, nothing to re-send`)}function Z(e){if(e.type==="open"){e.mode==="pty"?w?ee(e.session,e.sshd2userOffset||0,e.user2sshdOffset||0,e):(i(`session ${e.session}: PTY mode requested but node-pty not available \u2014 rejecting`),f({type:"close",session:e.session})):te(e.session,e.sshd2userOffset||0,e.user2sshdOffset||0);return}if(e.type==="resize"){let t=a.get(e.session);if(t?.ptyProcess)try{t.ptyProcess.resize(e.cols,e.rows)}catch{}return}if(e.type==="close"){y(e.session,"relay requested close");return}if(e.type==="ack"){let t=a.get(e.session);if(!t)return;e.direction==="sshd2user"&&typeof e.offset=="number"&&(t.sshd2user.acked=Math.max(t.sshd2user.acked,e.offset));return}if(e.type==="get-authorized-keys"){let t=e.user||"root";if(t!=="root"&&!/^[a-z_][a-z0-9_-]{0,31}$/.test(t)){f({type:"authorized-keys",requestId:e.requestId,keys:null});return}let s=t==="root"?"/root":`/home/${t}`,r=null;try{r=K.readFileSync(`${s}/.ssh/authorized_keys`,"utf8")}catch{}f({type:"authorized-keys",requestId:e.requestId,keys:r});return}}function ee(e,t,s,r){let n=a.get(e);if(n&&n.ptyProcess&&!n.closed){i(`session ${e}: re-attaching PTY (relay sshd2user=${t}, user2sshd=${s}, our sshd2user=${n.sshd2user.ring.totalBytes}, user2sshd=${n.user2sshd.received})`),n.sshd2user.acked=t,f({type:"ready",session:e,sshd2userOffset:n.sshd2user.ring.totalBytes,user2sshdOffset:n.user2sshd.received}),U(e,t);return}let o=r.user||"root",c=r.shell||null,d=process.env.SHELL||"/bin/bash",T=r.cols||80,P=r.rows||24,_=r.term||"xterm-256color",m,B,g;try{let h=V("getent",["passwd",o],{timeout:2e3}).toString().trim().split(":");m=parseInt(h[2]),B=parseInt(h[3]),g=h[5]||(o==="root"?"/root":`/home/${o}`)}catch{m=o==="root"?0:void 0,B=void 0,g=o==="root"?"/root":`/home/${o}`}let C=d,b=c?["-c",c]:["-l"];i(`session ${e}: spawning PTY ${C} ${b.join(" ")} as ${o} (uid=${m}, ${T}x${P})`);let x={socket:null,ptyProcess:null,closed:!1,sshd2user:{ring:new $,acked:0},user2sshd:{received:0}};a.set(e,x);try{let p=w.spawn(C,b,{name:_,cols:T,rows:P,cwd:g,env:{...process.env,TERM:_,HOME:g,USER:o,LOGNAME:o,SHELL:d,PATH:process.env.PATH},uid:m,gid:B});x.ptyProcess=p,p.onData(h=>{let S=Buffer.from(h),J=x.sshd2user.ring.append(S);E(e,0,J,S)}),p.onExit(({exitCode:h,signal:S})=>{i(`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){i(`session ${e}: PTY spawn failed: ${p.message}`),y(e,`pty spawn error: ${p.message}`)}}function te(e,t,s){let r=a.get(e);if(r&&r.socket&&!r.socket.destroyed){i(`session ${e}: re-attaching (relay sshd2user=${t}, user2sshd=${s}, our sshd2user=${r.sshd2user.ring.totalBytes}, user2sshd=${r.user2sshd.received})`),r.sshd2user.acked=t,f({type:"ready",session:e,sshd2userOffset:r.sshd2user.ring.totalBytes,user2sshdOffset:r.user2sshd.received}),U(e,t);return}i(`session ${e}: connecting to sshd :${D}`);let n={socket:null,sshd2user:{ring:new $,acked:0},user2sshd:{received:0}};a.set(e,n);let o=G.createConnection({host:"127.0.0.1",port:D},()=>{i(`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",c=>{let d=n.sshd2user.ring.append(c);E(e,0,d,c)}),o.on("close",()=>y(e,"sshd closed")),o.on("error",c=>y(e,`sshd error: ${c.message}`))}{let e=process.env.SESSION_PROXY_LOCK_PATH||`${k}.lock`,t=R(e,"session-proxy");t.acquired||(i(`Another session-proxy is already running (pid=${t.heldBy}, lock=${e}). Refusing to start. Set RELAY_CLIENT_SKIP_LOCK=1 to override.`),process.exit(2))}var he=q(k,e=>{if(i("Relay-client connected via IPC"),u&&!u.destroyed&&(i("Replacing previous relay-client IPC connection"),u.destroy()),u=e,a.size>0){let s=[];for(let[r,n]of a)s.push({id:r,sshd2userOffset:n.sshd2user.ring.totalBytes,user2sshdOffset:n.user2sshd.received,mode:n.ptyProcess?"pty":"sshd"});O(e,{type:"existing-sessions",sessions:s})}let t=new v(s=>{let r=H(s);r&&r.direction===1&&Q(r.sessionId,r.offset,r.payload)},s=>{Z(s)});e.on("data",s=>t.feed(s)),e.on("close",()=>{i("Relay-client IPC disconnected"),u===e&&(u=null)}),e.on("error",s=>{i(`IPC error: ${s.message}`)})});i(`IPC server listening on ${k}`);function se(){try{return K.readFileSync("/run/e2b/.E2B_SANDBOX_ID","utf8").trim()}catch{}return"unknown"}var z=X.createServer((e,t)=>{t.setHeader("Content-Type","application/json"),e.url==="/health"?t.end(JSON.stringify({ok:!0,sandboxId:se(),sessions:a.size,ipcConnected:!!(u&&!u.destroyed)})):(t.writeHead(404),t.end('{"error":"not found"}'))}),re=new j({server:z});re.on("connection",e=>{i("Keepalive WS connected from relay"),e.on("close",(t,s)=>i(`Keepalive WS closed: code=${t}, reason=${s?s.toString():""}`)),e.on("error",t=>i(`Keepalive WS error: ${t.message}`))});z.listen(Y,"0.0.0.0",()=>{i(`Keepalive WS server on :${Y}`)});