@typhons/sandbox-tools 0.6.5 → 0.6.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typhons/sandbox-tools",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sandbox-relay-client": "./relay-client.js",
package/relay-client.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import A from"ws";import bt from"http";import a from"fs";import b from"path";import Nt from"crypto";import{spawn as at,execSync as g,execFileSync as Rt}from"child_process";import{dirname as lt}from"path";import{fileURLToPath as ut}from"url";import St from"net";var Y=process.env.SESSION_PROXY_SOCK||"/tmp/session-proxy.sock",Gt=2*1024*1024;var J=1,j=2;function K(t,n){if(!t||t.destroyed)return!1;let e=Buffer.alloc(5);return e[0]=J,e.writeUInt32LE(n.length,1),t.write(e),t.write(n),!0}function W(t,n){if(!t||t.destroyed)return!1;let e=Buffer.from(JSON.stringify(n)),r=Buffer.alloc(5);return r[0]=j,r.writeUInt32LE(e.length,1),t.write(r),t.write(e),!0}var v=class{constructor(n,e){this.onBinary=n,this.onJSON=e,this.buf=Buffer.alloc(0)}feed(n){for(this.buf=this.buf.length?Buffer.concat([this.buf,n]):n;this.buf.length>=5;){let e=this.buf[0],r=this.buf.readUInt32LE(1);if(this.buf.length<5+r)break;let s=this.buf.slice(5,5+r);if(this.buf=this.buf.slice(5+r),e===J)this.onBinary(s);else if(e===j)try{this.onJSON(JSON.parse(s.toString()))}catch(o){console.error(`[ipc] bad JSON: ${o.message}`)}}}};function G(t,{onConnect:n,onClose:e,onError:r,retryMs:s=500}={}){let o=null,l=!1,i=null;function u(){l||(o=St.createConnection(t),o.on("connect",()=>{n&&n(o)}),o.on("close",()=>{o=null,e&&e(),l||(i=setTimeout(u,s))}),o.on("error",T=>{r&&r(T)}))}return u(),{getSocket(){return o},close(){l=!0,i&&clearTimeout(i),o&&o.destroy()}}}import E from"fs";import O from"path";var V="# typhons-managed-hostname";function Et(t){let n=t.replace(/\.local$/,"");return`127.0.1.1 ${n===t?t:`${t} ${n}`} ${V}`}function gt(t,n,e){let r=O.dirname(t),s=O.basename(t),o=O.join(r,`.${s}.tmp.${process.pid}.${Date.now()}`);try{E.writeFileSync(o,n,{mode:e})}catch{try{E.unlinkSync(o)}catch{}return!1}try{return E.renameSync(o,t),!0}catch{try{E.unlinkSync(o)}catch{}return!1}}function X(t,n="/etc/hosts"){let e=Et(t),r="",s=420;try{r=E.readFileSync(n,"utf8"),s=E.statSync(n).mode&511}catch{}let o=r.split(`
3
- `);o.length>0&&o[o.length-1]===""&&o.pop();let l=o.filter(u=>!u.includes(V));l.push(e);let i=l.join(`
2
+ import A from"ws";import Ne from"http";import a from"fs";import b from"path";import Re from"crypto";import{spawn as ae,execSync as g,execFileSync as ve}from"child_process";import{dirname as le}from"path";import{fileURLToPath as ue}from"url";import Ee from"net";var Y=process.env.SESSION_PROXY_SOCK||"/tmp/session-proxy.sock",Ve=2*1024*1024;var J=1,j=2;function K(e,n){if(!e||e.destroyed)return!1;let t=Buffer.alloc(5);return t[0]=J,t.writeUInt32LE(n.length,1),e.write(t),e.write(n),!0}function W(e,n){if(!e||e.destroyed)return!1;let t=Buffer.from(JSON.stringify(n)),r=Buffer.alloc(5);return r[0]=j,r.writeUInt32LE(t.length,1),e.write(r),e.write(t),!0}var v=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],r=this.buf.readUInt32LE(1);if(this.buf.length<5+r)break;let s=this.buf.slice(5,5+r);if(this.buf=this.buf.slice(5+r),t===J)this.onBinary(s);else if(t===j)try{this.onJSON(JSON.parse(s.toString()))}catch(o){console.error(`[ipc] bad JSON: ${o.message}`)}}}};function G(e,{onConnect:n,onClose:t,onError:r,retryMs:s=500}={}){let o=null,l=!1,i=null;function u(){l||(o=Ee.createConnection(e),o.on("connect",()=>{n&&n(o)}),o.on("close",()=>{o=null,t&&t(),l||(i=setTimeout(u,s))}),o.on("error",T=>{r&&r(T)}))}return u(),{getSocket(){return o},close(){l=!0,i&&clearTimeout(i),o&&o.destroy()}}}import E from"fs";import O from"path";var V="# typhons-managed-hostname";function ge(e){let n=e.replace(/\.local$/,"");return`127.0.1.1 ${n===e?e:`${e} ${n}`} ${V}`}function _e(e,n,t){let r=O.dirname(e),s=O.basename(e),o=O.join(r,`.${s}.tmp.${process.pid}.${Date.now()}`);try{E.writeFileSync(o,n,{mode:t})}catch{try{E.unlinkSync(o)}catch{}return!1}try{return E.renameSync(o,e),!0}catch{try{E.unlinkSync(o)}catch{}return!1}}function X(e,n="/etc/hosts"){let t=ge(e),r="",s=420;try{r=E.readFileSync(n,"utf8"),s=E.statSync(n).mode&511}catch{}let o=r.split(`
3
+ `);o.length>0&&o[o.length-1]===""&&o.pop();let l=o.filter(u=>!u.includes(V));l.push(t);let i=l.join(`
4
4
  `)+`
5
- `;return i===r?!1:(gt(n,i,s)||E.writeFileSync(n,i,{mode:s}),!0)}import p from"node:fs";import z from"node:path";var Z="<!-- typhons-public-url -->",Q="<!-- /typhons-public-url -->",q="d.typhons.dev",_t=[{dir:".claude",file:"CLAUDE.md"},{dir:".codex",file:"AGENTS.md"}];function Tt(t,n=q){let e=`https://${t}.${n}`;return[Z,`This server is publicly reachable at ${e}:<PORT>`,"(where <PORT> is whatever local port your dev server binds to \u2014 e.g.",`${e}:3000 for a server bound to :3000). When you stand something up`,"or want the user to test, hand them that URL \u2014 they can't reach","localhost inside this sandbox, but they can reach the public URL.","","After making changes, link the user to the dev server when applicable so they can test.",Q].join(`
6
- `)}function $t(t,n){let e=new RegExp(`${Z}[\\s\\S]*?${Q}`,"m");if(e.test(t))return t.replace(e,n);let r=t?t.endsWith(`
5
+ `;return i===r?!1:(_e(n,i,s)||E.writeFileSync(n,i,{mode:s}),!0)}import p from"node:fs";import z from"node:path";var Z="<!-- typhons-public-url -->",Q="<!-- /typhons-public-url -->",q="d.typhons.dev",Te=[{dir:".claude",file:"CLAUDE.md"},{dir:".codex",file:"AGENTS.md"}];function $e(e,n=q){let t=`https://${e}.${n}`;return[Z,`This server is publicly reachable at ${t}:<PORT>`,"(where <PORT> is whatever local port your dev server binds to \u2014 e.g.",`${t}:3000 for a server bound to :3000). When you stand something up`,"or want the user to test, hand them that URL \u2014 they can't reach","localhost inside this sandbox, but they can reach the public URL.","","After making changes, link the user to the dev server when applicable so they can test.",Q].join(`
6
+ `)}function Ae(e,n){let t=new RegExp(`${Z}[\\s\\S]*?${Q}`,"m");if(t.test(e))return e.replace(t,n);let r=e?e.endsWith(`
7
7
 
8
- `)?"":t.endsWith(`
8
+ `)?"":e.endsWith(`
9
9
  `)?`
10
10
  `:`
11
11
 
12
- `:"";return t+r+n+`
13
- `}function At(){let t=[];try{p.existsSync("/root")&&t.push("/root")}catch{}try{for(let n of p.readdirSync("/home")){let e=`/home/${n}`;try{p.statSync(e).isDirectory()&&t.push(e)}catch{}}}catch{}return t}function tt(t,n=q,e=r=>process.stderr.write(r+`
14
- `)){let r=Tt(t,n),s=typeof process.getuid=="function"?process.getuid():0;for(let o of At()){let l=0,i=0;try{let u=p.statSync(o);l=u.uid,i=u.gid}catch{continue}if(!(s!==0&&l!==s))for(let{dir:u,file:T}of _t){let d=z.join(o,u),h=z.join(d,T);try{let m=p.existsSync(d);m||p.mkdirSync(d,{recursive:!0});let F=p.existsSync(h)?p.readFileSync(h,"utf8"):"",M=$t(F,r);M!==F&&p.writeFileSync(h,M);try{m||p.chownSync(d,l,i)}catch{}try{p.chownSync(h,l,i)}catch{}}catch(m){try{e(`[agent-md] ${h}: ${m.message}`)}catch{}}}}}var pe=lt(ut(import.meta.url));process.on("uncaughtException",t=>{console.error(`[relay-client] UNCAUGHT: ${t.message}`),console.error(t.stack)});process.on("unhandledRejection",t=>{console.error(`[relay-client] UNHANDLED REJECTION: ${t.message||t}`)});var vt=process.env.RELAY_HOST||"ssh.typhons.dev",wt=process.env.RELAY_SCHEME||"wss",N=process.env.E2B_RUN_DIR||"/run/e2b",It=`${N}/.RELAY_TOKEN`,xt=`${N}/.E2B_SANDBOX_ID`,et=`${N}/.SAVED_SANDBOX_ID`,Ot=`${N}/.DEVBOX_NAME`,Dt=parseInt(process.env.RELAY_CLIENT_RECONNECT_DELAY_MS||"1500",10),Ht=3e3,nt=15e3,kt=6e4,rt=parseInt(process.env.RELAY_CLIENT_VERIFY_REJECT_RETRY_MS||"10000",10),ot="@typhons/sandbox-tools",Ct=process.env.RELAY_CLIENT_DISABLE_UPDATE==="1",w=parseInt(process.env.KEEPALIVE_PORT||"49999");function I(){try{return a.readFileSync(It,"utf8").trim()}catch{}return""}function Pt(t){return"tt1_"+Nt.createHmac("sha256",t).update("typhons:tunnel").digest("hex")}var D=!1,Bt=`${N}/.TYPHONS_SHARED_MOUNT`;function L(){let t={};try{for(let n of a.readFileSync(Bt,"utf8").split(`
15
- `)){let e=n.match(/^([A-Z_]+)=(.*)$/);e&&(t[e[1]]=e[2])}}catch{}return t}function R(){return(process.env.TYPHONS_SHARE_CREDENTIALS??L().TYPHONS_SHARE_CREDENTIALS)!=="0"}function Lt(){try{return a.readFileSync("/proc/mounts","utf8").includes(`/${ft} fuse.rclone`)}catch{return!1}}function x(){if(R())try{let t=b.join(lt(ut(import.meta.url)),"typhons-share-mount.js"),[n,e]=a.existsSync(t)?[process.execPath,[t]]:["/opt/sandbox/bin/typhons-share-mount",[]],r=at(n,e,{stdio:["ignore","inherit","inherit"],detached:!0,env:{...L(),...process.env}});r.on("error",s=>c(`typhons-share-mount spawn failed: ${s.message}`)),r.unref()}catch(t){c(`typhons-share-mount failed to start: ${t.message}`)}}var dt=[".claude/.credentials.json",".config/gh/hosts.yml",".codex/auth.json"],ft=".typhons-shared-mount";function P(t,n){try{let e=a.realpathSync(b.join(t,ft)),r=a.realpathSync(b.dirname(n));return r===e||r.startsWith(e+"/")}catch{return!1}}function ht(){let t=["/root"];try{t.push(...a.readdirSync("/home").map(n=>`/home/${n}`))}catch{}return t.filter(n=>{try{return a.existsSync(`${n}/.config/typhons-share/rclone.conf`)}catch{return!1}})}var H=null;function pt(t){H||(c(`${t} replaced by a regular file (login?) \u2014 folding into the share`),H=setTimeout(()=>{H=null,x()},300))}var k=new Set;function Ut(){if(R())for(let t of ht())for(let n of dt){let e=`${t}/${n}`,r=b.dirname(e),s=b.basename(e);if(!(k.has(r)||!a.existsSync(r))&&!P(t,e))try{let o=a.watch(r,(l,i)=>{if(i===s)try{a.lstatSync(e).isFile()&&!P(t,e)&&pt(e)}catch{}});o.on("error",()=>{k.delete(r);try{o.close()}catch{}}),k.add(r)}catch{}}}function Ft(){if(Ut(),!!R())try{for(let t of ht())for(let n of dt)try{if(a.lstatSync(`${t}/${n}`).isFile()&&!P(t,`${t}/${n}`)){pt(`${t}/${n}`);return}}catch{}}catch{}}setInterval(Ft,1e4);function Mt(){!R()||!I()||Lt()||x()}setInterval(Mt,18e4);function yt(t=0){if((process.env.TYPHONS_SHARE_CREDENTIALS??L().TYPHONS_SHARE_CREDENTIALS)!=="0"){if(!R()||!I()){t<120?setTimeout(()=>yt(t+1),500):c("share mount boot kick: gave up waiting for token/config");return}c("share mount boot kick: starting typhons-share-mount"),x()}}function U(){try{return a.readFileSync(xt,"utf8").trim()}catch{}return"unknown"}function c(t){console.log(`[relay-client ${new Date().toISOString().slice(11,19)}] ${t}`)}var ct=process.env.AGENT_MD_HTTP_DOMAIN||"d.typhons.dev",_="unknown";function Yt(){if(process.env.RELAY_URL)return process.env.RELAY_URL;_=U();try{let t=a.readFileSync(et,"utf8").trim();if(t&&t!==_){c(`Clone detected (${t} -> ${_}), interrupting agents...`);try{g("/opt/sandbox/bin/stop-agents",{timeout:1e4,stdio:"inherit"})}catch{}try{a.writeFileSync(et,_)}catch{}}}catch{}return`${wt}://${vt}/tunnel/${_}`}var S=null,C=null;function Jt(){return G(Y,{onConnect(n){c("Connected to session-proxy via IPC"),S=n,C=new v(e=>{f&&f.readyState===A.OPEN&&f.send(e)},e=>{f&&f.readyState===A.OPEN&&f.send(JSON.stringify(e))}),n.on("data",e=>C.feed(e))},onClose(){c("IPC connection to session-proxy lost, will retry..."),S=null,C=null},onError(n){n.code!=="ENOENT"&&n.code!=="ECONNREFUSED"&&c(`IPC error: ${n.message}`)}})}var f=null,B=null,y=0,$=null;function st(t=Dt){if(f){try{f.terminate()}catch{}f=null}$&&(clearTimeout($),$=null),$=setTimeout(()=>{$=null,mt()},t)}function mt(){let t=++y,n=Yt();c(`Connecting to ${n}... (gen=${t}, sandbox=${_})`);let e={},r=I();if(r){let d=D?r:Pt(r);e.Authorization=`Bearer ${d}`}if(f){try{f.terminate()}catch{}f=null}let s=new A(n,{headers:e});f=s;let o=null,l=!0,i=setTimeout(()=>{if(t===y){c(`Handshake timeout after ${nt}ms \u2014 aborting and reconnecting`);try{s.terminate()}catch{}u()}},nt);function u(){i&&(clearTimeout(i),i=null),o&&(clearInterval(o),o=null),t===y&&st()}function T(){l=!0,o=setInterval(()=>{if(t!==y){clearInterval(o);return}if(!l){c("No pong received \u2014 connection dead, reconnecting"),u();return}l=!1;try{s.ping()}catch{u()}},Ht)}s.on("open",()=>{if(t!==y){s.terminate();return}i&&(clearTimeout(i),i=null),c(`Connected${B?` (reconnect, port ${B})`:""} (gen=${t})`),T()}),s.on("pong",()=>{t===y&&(l=!0)}),s.on("message",(d,h)=>{if(t===y){if(!h){try{jt(JSON.parse(d.toString()))}catch(m){c(`Bad control msg: ${m.message}`)}return}S&&!S.destroyed&&K(S,Buffer.from(d))}}),s.on("close",(d,h)=>{if(t!==y)return;let m=h?h.toString():"";if(d===4100&&(c(`Replaced by newer connection (code 4100, reason=${m}) \u2014 exiting`),process.exit(42)),d===4101){c(`Identity verification rejected (code 4101, reason=${m}) \u2014 retrying in ${rt}ms (likely waiting for envd to update /run/e2b/.E2B_SANDBOX_ID)`),i&&(clearTimeout(i),i=null),o&&(clearInterval(o),o=null),t===y&&st(rt);return}if(d===4001&&!D&&I()){c("Derived tunnel token rejected (4001) \u2014 falling back to raw relay token"),D=!0,u();return}c(`Data WS closed: code=${d}, reason=${m}`),u()}),s.on("error",d=>{t===y&&(c(`Data WS error: ${d.message} (code=${d.code||"none"})`),u())}),s.on("unexpected-response",(d,h)=>{t===y&&(c(`Data WS unexpected response: status=${h.statusCode}`),u())})}function jt(t){if(t.type==="port"){B=t.port,c(`Assigned port ${t.port}`);return}if(t.type==="name"){if(c(`Name assigned: ${t.name}`),t.name&&/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(t.name)){let n=`${t.name}.local`;try{Rt("hostname",[n],{timeout:5e3,stdio:"ignore"}),a.writeFileSync("/etc/hostname",n+`
16
- `)}catch{}try{X(n)&&c(`Updated /etc/hosts entry for ${n}`)}catch(e){c(`Failed to update /etc/hosts: ${e.message}`)}try{a.writeFileSync(Ot,t.name)}catch{}try{tt(t.name,ct,c)}catch(e){c(`updateAgentMd failed: ${e.message}`)}try{let e=`${t.name}.${ct}`,r=`https://${e}`;a.writeFileSync("/etc/profile.d/typhons.sh",`export TYPHONS_HOST=${JSON.stringify(e)}
12
+ `:"";return e+r+n+`
13
+ `}function be(){let e=[];try{p.existsSync("/root")&&e.push("/root")}catch{}try{for(let n of p.readdirSync("/home")){let t=`/home/${n}`;try{p.statSync(t).isDirectory()&&e.push(t)}catch{}}}catch{}return e}function ee(e,n=q,t=r=>process.stderr.write(r+`
14
+ `)){let r=$e(e,n),s=typeof process.getuid=="function"?process.getuid():0;for(let o of be()){let l=0,i=0;try{let u=p.statSync(o);l=u.uid,i=u.gid}catch{continue}if(!(s!==0&&l!==s))for(let{dir:u,file:T}of Te){let d=z.join(o,u),h=z.join(d,T);try{let m=p.existsSync(d);m||p.mkdirSync(d,{recursive:!0});let F=p.existsSync(h)?p.readFileSync(h,"utf8"):"",M=Ae(F,r);M!==F&&p.writeFileSync(h,M);try{m||p.chownSync(d,l,i)}catch{}try{p.chownSync(h,l,i)}catch{}}catch(m){try{t(`[agent-md] ${h}: ${m.message}`)}catch{}}}}}var yt=le(ue(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 we=process.env.RELAY_HOST||"ssh.typhons.dev",Ie=process.env.RELAY_SCHEME||"wss",N=process.env.E2B_RUN_DIR||"/run/e2b",xe=`${N}/.RELAY_TOKEN`,Oe=`${N}/.E2B_SANDBOX_ID`,te=`${N}/.SAVED_SANDBOX_ID`,De=`${N}/.DEVBOX_NAME`,He=parseInt(process.env.RELAY_CLIENT_RECONNECT_DELAY_MS||"1500",10),ke=3e3,ne=15e3,Ce=6e4,re=parseInt(process.env.RELAY_CLIENT_VERIFY_REJECT_RETRY_MS||"10000",10),oe="@typhons/sandbox-tools",Pe=process.env.RELAY_CLIENT_DISABLE_UPDATE==="1",w=parseInt(process.env.KEEPALIVE_PORT||"49999");function I(){try{return a.readFileSync(xe,"utf8").trim()}catch{}return""}function Be(e){return"tt1_"+Re.createHmac("sha256",e).update("typhons:tunnel").digest("hex")}var D=!1,Le=`${N}/.TYPHONS_SHARED_MOUNT`;function L(){let e={};try{for(let n of a.readFileSync(Le,"utf8").split(`
15
+ `)){let t=n.match(/^([A-Z_]+)=(.*)$/);t&&(e[t[1]]=t[2])}}catch{}return e}function R(){return(process.env.TYPHONS_SHARE_CREDENTIALS??L().TYPHONS_SHARE_CREDENTIALS)!=="0"}function Ue(){try{return a.readFileSync("/proc/mounts","utf8").includes(`/${fe} fuse.rclone`)}catch{return!1}}function x(){if(R())try{let e=b.join(le(ue(import.meta.url)),"typhons-share-mount.js"),[n,t]=a.existsSync(e)?[process.execPath,[e]]:["/opt/sandbox/bin/typhons-share-mount",[]],r=ae(n,t,{stdio:["ignore","inherit","inherit"],detached:!0,env:{...L(),...process.env}});r.on("error",s=>c(`typhons-share-mount spawn failed: ${s.message}`)),r.unref()}catch(e){c(`typhons-share-mount failed to start: ${e.message}`)}}var de=[".claude/.credentials.json",".config/gh/hosts.yml",".codex/auth.json"],fe=".typhons-shared-mount";function P(e,n){try{let t=a.realpathSync(b.join(e,fe)),r=a.realpathSync(b.dirname(n));return r===t||r.startsWith(t+"/")}catch{return!1}}function he(){let e=["/root"];try{e.push(...a.readdirSync("/home").map(n=>`/home/${n}`))}catch{}return e.filter(n=>{try{return a.existsSync(`${n}/.config/typhons-share/rclone.conf`)}catch{return!1}})}var H=null;function pe(e){H||(c(`${e} replaced by a regular file (login?) \u2014 folding into the share`),H=setTimeout(()=>{H=null,x()},300))}var k=new Set;function Fe(){if(R())for(let e of he())for(let n of de){let t=`${e}/${n}`,r=b.dirname(t),s=b.basename(t);if(!(k.has(r)||!a.existsSync(r))&&!P(e,t))try{let o=a.watch(r,(l,i)=>{if(i===s)try{a.lstatSync(t).isFile()&&!P(e,t)&&pe(t)}catch{}});o.on("error",()=>{k.delete(r);try{o.close()}catch{}}),k.add(r)}catch{}}}function Me(){if(Fe(),!!R())try{for(let e of he())for(let n of de)try{if(a.lstatSync(`${e}/${n}`).isFile()&&!P(e,`${e}/${n}`)){pe(`${e}/${n}`);return}}catch{}}catch{}}setInterval(Me,1e4);function Ye(){!R()||!I()||Ue()||x()}setInterval(Ye,18e4);function ye(e=0){if((process.env.TYPHONS_SHARE_CREDENTIALS??L().TYPHONS_SHARE_CREDENTIALS)!=="0"){if(!R()||!I()){e<120?setTimeout(()=>ye(e+1),500):c("share mount boot kick: gave up waiting for token/config");return}c("share mount boot kick: starting typhons-share-mount"),x()}}function U(){try{return a.readFileSync(Oe,"utf8").trim()}catch{}return"unknown"}function c(e){console.log(`[relay-client ${new Date().toISOString().slice(11,19)}] ${e}`)}var ce=process.env.AGENT_MD_HTTP_DOMAIN||"d.typhons.dev",_="unknown";function Je(){if(process.env.RELAY_URL)return process.env.RELAY_URL;_=U();try{let e=a.readFileSync(te,"utf8").trim();if(e&&e!==_){c(`Clone detected (${e} -> ${_}), interrupting agents...`);try{g("/opt/sandbox/bin/stop-agents",{timeout:1e4,stdio:"inherit"})}catch{}try{a.writeFileSync(te,_)}catch{}}}catch{}return`${Ie}://${we}/tunnel/${_}`}var S=null,C=null;function je(){return G(Y,{onConnect(n){c("Connected to session-proxy via IPC"),S=n,C=new v(t=>{f&&f.readyState===A.OPEN&&f.send(t)},t=>{f&&f.readyState===A.OPEN&&f.send(JSON.stringify(t))}),n.on("data",t=>C.feed(t))},onClose(){c("IPC connection to session-proxy lost, will retry..."),S=null,C=null},onError(n){n.code!=="ENOENT"&&n.code!=="ECONNREFUSED"&&c(`IPC error: ${n.message}`)}})}var f=null,B=null,y=0,$=null;function se(e=He){if(f){try{f.terminate()}catch{}f=null}$&&(clearTimeout($),$=null),$=setTimeout(()=>{$=null,me()},e)}function me(){let e=++y,n=Je();c(`Connecting to ${n}... (gen=${e}, sandbox=${_})`);let t={},r=I();if(r){let d=D?r:Be(r);t.Authorization=`Bearer ${d}`}if(f){try{f.terminate()}catch{}f=null}let s=new A(n,{headers:t});f=s;let o=null,l=!0,i=setTimeout(()=>{if(e===y){c(`Handshake timeout after ${ne}ms \u2014 aborting and reconnecting`);try{s.terminate()}catch{}u()}},ne);function u(){i&&(clearTimeout(i),i=null),o&&(clearInterval(o),o=null),e===y&&se()}function T(){l=!0,o=setInterval(()=>{if(e!==y){clearInterval(o);return}if(!l){c("No pong received \u2014 connection dead, reconnecting"),u();return}l=!1;try{s.ping()}catch{u()}},ke)}s.on("open",()=>{if(e!==y){s.terminate();return}i&&(clearTimeout(i),i=null),c(`Connected${B?` (reconnect, port ${B})`:""} (gen=${e})`),T()}),s.on("pong",()=>{e===y&&(l=!0)}),s.on("message",(d,h)=>{if(e===y){if(!h){try{Ke(JSON.parse(d.toString()))}catch(m){c(`Bad control msg: ${m.message}`)}return}S&&!S.destroyed&&K(S,Buffer.from(d))}}),s.on("close",(d,h)=>{if(e!==y)return;let m=h?h.toString():"";if(d===4100&&(c(`Replaced by newer connection (code 4100, reason=${m}) \u2014 exiting`),process.exit(42)),d===4101){c(`Identity verification rejected (code 4101, reason=${m}) \u2014 retrying in ${re}ms (likely waiting for envd to update /run/e2b/.E2B_SANDBOX_ID)`),i&&(clearTimeout(i),i=null),o&&(clearInterval(o),o=null),e===y&&se(re);return}if(d===4001&&!D&&I()){c("Derived tunnel token rejected (4001) \u2014 falling back to raw relay token"),D=!0,u();return}c(`Data WS closed: code=${d}, reason=${m}`),u()}),s.on("error",d=>{e===y&&(c(`Data WS error: ${d.message} (code=${d.code||"none"})`),u())}),s.on("unexpected-response",(d,h)=>{e===y&&(c(`Data WS unexpected response: status=${h.statusCode}`),u())})}function Ke(e){if(e.type==="port"){B=e.port,c(`Assigned port ${e.port}`);return}if(e.type==="name"){if(c(`Name assigned: ${e.name}`),e.name&&/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(e.name)){let n=`${e.name}.local`;try{ve("hostname",[n],{timeout:5e3,stdio:"ignore"}),a.writeFileSync("/etc/hostname",n+`
16
+ `)}catch{}try{X(n)&&c(`Updated /etc/hosts entry for ${n}`)}catch(t){c(`Failed to update /etc/hosts: ${t.message}`)}try{a.writeFileSync(De,e.name)}catch{}try{ee(e.name,ce,c)}catch(t){c(`updateAgentMd failed: ${t.message}`)}try{let t=`${e.name}.${ce}`,r=`https://${t}`;a.writeFileSync("/etc/profile.d/typhons.sh",`export TYPHONS_HOST=${JSON.stringify(t)}
17
17
  export TYPHONS_URL=${JSON.stringify(r)}
18
- `,{mode:420})}catch(e){c(`Failed to write /etc/profile.d/typhons.sh: ${e.message}`)}x()}return}S&&!S.destroyed&&W(S,t)}function it(){try{let t=g(`npm view ${ot} version 2>/dev/null`,{timeout:15e3}).toString().trim();if(!t)return;let n="";try{n=JSON.parse(a.readFileSync("/opt/sandbox/lib/node_modules/@typhons/sandbox-tools/package.json","utf8")).version||""}catch{return}if(t!==n){c(`Update available: ${n} \u2192 ${t}, installing...`),g(`npm install -g --prefix /opt/sandbox ${ot}@latest`,{timeout:12e4,stdio:"inherit"});try{g('pkill -f "check-claude-active.*--daemon" 2>/dev/null',{shell:!0,timeout:5e3})}catch{}try{let e=a.openSync("/tmp/claude-keepalive.log","a");at("/opt/sandbox/bin/check-claude-active",["--daemon"],{detached:!0,stdio:["ignore",e,e]}).unref(),a.closeSync(e)}catch{}try{g("/opt/sandbox/bin/start-webchat --restart",{timeout:2e4})}catch{}c("Update installed, exiting for restart..."),process.exit(0)}}catch{}}function Kt(){let t=bt.createServer((e,r)=>{if(r.setHeader("Content-Type","application/json"),e.method==="GET"&&e.url==="/health"){r.end(JSON.stringify({ok:!0,sandboxId:U(),wsConnected:!!(f&&f.readyState===A.OPEN)}));return}if(e.method==="POST"&&e.url==="/_relay/verify"){let s="";e.on("data",o=>{s+=o,s.length>4096&&e.destroy()}),e.on("end",()=>{let o;try{o=JSON.parse(s).nonce}catch{r.writeHead(400),r.end('{"error":"bad json"}');return}if(typeof o!="string"||!o.length||o.length>256){r.writeHead(400),r.end('{"error":"missing or invalid nonce"}');return}if(!f||f.readyState!==A.OPEN){r.writeHead(503),r.end('{"error":"WS not connected"}');return}try{f.send(JSON.stringify({type:"verify-ack",nonce:o}))}catch(l){r.writeHead(500),r.end(`{"error":"${l.message}"}`);return}r.writeHead(200),r.end('{"ok":true}')}),e.on("error",()=>{try{r.writeHead(400),r.end('{"error":"req error"}')}catch{}});return}r.writeHead(404),r.end('{"error":"not found"}')}),n=!1;return t.on("error",e=>{if(e.code==="EADDRINUSE"&&!n){n=!0,c(`Port ${w} 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{g("pkill -f /opt/sandbox/lib/node_modules/@typhons/sandbox-tools/session-proxy.js",{timeout:3e3})}catch{}setTimeout(()=>{try{t.listen(w,"0.0.0.0")}catch{}},1500);return}c(`Keepalive HTTP server error: ${e.message}`)}),t.listen(w,"0.0.0.0",()=>{c(`Keepalive HTTP server on :${w}`)}),t}var ye=Jt();Kt();mt();try{g("/opt/sandbox/bin/start-webchat",{timeout:2e4})}catch{}Ct||(setTimeout(it,1e4),setInterval(it,kt));c(`Relay client started (sandbox=${U()})`);yt();
18
+ `,{mode:420})}catch(t){c(`Failed to write /etc/profile.d/typhons.sh: ${t.message}`)}x()}return}S&&!S.destroyed&&W(S,e)}function ie(){try{let e=g(`npm view ${oe} version 2>/dev/null`,{timeout:15e3}).toString().trim();if(!e)return;let n="";try{n=JSON.parse(a.readFileSync("/opt/sandbox/lib/node_modules/@typhons/sandbox-tools/package.json","utf8")).version||""}catch{return}if(e!==n){c(`Update available: ${n} \u2192 ${e}, installing...`),g(`npm install -g --prefix /opt/sandbox ${oe}@latest`,{timeout:12e4,stdio:"inherit"});try{g('pkill -f "check-claude-active.*--daemon" 2>/dev/null',{shell:!0,timeout:5e3})}catch{}try{let t=a.openSync("/tmp/claude-keepalive.log","a");ae("/opt/sandbox/bin/check-claude-active",["--daemon"],{detached:!0,stdio:["ignore",t,t]}).unref(),a.closeSync(t)}catch{}try{g("/opt/sandbox/bin/start-webchat --restart",{timeout:2e4})}catch{}c("Update installed, exiting for restart..."),process.exit(0)}}catch{}}function We(){let e=Ne.createServer((t,r)=>{if(r.setHeader("Content-Type","application/json"),t.method==="GET"&&t.url==="/health"){r.end(JSON.stringify({ok:!0,sandboxId:U(),wsConnected:!!(f&&f.readyState===A.OPEN)}));return}if(t.method==="POST"&&t.url==="/_relay/verify"){let s="";t.on("data",o=>{s+=o,s.length>4096&&t.destroy()}),t.on("end",()=>{let o;try{o=JSON.parse(s).nonce}catch{r.writeHead(400),r.end('{"error":"bad json"}');return}if(typeof o!="string"||!o.length||o.length>256){r.writeHead(400),r.end('{"error":"missing or invalid nonce"}');return}if(!f||f.readyState!==A.OPEN){r.writeHead(503),r.end('{"error":"WS not connected"}');return}try{f.send(JSON.stringify({type:"verify-ack",nonce:o}))}catch(l){r.writeHead(500),r.end(`{"error":"${l.message}"}`);return}r.writeHead(200),r.end('{"ok":true}')}),t.on("error",()=>{try{r.writeHead(400),r.end('{"error":"req error"}')}catch{}});return}r.writeHead(404),r.end('{"error":"not found"}')}),n=!1;return e.on("error",t=>{if(t.code==="EADDRINUSE"&&!n){n=!0,c(`Port ${w} 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{g("pkill -f /opt/sandbox/lib/node_modules/@typhons/sandbox-tools/session-proxy.js",{timeout:3e3})}catch{}setTimeout(()=>{try{e.listen(w,"0.0.0.0")}catch{}},1500);return}c(`Keepalive HTTP server error: ${t.message}`)}),e.listen(w,"0.0.0.0",()=>{c(`Keepalive HTTP server on :${w}`)}),e}var mt=je();We();me();function Se(){try{g("/opt/sandbox/bin/start-webchat",{timeout:2e4})}catch{}}Se();setInterval(Se,3e4);Pe||(setTimeout(ie,1e4),setInterval(ie,Ce));c(`Relay client started (sandbox=${U()})`);ye();
package/start-webchat.sh CHANGED
@@ -37,12 +37,20 @@ for f in /opt/sandbox/lib/node_modules/@typhons/sandbox-tools/webchat-serve[r].c
37
37
  done
