@typhons/sandbox-tools 0.5.10 → 0.5.11
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 +1 -1
- package/relay-client.js +13 -4
package/package.json
CHANGED
package/relay-client.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
`);
|
|
2
|
+
import $ from"ws";import ie from"http";import m from"fs";import{spawn as se,execSync as A,execFileSync as ae}from"child_process";import{dirname as le}from"path";import{fileURLToPath as ue}from"url";import q from"net";var B=process.env.SESSION_PROXY_SOCK||"/tmp/session-proxy.sock",be=2*1024*1024;var k=1,C=2;function L(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 P(e,n){if(!e||e.destroyed)return!1;let t=Buffer.from(JSON.stringify(n)),r=Buffer.alloc(5);return r[0]=C,r.writeUInt32LE(t.length,1),e.write(r),e.write(t),!0}var N=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 c=this.buf.slice(5,5+r);if(this.buf=this.buf.slice(5+r),t===k)this.onBinary(c);else if(t===C)try{this.onJSON(JSON.parse(c.toString()))}catch(o){console.error(`[ipc] bad JSON: ${o.message}`)}}}};function H(e,{onConnect:n,onClose:t,onError:r,retryMs:c=500}={}){let o=null,a=!1,s=null;function u(){a||(o=q.createConnection(e),o.on("connect",()=>{n&&n(o)}),o.on("close",()=>{o=null,t&&t(),a||(s=setTimeout(u,c))}),o.on("error",_=>{r&&r(_)}))}return u(),{getSocket(){return o},close(){a=!0,s&&clearTimeout(s),o&&o.destroy()}}}import S from"fs";import b from"path";var U="# typhons-managed-hostname";function ee(e){let n=e.replace(/\.local$/,"");return`127.0.1.1 ${n===e?e:`${e} ${n}`} ${U}`}function te(e,n,t){let r=b.dirname(e),c=b.basename(e),o=b.join(r,`.${c}.tmp.${process.pid}.${Date.now()}`);try{S.writeFileSync(o,n,{mode:t})}catch{try{S.unlinkSync(o)}catch{}return!1}try{return S.renameSync(o,e),!0}catch{try{S.unlinkSync(o)}catch{}return!1}}function F(e,n="/etc/hosts"){let t=ee(e),r="",c=420;try{r=S.readFileSync(n,"utf8"),c=S.statSync(n).mode&511}catch{}let o=r.split(`
|
|
3
|
+
`);o.length>0&&o[o.length-1]===""&&o.pop();let a=o.filter(u=>!u.includes(U));a.push(t);let s=a.join(`
|
|
4
4
|
`)+`
|
|
5
|
-
`;return
|
|
6
|
-
`)}
|
|
5
|
+
`;return s===r?!1:(te(n,s,c)||S.writeFileSync(n,s,{mode:c}),!0)}import p from"node:fs";import M from"node:path";var Y="<!-- typhons-public-url -->",J="<!-- /typhons-public-url -->",K="d.typhons.dev",ne=[{dir:".claude",file:"CLAUDE.md"},{dir:".codex",file:"AGENTS.md"}];function re(e,n=K){let t=`https://${e}.${n}`;return[Y,`This sandbox 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.",J].join(`
|
|
6
|
+
`)}function oe(e,n){let t=new RegExp(`${Y}[\\s\\S]*?${J}`,"m");if(t.test(e))return e.replace(t,n);let r=e?e.endsWith(`
|
|
7
|
+
|
|
8
|
+
`)?"":e.endsWith(`
|
|
9
|
+
`)?`
|
|
10
|
+
`:`
|
|
11
|
+
|
|
12
|
+
`:"";return e+r+n+`
|
|
13
|
+
`}function ce(){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 G(e,n=K,t=r=>process.stderr.write(r+`
|
|
14
|
+
`)){let r=re(e,n),c=typeof process.getuid=="function"?process.getuid():0;for(let o of ce()){let a=0,s=0;try{let u=p.statSync(o);a=u.uid,s=u.gid}catch{continue}if(!(c!==0&&a!==c))for(let{dir:u,file:_}of ne){let d=M.join(o,u),f=M.join(d,_);try{let y=p.existsSync(d);y||p.mkdirSync(d,{recursive:!0});let O=p.existsSync(f)?p.readFileSync(f,"utf8"):"",D=oe(O,r);D!==O&&p.writeFileSync(f,D);try{y||p.chownSync(d,a,s)}catch{}try{p.chownSync(f,a,s)}catch{}}catch(y){try{t(`[agent-md] ${f}: ${y.message}`)}catch{}}}}}var je=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 de=process.env.RELAY_HOST||"ssh.typhons.dev",fe=process.env.RELAY_SCHEME||"wss",I=process.env.E2B_RUN_DIR||"/run/e2b",pe=`${I}/.RELAY_TOKEN`,he=`${I}/.E2B_SANDBOX_ID`,j=`${I}/.SAVED_SANDBOX_ID`,ye=`${I}/.DEVBOX_NAME`,me=parseInt(process.env.RELAY_CLIENT_RECONNECT_DELAY_MS||"1500",10),Ee=3e3,V=15e3,Se=6e4,W=parseInt(process.env.RELAY_CLIENT_VERIFY_REJECT_RETRY_MS||"10000",10),X="@typhons/sandbox-tools",ge=process.env.RELAY_CLIENT_DISABLE_UPDATE==="1",R=parseInt(process.env.KEEPALIVE_PORT||"49999");function _e(){try{return m.readFileSync(pe,"utf8").trim()}catch{}return""}function x(){try{return m.readFileSync(he,"utf8").trim()}catch{}return"unknown"}function i(e){console.log(`[relay-client ${new Date().toISOString().slice(11,19)}] ${e}`)}var Te=process.env.AGENT_MD_HTTP_DOMAIN||"d.typhons.dev",g="unknown";function Ae(){if(process.env.RELAY_URL)return process.env.RELAY_URL;g=x();try{let e=m.readFileSync(j,"utf8").trim();if(e&&e!==g){i(`Clone detected (${e} -> ${g}), interrupting agents...`);try{A("/opt/sandbox/bin/stop-agents",{timeout:1e4,stdio:"inherit"})}catch{}try{m.writeFileSync(j,g)}catch{}}}catch{}return`${fe}://${de}/tunnel/${g}`}var E=null,v=null;function $e(){return H(B,{onConnect(n){i("Connected to session-proxy via IPC"),E=n,v=new N(t=>{l&&l.readyState===$.OPEN&&l.send(t)},t=>{l&&l.readyState===$.OPEN&&l.send(JSON.stringify(t))}),n.on("data",t=>v.feed(t))},onClose(){i("IPC connection to session-proxy lost, will retry..."),E=null,v=null},onError(n){n.code!=="ENOENT"&&n.code!=="ECONNREFUSED"&&i(`IPC error: ${n.message}`)}})}var l=null,w=null,h=0,T=null;function z(e=me){if(l){try{l.terminate()}catch{}l=null}T&&(clearTimeout(T),T=null),T=setTimeout(()=>{T=null,Z()},e)}function Z(){let e=++h,n=Ae();i(`Connecting to ${n}... (gen=${e}, sandbox=${g})`);let t={},r=_e();if(r&&(t.Authorization=`Bearer ${r}`),l){try{l.terminate()}catch{}l=null}let c=new $(n,{headers:t});l=c;let o=null,a=!0,s=setTimeout(()=>{if(e===h){i(`Handshake timeout after ${V}ms \u2014 aborting and reconnecting`);try{c.terminate()}catch{}u()}},V);function u(){s&&(clearTimeout(s),s=null),o&&(clearInterval(o),o=null),e===h&&z()}function _(){a=!0,o=setInterval(()=>{if(e!==h){clearInterval(o);return}if(!a){i("No pong received \u2014 connection dead, reconnecting"),u();return}a=!1;try{c.ping()}catch{u()}},Ee)}c.on("open",()=>{if(e!==h){c.terminate();return}s&&(clearTimeout(s),s=null),i(`Connected${w?` (reconnect, port ${w})`:""} (gen=${e})`),_()}),c.on("pong",()=>{e===h&&(a=!0)}),c.on("message",(d,f)=>{if(e===h){if(!f){try{Ne(JSON.parse(d.toString()))}catch(y){i(`Bad control msg: ${y.message}`)}return}E&&!E.destroyed&&L(E,Buffer.from(d))}}),c.on("close",(d,f)=>{if(e!==h)return;let y=f?f.toString():"";if(d===4100&&(i(`Replaced by newer connection (code 4100, reason=${y}) \u2014 exiting`),process.exit(42)),d===4101){i(`Identity verification rejected (code 4101, reason=${y}) \u2014 retrying in ${W}ms (likely waiting for envd to update /run/e2b/.E2B_SANDBOX_ID)`),s&&(clearTimeout(s),s=null),o&&(clearInterval(o),o=null),e===h&&z(W);return}i(`Data WS closed: code=${d}, reason=${y}`),u()}),c.on("error",d=>{e===h&&(i(`Data WS error: ${d.message} (code=${d.code||"none"})`),u())}),c.on("unexpected-response",(d,f)=>{e===h&&(i(`Data WS unexpected response: status=${f.statusCode}`),u())})}function Ne(e){if(e.type==="port"){w=e.port,i(`Assigned port ${e.port}`);return}if(e.type==="name"){if(i(`Name assigned: ${e.name}`),e.name&&/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(e.name)){let n=`${e.name}.local`;try{ae("hostname",[n],{timeout:5e3,stdio:"ignore"}),m.writeFileSync("/etc/hostname",n+`
|
|
15
|
+
`)}catch{}try{F(n)&&i(`Updated /etc/hosts entry for ${n}`)}catch(t){i(`Failed to update /etc/hosts: ${t.message}`)}try{m.writeFileSync(ye,e.name)}catch{}try{G(e.name,Te,i)}catch(t){i(`updateAgentMd failed: ${t.message}`)}}return}E&&!E.destroyed&&P(E,e)}function Q(){try{let e=A(`npm view ${X} version 2>/dev/null`,{timeout:15e3}).toString().trim();if(!e)return;let n="";try{n=JSON.parse(m.readFileSync("/opt/sandbox/lib/node_modules/@typhons/sandbox-tools/package.json","utf8")).version||""}catch{return}if(e!==n){i(`Update available: ${n} \u2192 ${e}, installing...`),A(`npm install -g --prefix /opt/sandbox ${X}@latest`,{timeout:12e4,stdio:"inherit"});try{A('pkill -f "check-claude-active.*--daemon" 2>/dev/null',{shell:!0,timeout:5e3})}catch{}try{let t=m.openSync("/tmp/claude-keepalive.log","a");se("/opt/sandbox/bin/check-claude-active",["--daemon"],{detached:!0,stdio:["ignore",t,t]}).unref(),m.closeSync(t)}catch{}i("Update installed, exiting for restart..."),process.exit(0)}}catch{}}function Re(){let e=ie.createServer((t,r)=>{if(r.setHeader("Content-Type","application/json"),t.method==="GET"&&t.url==="/health"){r.end(JSON.stringify({ok:!0,sandboxId:x(),wsConnected:!!(l&&l.readyState===$.OPEN)}));return}if(t.method==="POST"&&t.url==="/_relay/verify"){let c="";t.on("data",o=>{c+=o,c.length>4096&&t.destroy()}),t.on("end",()=>{let o;try{o=JSON.parse(c).nonce}catch{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(!l||l.readyState!==$.OPEN){r.writeHead(503),r.end('{"error":"WS not connected"}');return}try{l.send(JSON.stringify({type:"verify-ack",nonce:o}))}catch(a){r.writeHead(500),r.end(`{"error":"${a.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,i(`Port ${R} held by another process \u2014 killing it (likely a pre-0.5.5 session-proxy that owned this port; watchdog will restart it under new code which no longer binds it)`);try{A("pkill -f /opt/sandbox/lib/node_modules/@typhons/sandbox-tools/session-proxy.js",{timeout:3e3})}catch{}setTimeout(()=>{try{e.listen(R,"0.0.0.0")}catch{}},1500);return}i(`Keepalive HTTP server error: ${t.message}`)}),e.listen(R,"0.0.0.0",()=>{i(`Keepalive HTTP server on :${R}`)}),e}var Ve=$e();Re();Z();ge||(setTimeout(Q,1e4),setInterval(Q,Se));i(`Relay client started (sandbox=${x()})`);
|