@typhons/sandbox-tools 0.6.3 → 0.6.5

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,16 +1,18 @@
1
1
  {
2
2
  "name": "@typhons/sandbox-tools",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sandbox-relay-client": "./relay-client.js",
7
7
  "sandbox-session-proxy": "./session-proxy.js",
8
8
  "sandbox-watchdog": "./sandbox-watchdog.sh",
9
9
  "sandbox-start-all": "./sandbox-start-all.sh",
10
+ "start-webchat": "./start-webchat.sh",
10
11
  "check-claude-active": "./check-claude-active.js",
11
12
  "stop-agents": "./stop-agents.js",
12
13
  "sandbox-clone": "./clone.js",
13
- "typhons-share-mount": "./typhons-share-mount.js"
14
+ "typhons-share-mount": "./typhons-share-mount.js",
15
+ "webchat-server": "./webchat-server.cjs"
14
16
  },
15
17
  "dependencies": {
16
18
  "ws": "^8.17.0",
package/relay-client.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import A from"ws";import Ne from"http";import a from"fs";import N from"path";import be from"crypto";import{spawn as ae,execSync as $,execFileSync as Re}from"child_process";import{dirname as le}from"path";import{fileURLToPath as ue}from"url";import Se from"net";var Y=process.env.SESSION_PROXY_SOCK||"/tmp/session-proxy.sock",Ge=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=Se.createConnection(e),o.on("connect",()=>{n&&n(o)}),o.on("close",()=>{o=null,t&&t(),l||(i=setTimeout(u,s))}),o.on("error",_=>{r&&r(_)}))}return u(),{getSocket(){return o},close(){l=!0,i&&clearTimeout(i),o&&o.destroy()}}}import E from"fs";import x from"path";var V="# typhons-managed-hostname";function Ee(e){let n=e.replace(/\.local$/,"");return`127.0.1.1 ${n===e?e:`${e} ${n}`} ${V}`}function ge(e,n,t){let r=x.dirname(e),s=x.basename(e),o=x.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=Ee(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(`
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(`
4
4
  `)+`
5
- `;return i===r?!1:(ge(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",_e=[{dir:".claude",file:"CLAUDE.md"},{dir:".codex",file:"AGENTS.md"}];function Te(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 $e(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(`
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(`
7
7
 
8
- `)?"":e.endsWith(`
8
+ `)?"":t.endsWith(`
9
9
  `)?`
10
10
  `:`
11
11
 
12
- `:"";return e+r+n+`
13
- `}function Ae(){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=Te(e,n),s=typeof process.getuid=="function"?process.getuid():0;for(let o of Ae()){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:_}of _e){let d=z.join(o,u),h=z.join(d,_);try{let m=p.existsSync(d);m||p.mkdirSync(d,{recursive:!0});let F=p.existsSync(h)?p.readFileSync(h,"utf8"):"",M=$e(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 pt=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 ve=process.env.RELAY_HOST||"ssh.typhons.dev",we=process.env.RELAY_SCHEME||"wss",b=process.env.E2B_RUN_DIR||"/run/e2b",Ie=`${b}/.RELAY_TOKEN`,Oe=`${b}/.E2B_SANDBOX_ID`,te=`${b}/.SAVED_SANDBOX_ID`,xe=`${b}/.DEVBOX_NAME`,De=parseInt(process.env.RELAY_CLIENT_RECONNECT_DELAY_MS||"1500",10),He=3e3,ne=15e3,ke=6e4,re=parseInt(process.env.RELAY_CLIENT_VERIFY_REJECT_RETRY_MS||"10000",10),oe="@typhons/sandbox-tools",Ce=process.env.RELAY_CLIENT_DISABLE_UPDATE==="1",w=parseInt(process.env.KEEPALIVE_PORT||"49999");function I(){try{return a.readFileSync(Ie,"utf8").trim()}catch{}return""}function Pe(e){return"tt1_"+be.createHmac("sha256",e).update("typhons:tunnel").digest("hex")}var D=!1,Be=`${b}/.TYPHONS_SHARED_MOUNT`;function L(){let e={};try{for(let n of a.readFileSync(Be,"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 Le(){try{return a.readFileSync("/proc/mounts","utf8").includes(`/${fe} fuse.rclone`)}catch{return!1}}function O(){if(R())try{let e=N.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(N.join(e,fe)),r=a.realpathSync(N.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,O()},300))}var k=new Set;function Ue(){if(R())for(let e of he())for(let n of de){let t=`${e}/${n}`,r=N.dirname(t),s=N.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 Fe(){if(Ue(),!!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(Fe,1e4);function Me(){!R()||!I()||Le()||O()}setInterval(Me,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"),O()}}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",g="unknown";function Ye(){if(process.env.RELAY_URL)return process.env.RELAY_URL;g=U();try{let e=a.readFileSync(te,"utf8").trim();if(e&&e!==g){c(`Clone detected (${e} -> ${g}), interrupting agents...`);try{$("/opt/sandbox/bin/stop-agents",{timeout:1e4,stdio:"inherit"})}catch{}try{a.writeFileSync(te,g)}catch{}}}catch{}return`${we}://${ve}/tunnel/${g}`}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,T=null;function se(e=De){if(f){try{f.terminate()}catch{}f=null}T&&(clearTimeout(T),T=null),T=setTimeout(()=>{T=null,me()},e)}function me(){let e=++y,n=Ye();c(`Connecting to ${n}... (gen=${e}, sandbox=${g})`);let t={},r=I();if(r){let d=D?r:Pe(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 _(){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()}},He)}s.on("open",()=>{if(e!==y){s.terminate();return}i&&(clearTimeout(i),i=null),c(`Connected${B?` (reconnect, port ${B})`:""} (gen=${e})`),_()}),s.on("pong",()=>{e===y&&(l=!0)}),s.on("message",(d,h)=>{if(e===y){if(!h){try{je(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 je(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{Re("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(xe,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)}
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)}
17
17
  export TYPHONS_URL=${JSON.stringify(r)}
18
- `,{mode:420})}catch(t){c(`Failed to write /etc/profile.d/typhons.sh: ${t.message}`)}O()}return}S&&!S.destroyed&&W(S,e)}function ie(){try{let e=$(`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...`),$(`npm install -g --prefix /opt/sandbox ${oe}@latest`,{timeout:12e4,stdio:"inherit"});try{$('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{}c("Update installed, exiting for restart..."),process.exit(0)}}catch{}}function Ke(){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{$("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 yt=Je();Ke();me();Ce||(setTimeout(ie,1e4),setInterval(ie,ke));c(`Relay client started (sandbox=${U()})`);ye();
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();
@@ -16,6 +16,7 @@ pkill -9 -f session-proxy.js 2>/dev/null || true
16
16
  pkill -9 -f relay-client.js 2>/dev/null || true
17
17
  pkill -9 -f sandbox-watchdog 2>/dev/null || true
18
18
  pkill -9 -f check-claude-active 2>/dev/null || true
19
+ pkill -9 -f webchat-server 2>/dev/null || true
19
20
  sleep 1
20
21
 
21
22
  # Start watchdog (manages both session-proxy and relay-client)
@@ -25,3 +26,11 @@ nohup bash "$BIN/sandbox-watchdog.sh" >> /tmp/ssh-relay.log 2>&1 &
25
26
  if ! pgrep -f "check-claude-active.*--daemon" > /dev/null 2>&1; then
26
27
  nohup node "$BIN/check-claude-active.js" --daemon >> /tmp/claude-keepalive.log 2>&1 &
27
28
  fi
29
+
30
+ # Start webchat sidecar (native web chat UI over claude tmux sessions).
31
+ # Delegated to start-webchat.sh so relay-client can (re)start it identically
32
+ # after an auto-update. Token auto-persists to ~/.webchat/token (0600); the app
33
+ # reads it via runCommand when proxying.
34
+ if [ -x "$BIN/start-webchat.sh" ]; then
35
+ "$BIN/start-webchat.sh"
36
+ fi
@@ -0,0 +1,53 @@
1
+ #!/bin/bash
2
+ # Start (or restart) the webchat sidecar as the box user.
3
+ #
4
+ # Shared by sandbox-start-all.sh (at box creation) and relay-client.js (on
5
+ # startup + after an auto-update), so an EXISTING box brings webchat up on its
6
+ # own — sandbox-start-all only runs once at creation and E2B resume is a memory
7
+ # restore, so the box would otherwise never (re)start the sidecar after the
8
+ # package is updated. Must run as the box user: tmux sockets are per-uid and the
9
+ # transcripts/registry live under the user's ~/.claude; this script runs as root
10
+ # and su's into the user.
11
+ #
12
+ # start-webchat.sh start only if not already running (idempotent)
13
+ # start-webchat.sh --restart kill any running instance first (pick up new code)
14
+
15
+ PORT=47291
16
+
17
+ # [r] bracket trick: keeps pgrep/pkill from matching this script's own command
18
+ # line ("webchat-serve[r]" resolves to "webchat-server" without appearing literally).
19
+ if pgrep -f "webchat-serve[r]" > /dev/null 2>&1; then
20
+ if [ "$1" = "--restart" ]; then
21
+ pkill -9 -f "webchat-serve[r]" 2>/dev/null
22
+ sleep 0.3
23
+ else
24
+ exit 0 # already running, nothing to do
25
+ fi
26
+ fi
27
+
28
+ # Locate the sidecar binary, npm-installed package file first, then the bin
29
+ # symlink (created without the .cjs extension), then a /tmp bootstrap the app
30
+ # may have pushed. Same priority order as the app's bootstrap probe.
31
+ BIN=""
32
+ for f in /opt/sandbox/lib/node_modules/@typhons/sandbox-tools/webchat-serve[r].cjs \
33
+ /opt/sandbox/bin/webchat-serve[r].cjs \
34
+ /opt/sandbox/bin/webchat-serve[r] \
35
+ /tmp/webchat/webchat-serve[r].cjs; do
36
+ [ -e "$f" ] && { BIN="$f"; break; }
37
+ done
38
+ [ -z "$BIN" ] && exit 0
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)
46
+
47
+ if [ -n "$WEBCHAT_USER" ] && id "$WEBCHAT_USER" > /dev/null 2>&1; then
48
+ su -s /bin/bash "$WEBCHAT_USER" -c "WEBCHAT_PORT=$PORT nohup node '$BIN' >> /tmp/webchat.log 2>&1 &"
49
+ echo "[start-webchat] started on :$PORT as $WEBCHAT_USER ($BIN)"
50
+ else
51
+ WEBCHAT_PORT=$PORT nohup node "$BIN" >> /tmp/webchat.log 2>&1 &
52
+ echo "[start-webchat] started on :$PORT (root fallback) ($BIN)"
53
+ fi