38
38
  [ -z "$BIN" ] && exit 0
39
39
 
40
- # Run as the box user (first /home/*/.claude owner), else root fallback.
41
- WEBCHAT_USER=""
42
- for d in /home/*/.claude; do
43
- [ -d "$d" ] && WEBCHAT_USER=$(basename "$(dirname "$d")") && break
44
- done
45
- [ -z "$WEBCHAT_USER" ] && WEBCHAT_USER=$(ls /home 2>/dev/null | head -1)
40
+ # Run as the box user: whoever is actually running claude/tmux their per-uid
41
+ # tmux sockets (/tmp/tmux-<uid>) and ~/.claude transcripts are exactly what
42
+ # webchat reads. Detect THAT user, not the first /home/*/.claude: a system
43
+ # 'node' home sorts ahead of the real user and would land webchat on the wrong
44
+ # uid, blind to the session ("rc-chat not found / no sessions").
45
+ WEBCHAT_USER=$(ps -eo user:32=,comm= 2>/dev/null | awk '$2=="claude"{print $1; exit}')
46
+ [ -z "$WEBCHAT_USER" ] && WEBCHAT_USER=$(ps -eo user:32=,comm= 2>/dev/null | awk '$2=="tmux"{print $1; exit}')
47
+ if [ -z "$WEBCHAT_USER" ]; then
48
+ for d in /home/*/.claude; do
49
+ u=$(basename "$(dirname "$d")")
50
+ [ -d "$d" ] && [ "$u" != node ] && { WEBCHAT_USER="$u"; break; }
51
+ done
52
+ fi
53
+ [ -z "$WEBCHAT_USER" ] && WEBCHAT_USER=$(ls /home 2>/dev/null | grep -vx node | head -1)
46
54
 
47
55
  if [ -n "$WEBCHAT_USER" ] && id "$WEBCHAT_USER" > /dev/null 2>&1; then
48
56
  su -s /bin/bash "$WEBCHAT_USER" -c "WEBCHAT_PORT=$PORT nohup node '$BIN' >> /tmp/webchat.log 2>&1 &"