thinkncollab-shell 2.0.0-beta.5 → 2.0.0-beta.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.
Files changed (72) hide show
  1. package/Readme.md +34 -17
  2. package/dist/src/commands/dock/cluster.js +1 -1
  3. package/dist/src/commands/dock/containers.js +1 -1
  4. package/dist/src/commands/room/sync.js +1 -1
  5. package/dist/src/commands/sandbox/common.js +7 -0
  6. package/dist/src/commands/sandbox/console.js +1 -1
  7. package/dist/src/commands/sandbox/dep.js +1 -1
  8. package/dist/src/commands/sandbox/deploy.js +1 -1
  9. package/dist/src/commands/sandbox/dev.js +1 -1
  10. package/dist/src/commands/sandbox/exec.js +1 -1
  11. package/dist/src/commands/sandbox/push.js +1 -1
  12. package/dist/src/commands/sandbox/sandbox.js +1 -1
  13. package/dist/src/commands/sandbox/share.js +1 -1
  14. package/dist/src/commands/sandbox/snapshot.js +1 -1
  15. package/dist/src/commands/shell/doctor.js +1 -1
  16. package/dist/src/commands/tasks/build.js +1 -1
  17. package/dist/src/commands/terminal/share.js +1 -1
  18. package/dist/src/core/crypto/seal.js +7 -0
  19. package/dist/src/core/dependency-tracker.js +1 -1
  20. package/dist/src/core/foreground.js +7 -0
  21. package/dist/src/core/platform-socket.js +7 -0
  22. package/dist/src/core/room/sync-manager.js +7 -0
  23. package/dist/src/core/shell.js +1 -1
  24. package/dist/src/core/{sandbox/snapshot-manager.js → snapshot-manager.js} +1 -1
  25. package/dist/src/core/tunnel/client.js +7 -0
  26. package/dist/src/core/tunnel/daemon.js +7 -0
  27. package/dist/src/core/tunnel/index.js +7 -0
  28. package/dist/src/core/websocket.js +1 -1
  29. package/dist/src/core/window-launcher.js +7 -0
  30. package/dist/src/dock/bench/memory.js +1 -1
  31. package/dist/src/dock/cli/commands.js +1 -1
  32. package/dist/src/dock/cli/k8s.js +7 -0
  33. package/dist/src/dock/cli/services.js +1 -1
  34. package/dist/src/dock/cli/session.js +1 -1
  35. package/dist/src/dock/cli/workspace.js +1 -1
  36. package/dist/src/dock/engine.js +1 -1
  37. package/dist/src/dock/image/certs.js +7 -0
  38. package/dist/src/dock/image/registry.js +1 -1
  39. package/dist/src/dock/room-data.js +7 -0
  40. package/dist/src/dock/runtime/network.js +1 -1
  41. package/dist/src/dock/sandbox.js +7 -0
  42. package/dist/src/dock/setup/index.js +1 -1
  43. package/dist/src/dock/workspace/share.js +7 -0
  44. package/dist/src/dock/workspace/workspace.js +1 -1
  45. package/package.json +1 -1
  46. package/dist/src/core/sandbox/arch-shim.js +0 -7
  47. package/dist/src/core/sandbox/capsule-builder.js +0 -7
  48. package/dist/src/core/sandbox/cgroups.js +0 -7
  49. package/dist/src/core/sandbox/container-monitor.js +0 -7
  50. package/dist/src/core/sandbox/crun-manager.js +0 -7
  51. package/dist/src/core/sandbox/engine.js +0 -7
  52. package/dist/src/core/sandbox/image-manager.js +0 -7
  53. package/dist/src/core/sandbox/image-store.js +0 -7
  54. package/dist/src/core/sandbox/index.js +0 -7
  55. package/dist/src/core/sandbox/macos-sandbox.js +0 -7
  56. package/dist/src/core/sandbox/microvm-manager.js +0 -7
  57. package/dist/src/core/sandbox/native-service-manager.js +0 -7
  58. package/dist/src/core/sandbox/network.js +0 -7
  59. package/dist/src/core/sandbox/oci-exporter.js +0 -7
  60. package/dist/src/core/sandbox/oci-puller.js +0 -7
  61. package/dist/src/core/sandbox/oci-runtime-spec.js +0 -7
  62. package/dist/src/core/sandbox/port-proxy-daemon.js +0 -7
  63. package/dist/src/core/sandbox/room-sync-manager.js +0 -7
  64. package/dist/src/core/sandbox/runtime-manager.js +0 -7
  65. package/dist/src/core/sandbox/storage.js +0 -7
  66. package/dist/src/core/sandbox/supervisor.js +0 -7
  67. package/dist/src/core/sandbox/tunnel-daemon.js +0 -7
  68. package/dist/src/core/sandbox/tunnel.js +0 -7
  69. package/dist/src/core/sandbox/window-launcher.js +0 -7
  70. package/dist/src/core/sandbox/windows-wsl2.js +0 -7
  71. package/dist/src/core/sandbox-window.js +0 -8
  72. /package/dist/src/core/{sandbox/policy-engine.js → policy-engine.js} +0 -0
@@ -4,4 +4,4 @@
4
4
  * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
5
  * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
6
  */
7
- "use strict";const crypto=require("crypto"),DEFAULT_BRIDGE={id:"bridge",name:"bridge",driver:"bridge",builtin:!0,dns:!1,bridgeIf:"tnc0",subnet:"172.28.0.0/16",gateway:"172.28.0.1",prefixLen:16,poolStart:"172.28.128.1",poolEnd:"172.28.255.254"},HOST={id:"host",name:"host",driver:"host",builtin:!0},NONE={id:"none",name:"none",driver:"null",builtin:!0},USER_POOL={base:"172.29",first:0,last:255},ipToInt=e=>e.split(".").reduce((e,n)=>(e<<8|Number(n))>>>0,0),intToIp=e=>[e>>>24,e>>>16&255,e>>>8&255,255&e].join(".");function httpError(e,n){const t=new Error(n);return t.statusCode=e,t}function ipv4Range(e){const n=/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/.exec(e.Subnet);if(!n||Number(n[2])<8||Number(n[2])>30)throw httpError(400,`invalid subnet ${e.Subnet}`);const t=Number(n[2]),i=-1<<32-t>>>0,r=(ipToInt(n[1])&i)>>>0,s=(r|~i)>>>0;let[a,o]=[r+1,s-1];if(e.IPRange){const n=/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/.exec(e.IPRange);if(!n)throw httpError(400,`invalid ip range ${e.IPRange}`);const t=-1<<32-Number(n[2])>>>0,s=(ipToInt(n[1])&t)>>>0;if((s&i)>>>0!==r)throw httpError(400,`ip range ${e.IPRange} is not inside ${e.Subnet}`);[a,o]=[Math.max(a,s),Math.min(o,(s|~t)>>>0)]}return{netInt:r,plen:t,lo:a,hi:o}}const NETNS_DEFAULTS='ip netns exec "$NS" sh -c \'echo 0 > /proc/sys/net/ipv4/ip_unprivileged_port_start; echo "0 2147483647" > /proc/sys/net/ipv4/ping_group_range\' 2>/dev/null || true',BASE_SETUP="\nmkdir -p /var/run/netns\necho 1 > /proc/sys/net/ipv4/ip_forward\niptables -N TNC-ISOLATION 2>/dev/null || true\niptables -C FORWARD -j TNC-ISOLATION 2>/dev/null || iptables -I FORWARD 1 -j TNC-ISOLATION\niptables -C FORWARD -i tnc+ -j ACCEPT 2>/dev/null || iptables -A FORWARD -i tnc+ -j ACCEPT\niptables -C FORWARD -o tnc+ -j ACCEPT 2>/dev/null || iptables -A FORWARD -o tnc+ -j ACCEPT\n",BRIDGE_SETUP=`\n${BASE_SETUP}\nbr="$1"; gw="$2"; plen="$3"; subnet="$4"; chain="$5"; internal="$6"\nip link show "$br" >/dev/null 2>&1 || ip link add "$br" type bridge\nip -4 addr show dev "$br" | grep -q "$gw/$plen" || ip addr add "$gw/$plen" dev "$br"\nip link set "$br" up\nif [ "$internal" = 1 ]; then\n iptables -t nat -D POSTROUTING -s "$subnet" ! -o "$br" -j MASQUERADE 2>/dev/null || true\nelse\n iptables -t nat -C POSTROUTING -s "$subnet" ! -o "$br" -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s "$subnet" ! -o "$br" -j MASQUERADE\nfi\nif ! iptables -L "$chain" -n >/dev/null 2>&1; then\n iptables -N "$chain"\n iptables -A "$chain" -o "$br" -j RETURN\n iptables -A "$chain" -o tnc+ -j DROP\n if [ "$internal" = 1 ]; then iptables -A "$chain" -j DROP; fi\n iptables -A "$chain" -j RETURN\nfi\niptables -C TNC-ISOLATION -i "$br" -j "$chain" 2>/dev/null || iptables -A TNC-ISOLATION -i "$br" -j "$chain"\n# IPv6 (network create --ipv6): $7 subnet6, $8 gateway6\nsubnet6="$7"; gw6="$8"\nif [ -n "$gw6" ]; then\n echo 0 > "/proc/sys/net/ipv6/conf/$br/disable_ipv6"\n echo 1 > /proc/sys/net/ipv6/conf/all/forwarding\n ip -6 addr show dev "$br" | grep -q "$gw6/64" || ip -6 addr add "$gw6/64" dev "$br" nodad\n if command -v ip6tables >/dev/null 2>&1; then\n ip6tables -N TNC-ISOLATION 2>/dev/null || true\n ip6tables -C FORWARD -j TNC-ISOLATION 2>/dev/null || ip6tables -I FORWARD 1 -j TNC-ISOLATION\n ip6tables -C FORWARD -i tnc+ -j ACCEPT 2>/dev/null || ip6tables -A FORWARD -i tnc+ -j ACCEPT\n ip6tables -C FORWARD -o tnc+ -j ACCEPT 2>/dev/null || ip6tables -A FORWARD -o tnc+ -j ACCEPT\n if ! ip6tables -L "$chain" -n >/dev/null 2>&1; then\n ip6tables -N "$chain"\n ip6tables -A "$chain" -o "$br" -j RETURN\n ip6tables -A "$chain" -o tnc+ -j DROP\n if [ "$internal" = 1 ]; then ip6tables -A "$chain" -j DROP; fi\n ip6tables -A "$chain" -j RETURN\n fi\n ip6tables -C TNC-ISOLATION -i "$br" -j "$chain" 2>/dev/null || ip6tables -A TNC-ISOLATION -i "$br" -j "$chain"\n # outbound IPv6 through this machine's address; best effort (a kernel without IPv6 NAT keeps it inside)\n if [ "$internal" != 1 ]; then\n ip6tables -t nat -C POSTROUTING -s "$subnet6" ! -o "$br" -j MASQUERADE 2>/dev/null || ip6tables -t nat -A POSTROUTING -s "$subnet6" ! -o "$br" -j MASQUERADE 2>/dev/null || true\n fi\n fi\nfi\n`,BRIDGE_REMOVE='\nbr="$1"; subnet="$2"; chain="$3"; subnet6="$4"\niptables -D TNC-ISOLATION -i "$br" -j "$chain" 2>/dev/null\niptables -F "$chain" 2>/dev/null; iptables -X "$chain" 2>/dev/null\niptables -t nat -D POSTROUTING -s "$subnet" ! -o "$br" -j MASQUERADE 2>/dev/null\nif [ -n "$subnet6" ] && command -v ip6tables >/dev/null 2>&1; then\n ip6tables -D TNC-ISOLATION -i "$br" -j "$chain" 2>/dev/null\n ip6tables -F "$chain" 2>/dev/null; ip6tables -X "$chain" 2>/dev/null\n ip6tables -t nat -D POSTROUTING -s "$subnet6" ! -o "$br" -j MASQUERADE 2>/dev/null\nfi\nip link del "$br" 2>/dev/null\ntrue\n';function ipv6Subnet(e,n={}){const t=(n.Config||[]).map(e=>e.Subnet).find(e=>e&&e.includes(":"));if(t){const[e,n]=t.split("/");if(64!==Number(n))throw httpError(400,`IPv6 subnet ${t}: only /64 subnets are supported`);const i=expand6(e);if(!i)throw httpError(400,`invalid IPv6 subnet ${t}`);return i.slice(0,4).map(e=>e.replace(/^0+(?=.)/,"")).join(":")}return`fd${e.slice(0,2)}:${e.slice(2,6)}:${e.slice(6,10)}:0`}function expand6(e){if(!/^[0-9a-fA-F:]+$/.test(e)||(e.match(/::/g)||[]).length>1)return null;const[n,t]=e.split("::"),i=n?n.split(":"):[],r=void 0!==t&&t?t.split(":"):[],s=e.includes("::")?8-i.length-r.length:0,a=[...i,...Array(Math.max(0,s)).fill("0"),...r];return 8!==a.length||a.some(e=>!/^[0-9a-fA-F]{1,4}$/.test(e))?null:a.map(e=>e.toLowerCase().padStart(4,"0"))}const ATTACH=`\nset -e\nns="$1"; br="$2"; vh="$3"; vp="$4"; ifn="$5"; ip="$6"; plen="$7"; mac="$8"; gw="$9"; route="\${10}"\nmkdir -p /var/run/netns\n[ -e "/var/run/netns/$ns" ] || { ip netns add "$ns"; ip -n "$ns" link set lo up; ${NETNS_DEFAULTS.replace(/\$NS/g,"$ns")}; }\nip link del "$vh" 2>/dev/null || true\nip link add "$vh" type veth peer name "$vp"\nip link set "$vh" master "$br"\nip link set "$vh" up\nip link set "$vp" netns "$ns"\nip -n "$ns" link set "$vp" name "$ifn"\nip -n "$ns" link set "$ifn" address "$mac"\nip -n "$ns" addr add "$ip/$plen" dev "$ifn"\n# an address may have belonged to a container that just left: drop the bridge's\n# stale neighbour entry and announce the new MAC (gratuitous ARP on link up)\nip neigh flush to "$ip" dev "$br" 2>/dev/null || true\nip netns exec "$ns" sh -c "echo 1 > /proc/sys/net/ipv4/conf/$ifn/arp_notify" 2>/dev/null || true\nip -n "$ns" link set "$ifn" up\nif [ "$route" = 1 ]; then ip -n "$ns" route replace default via "$gw"; fi\n# \${13}: MTU of an overlay network (the tunnel takes 50 bytes of every packet)\nif [ -n "\${13}" ]; then ip link set "$vh" mtu "\${13}"; ip -n "$ns" link set "$ifn" mtu "\${13}"; fi\n# IPv6: \${11} address, \${12} gateway (empty: an IPv4-only network)\nip6="\${11}"; gw6="\${12}"\nif [ -n "$ip6" ]; then\n ip netns exec "$ns" sh -c "echo 0 > /proc/sys/net/ipv6/conf/$ifn/disable_ipv6"\n ip -n "$ns" -6 addr add "$ip6/64" dev "$ifn" nodad\n if [ "$route" = 1 ]; then ip -n "$ns" -6 route replace default via "$gw6" dev "$ifn"; fi\nfi\n`,LAN_ATTACH=`\nset -e\nns="$1"; parent="$2"; kind="$3"; mode="$4"; vp="$5"; ifn="$6"; ip="$7"; plen="$8"; gw="$9"; route="\${10}"; mac="\${11}"\nmkdir -p /var/run/netns\n[ -e "/var/run/netns/$ns" ] || { ip netns add "$ns"; ip -n "$ns" link set lo up; ${NETNS_DEFAULTS.replace(/\$NS/g,"$ns")}; }\n[ -n "$parent" ] || parent=$(ip -4 route show default | awk '{print $5; exit}')\nip link show "$parent" >/dev/null 2>&1 || { echo "parent interface \\"$parent\\" does not exist on this machine" >&2; exit 1; }\nip link del "$vp" 2>/dev/null || true\nif [ "$kind" = macvlan ]; then ip link add "$vp" link "$parent" type macvlan mode "$mode"; else ip link add "$vp" link "$parent" type ipvlan mode "$mode"; fi\nip link set "$vp" netns "$ns"\nip -n "$ns" link set "$vp" name "$ifn"\nif [ "$kind" = macvlan ]; then ip -n "$ns" link set "$ifn" address "$mac"; fi\nip -n "$ns" addr add "$ip/$plen" dev "$ifn"\nip -n "$ns" link set "$ifn" up\nif [ "$route" = 1 ] && [ -n "$gw" ]; then ip -n "$ns" route replace default via "$gw"; fi\n`,VXLAN_SETUP='\nset -e\nbr="$1"; vx="$2"; vni="$3"; port="$4"; local="$5"; mtu="$6"; shift 6\nif ! ip link show "$vx" >/dev/null 2>&1; then\n if [ -n "$local" ]; then ip link add "$vx" type vxlan id "$vni" dstport "$port" local "$local"; else ip link add "$vx" type vxlan id "$vni" dstport "$port"; fi\nfi\nip link set "$vx" mtu "$mtu"\nip link set "$br" mtu "$mtu"\nip link set "$vx" master "$br"\nip link set "$vx" up\nfor peer; do\n bridge fdb show dev "$vx" | grep -q "dst $peer " || bridge fdb append 00:00:00:00:00:00 dev "$vx" dst "$peer"\ndone\niptables -C INPUT -p udp --dport "$port" -j ACCEPT 2>/dev/null || iptables -I INPUT -p udp --dport "$port" -j ACCEPT\n# the engines\' name announcements on the overlay itself (runtime/overlaydns.js)\niptables -C INPUT -i "$br" -p udp --dport 7946 -j ACCEPT 2>/dev/null || iptables -I INPUT -i "$br" -p udp --dport 7946 -j ACCEPT\n',NETNS_ONLY=`set -e; mkdir -p /var/run/netns; [ -e "/var/run/netns/$1" ] || { ip netns add "$1"; ip -n "$1" link set lo up; ${NETNS_DEFAULTS.replace(/\$NS/g,"$1")}; }`,DETACH_ONE='ip -n "$1" link del "$2" 2>/dev/null; ip link del "$3" 2>/dev/null; true',DETACH_ALL='ip netns del "$1" 2>/dev/null; true';class Network{constructor({host:e,store:n}){this.host=e,this.store=n,this.reserved=new Set,this.lastIp=new Map}builtins(){return[DEFAULT_BRIDGE,HOST,NONE]}resolve(e){if(!e||"default"===e||"bridge"===e)return DEFAULT_BRIDGE;if("host"===e)return HOST;if("none"===e)return NONE;const n=this.store.findNetwork(e);if(!n)throw httpError(404,`network ${e} not found`);return{...n,dns:!0,prefixLen:n.prefixLen||24}}list(){return[...this.builtins(),...this.store.listNetworks().map(e=>({...e,dns:!0,prefixLen:e.prefixLen||24}))]}create({Name:e,Driver:n,Labels:t,Internal:i=!1,Options:r,EnableIPv6:s=!1,IPAM:a}){if(n=n||"bridge",t=t||{},r=r||{},!e)throw httpError(400,"network name is required");if(!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(e))throw httpError(400,`invalid network name "${e}", only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed`);if(["bridge","host","none","default"].includes(e))throw httpError(403,`${e} is a pre-defined network and cannot be created`);if(this.store.findNetwork(e)?.name===e)throw httpError(409,`network with name ${e} already exists`);if("macvlan"===n||"ipvlan"===n)return this._createLan({Name:e,Driver:n,Labels:t,Internal:i,Options:r,IPAM:a});if("overlay"===n)return this._createOverlay({Name:e,Labels:t,Internal:i,Options:r,IPAM:a});if("bridge"!==n)throw httpError(400,`network driver "${n}" is not supported; use bridge, macvlan, ipvlan or overlay`);const o=new Set(this.store.listNetworks().map(e=>e.subnet));let l=null;for(let e=USER_POOL.first;e<=USER_POOL.last;e++){const n=`${USER_POOL.base}.${e}.0/24`;if(!o.has(n)){l=n;break}}if(!l)throw httpError(500,"no free subnets left in 172.29.0.0/16");const p=crypto.randomBytes(32).toString("hex"),d=l.split("/")[0].split(".").slice(0,3).join("."),h=s||(a?.Config||[]).some(e=>e.Subnet?.includes(":"))?ipv6Subnet(p,a):null;return this.store.insertNetwork({id:p,name:e,driver:"bridge",subnet:l,gateway:`${d}.1`,poolStart:`${d}.2`,poolEnd:`${d}.254`,...h?{subnet6:`${h}::/64`,gateway6:`${h}::1`,prefix6:h}:{},bridgeIf:`tncb-${p.slice(0,8)}`,chain:`TNC-ISO-${p.slice(0,8)}`,labels:t,internal:!!i,options:r})}_needLinuxRoot(e,n){if("local"!==this.host.kind)throw httpError(400,`${e} networks need dock on Linux as root: here containers run behind ${{vm:"the Linux VM's NAT",wsl:"WSL's NAT",rootless:"rootless user-mode networking"}[this.host.kind]||"NAT"}, so ${n}`)}_createLan({Name:e,Driver:n,Labels:t,Internal:i,Options:r,IPAM:s}){this._needLinuxRoot(n,"there is no LAN interface to put them on");const a=(s?.Config||[]).find(e=>e.Subnet&&!e.Subnet.includes(":"));if(!a)throw httpError(400,`a ${n} network needs the LAN's subnet and gateway, e.g. --subnet 192.168.1.0/24 --gateway 192.168.1.1`);const{netInt:o,plen:l,lo:p,hi:d}=ipv4Range(a),h=a.Gateway||intToIp(o+1),$="macvlan"===n?r.macvlan_mode||"bridge":r.ipvlan_mode||"l2";if(!("macvlan"===n?["bridge","private","vepa","passthru"]:["l2","l3","l3s"]).includes($))throw httpError(400,`invalid ${n} mode ${$}`);if(r.parent&&!/^[A-Za-z0-9_.@-]{1,15}$/.test(r.parent))throw httpError(400,`invalid parent interface ${r.parent}`);const u=crypto.randomBytes(32).toString("hex");return this.store.insertNetwork({id:u,name:e,driver:n,subnet:`${intToIp(o)}/${l}`,gateway:h,prefixLen:l,poolStart:intToIp(p),poolEnd:intToIp(d),parent:r.parent||"",mode:$,labels:t,internal:!!i,options:r})}_createOverlay({Name:e,Labels:n,Internal:t,Options:i,IPAM:r}){this._needLinuxRoot("overlay","other machines cannot reach its tunnel");const s=(r?.Config||[]).find(e=>e.Subnet&&!e.Subnet.includes(":"));if(!s)throw httpError(400,"an overlay network needs its subnet (the same on every machine), e.g. --subnet 10.20.0.0/16 --ip-range 10.20.1.0/24");const a=String(i.peers||"").split(",").map(e=>e.trim()).filter(Boolean);for(const e of a)if(!require("net").isIPv4(e))throw httpError(400,`peer ${e} is not an IPv4 address`);if(a.length&&!s.IPRange)throw httpError(400,"an overlay network across machines needs --ip-range: a different part of the subnet on each machine, so their containers never get the same address");const{netInt:o,plen:l,hi:p}=ipv4Range(s);let{lo:d}=ipv4Range(s);if(255&d||d++,d>=p)throw httpError(400,`ip range ${s.IPRange||s.Subnet} is too small`);const h=s.Gateway||intToIp(d),$=2**(32-l);for(const e of[DEFAULT_BRIDGE,...this.store.listNetworks()]){const[n,t]=String(e.subnet||"").split("/");if(!n||n.includes(":"))continue;const i=ipToInt(n);if(o<i+2**(32-Number(t))&&i<o+$)throw httpError(400,`subnet ${s.Subnet} overlaps network ${e.name} (${e.subnet})`)}const u=void 0!==i.vni?Number(i.vni):parseInt(crypto.createHash("sha256").update(e).digest("hex").slice(0,6),16)||1;if(!Number.isInteger(u)||u<1||u>16777215)throw httpError(400,"vni must be a number from 1 to 16777215");const c=Number(i.port||4789),v=Number(i.mtu||1450);if(!Number.isInteger(c)||c<1||c>65535)throw httpError(400,"port must be a UDP port number");if(!Number.isInteger(v)||v<1280||v>9e3)throw httpError(400,"mtu must be from 1280 to 9000");if(i.local&&!require("net").isIPv4(i.local))throw httpError(400,`local ${i.local} is not an IPv4 address`);const f=crypto.randomBytes(32).toString("hex");return this.store.insertNetwork({id:f,name:e,driver:"overlay",subnet:`${intToIp(o)}/${l}`,gateway:h,prefixLen:l,poolStart:intToIp(s.Gateway?d:d+1),poolEnd:intToIp(p),bridgeIf:`tnco-${f.slice(0,8)}`,vxlanIf:`tncx-${f.slice(0,8)}`,chain:`TNC-ISO-${f.slice(0,8)}`,vni:u,port:c,mtu:v,peers:a,local:i.local||"",labels:n,internal:!!t,options:i})}_allocate6(e,n){if(!e.prefix6)return null;const t=new Set(this.reserved);for(const e of this.store.listContainers())if(e.id!==n)for(const n of Object.values(e.runtime.networks||{}))n.ip6&&t.add(n.ip6);for(let n=2;n<65535;n++){const i=`${e.prefix6}::${n.toString(16)}`;if(!t.has(i))return this.reserved.add(i),i}throw httpError(500,`no free IPv6 addresses left in network ${e.name}`)}async remove(e){if(e.builtin)throw httpError(403,`${e.name} is a pre-defined network and cannot be removed`);"bridge"!==e.driver&&"overlay"!==e.driver||!await this.host.isUp().catch(()=>!1)||(e.vxlanIf&&await this.host.sh('ip link del "$1" 2>/dev/null; true',[e.vxlanIf]),await this.host.sh(BRIDGE_REMOVE,[e.bridgeIf,e.subnet,e.chain,e.subnet6||""])),this.store.deleteNetwork(e.id)}_allocate(e,n){const t=new Set(this.reserved);e.gateway&&t.add(e.gateway);for(const e of this.store.listContainers())if(e.id!==n){for(const n of Object.values(e.runtime.networks||{}))n.ip&&t.add(n.ip);e.runtime.ip&&t.add(e.runtime.ip)}const i=ipToInt(e.poolStart),r=ipToInt(e.poolEnd)-i+1,s=this.lastIp.get(e.id),a=s?(ipToInt(s)-i+1)%r:0;for(let n=0;n<r;n++){const s=intToIp(i+(a+n)%r);if(!t.has(s))return this.reserved.add(s),this.lastIp.set(e.id,s),s}throw httpError(500,`no free addresses left in network ${e.name}`)}names(e,n){const t=/^[0-9a-f]{12}/.test(e)?e:crypto.createHash("sha256").update(e).digest("hex");return{ns:`tnc-${t.slice(0,12)}`,vh:`vt${t.slice(0,8)}${n}`,vp:`vp${t.slice(0,8)}${n}`}}async _ensureBridge(e){const n=e.builtin?"TNC-ISO-default":e.chain;await this.host.sh(BRIDGE_SETUP,[e.bridgeIf,e.gateway,e.prefixLen,e.subnet,n,e.internal?1:0,e.subnet6||"",e.gateway6||""]),"overlay"===e.driver&&await this.host.sh(VXLAN_SETUP,[e.bridgeIf,e.vxlanIf,String(e.vni),String(e.port),e.local||"",String(e.mtu),...e.peers||[]]).catch(n=>{throw httpError(500,`overlay network ${e.name}: ${/vxlan|Unknown device type|not supported/i.test(n.message)?"this kernel has no VXLAN support (modprobe vxlan)":n.message}`)})}async attachOne(e,n,t,i=[]){const{ns:r,vh:s,vp:a}=this.names(e,t);if("null"===n.driver)return await this.host.sh(NETNS_ONLY,[r]),{networkId:"none",ifname:null,aliases:i};if("macvlan"===n.driver||"ipvlan"===n.driver){const s=this._allocate(n,e),o="02:42:"+crypto.createHash("sha1").update(`${e}:${n.id}`).digest("hex").slice(0,8).match(/../g).join(":");try{await this.host.sh(LAN_ATTACH,[r,n.parent||"",n.driver,n.mode,a,`eth${t}`,s,n.prefixLen,n.gateway,0===t?1:0,o])}catch(e){throw this.reserved.delete(s),e}return{networkId:n.id,ifname:`eth${t}`,idx:t,ip:s,mac:"macvlan"===n.driver?o:"",gateway:n.gateway,prefixLen:n.prefixLen,aliases:i}}await this._ensureBridge(n);const o=this._allocate(n,e),l=this._allocate6(n,e),p="02:42:"+crypto.createHash("sha1").update(`${e}:${n.id}`).digest("hex").slice(0,8).match(/../g).join(":");try{await this.host.sh(ATTACH,[r,n.bridgeIf,s,a,`eth${t}`,o,n.prefixLen,p,n.gateway,0===t?1:0,l||"",n.gateway6||"","overlay"===n.driver?String(n.mtu):""])}catch(e){throw this.reserved.delete(o),l&&this.reserved.delete(l),e}return{networkId:n.id,ifname:`eth${t}`,idx:t,ip:o,mac:p,gateway:n.gateway,prefixLen:n.prefixLen,aliases:i,...l?{ip6:l,gateway6:n.gateway6,prefixLen6:64}:{}}}async attachAll(e,n){const t=n.map(e=>({net:this.resolve(e.name),aliases:e.aliases||[]}));if("host"===t[0].net.driver)return{nsPath:null,endpoints:{host:{networkId:"host"}}};const i={};try{for(let n=0;n<t.length;n++)i[t[n].net.name]=await this.attachOne(e,t[n].net,n,t[n].aliases)}catch(n){throw await this.detachAll(e,i),n}return{nsPath:`/var/run/netns/${this.names(e,0).ns}`,endpoints:i}}async detachOne(e,n){if(!n||void 0===n.idx)return;const{ns:t,vh:i}=this.names(e,n.idx);await this.host.sh(DETACH_ONE,[t,n.ifname,i]).catch(()=>{}),n.ip&&this.reserved.delete(n.ip),n.ip6&&this.reserved.delete(n.ip6)}async detachAll(e,n={}){if(!n.host){await this.host.sh(DETACH_ALL,[this.names(e,0).ns]).catch(()=>{});for(const e of Object.values(n))e.ip&&this.reserved.delete(e.ip),e.ip6&&this.reserved.delete(e.ip6)}}}module.exports={Network,DEFAULT_BRIDGE,ipToInt};
7
+ "use strict";const crypto=require("crypto"),DEFAULT_BRIDGE={id:"bridge",name:"bridge",driver:"bridge",builtin:!0,dns:!1,bridgeIf:"tnc0",subnet:"172.28.0.0/16",gateway:"172.28.0.1",prefixLen:16,poolStart:"172.28.128.1",poolEnd:"172.28.255.254"},HOST={id:"host",name:"host",driver:"host",builtin:!0},NONE={id:"none",name:"none",driver:"null",builtin:!0},USER_POOL={base:"172.29",first:0,last:255},ipToInt=e=>e.split(".").reduce((e,t)=>(e<<8|Number(t))>>>0,0),intToIp=e=>[e>>>24,e>>>16&255,e>>>8&255,255&e].join(".");function httpError(e,t){const n=new Error(t);return n.statusCode=e,n}function ipv4Range(e){const t=/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/.exec(e.Subnet);if(!t||Number(t[2])<8||Number(t[2])>30)throw httpError(400,`invalid subnet ${e.Subnet}`);const n=Number(t[2]),i=-1<<32-n>>>0,r=(ipToInt(t[1])&i)>>>0,s=(r|~i)>>>0;let[a,o]=[r+1,s-1];if(e.IPRange){const t=/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/.exec(e.IPRange);if(!t)throw httpError(400,`invalid ip range ${e.IPRange}`);const n=-1<<32-Number(t[2])>>>0,s=(ipToInt(t[1])&n)>>>0;if((s&i)>>>0!==r)throw httpError(400,`ip range ${e.IPRange} is not inside ${e.Subnet}`);[a,o]=[Math.max(a,s),Math.min(o,(s|~n)>>>0)]}return{netInt:r,plen:n,lo:a,hi:o}}const NETNS_DEFAULTS='ip netns exec "$NS" sh -c \'echo 0 > /proc/sys/net/ipv4/ip_unprivileged_port_start; echo "0 2147483647" > /proc/sys/net/ipv4/ping_group_range\' 2>/dev/null || true',BASE_SETUP="\nmkdir -p /var/run/netns\necho 1 > /proc/sys/net/ipv4/ip_forward\niptables -N TNC-ISOLATION 2>/dev/null || true\niptables -C FORWARD -j TNC-ISOLATION 2>/dev/null || iptables -I FORWARD 1 -j TNC-ISOLATION\niptables -C FORWARD -i tnc+ -j ACCEPT 2>/dev/null || iptables -A FORWARD -i tnc+ -j ACCEPT\niptables -C FORWARD -o tnc+ -j ACCEPT 2>/dev/null || iptables -A FORWARD -o tnc+ -j ACCEPT\n",BRIDGE_SETUP=`\n${BASE_SETUP}\nbr="$1"; gw="$2"; plen="$3"; subnet="$4"; chain="$5"; internal="$6"\nip link show "$br" >/dev/null 2>&1 || ip link add "$br" type bridge\nip -4 addr show dev "$br" | grep -q "$gw/$plen" || ip addr add "$gw/$plen" dev "$br"\nip link set "$br" up\nif [ "$internal" = 1 ]; then\n iptables -t nat -D POSTROUTING -s "$subnet" ! -o "$br" -j MASQUERADE 2>/dev/null || true\nelse\n iptables -t nat -C POSTROUTING -s "$subnet" ! -o "$br" -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s "$subnet" ! -o "$br" -j MASQUERADE\nfi\nif ! iptables -L "$chain" -n >/dev/null 2>&1; then\n iptables -N "$chain"\n iptables -A "$chain" -o "$br" -j RETURN\n iptables -A "$chain" -o tnc+ -j DROP\n if [ "$internal" = 1 ]; then iptables -A "$chain" -j DROP; fi\n iptables -A "$chain" -j RETURN\nfi\niptables -C TNC-ISOLATION -i "$br" -j "$chain" 2>/dev/null || iptables -A TNC-ISOLATION -i "$br" -j "$chain"\n# IPv6 (network create --ipv6): $7 subnet6, $8 gateway6\nsubnet6="$7"; gw6="$8"\nif [ -n "$gw6" ]; then\n echo 0 > "/proc/sys/net/ipv6/conf/$br/disable_ipv6"\n echo 1 > /proc/sys/net/ipv6/conf/all/forwarding\n ip -6 addr show dev "$br" | grep -q "$gw6/64" || ip -6 addr add "$gw6/64" dev "$br" nodad\n if command -v ip6tables >/dev/null 2>&1; then\n ip6tables -N TNC-ISOLATION 2>/dev/null || true\n ip6tables -C FORWARD -j TNC-ISOLATION 2>/dev/null || ip6tables -I FORWARD 1 -j TNC-ISOLATION\n ip6tables -C FORWARD -i tnc+ -j ACCEPT 2>/dev/null || ip6tables -A FORWARD -i tnc+ -j ACCEPT\n ip6tables -C FORWARD -o tnc+ -j ACCEPT 2>/dev/null || ip6tables -A FORWARD -o tnc+ -j ACCEPT\n if ! ip6tables -L "$chain" -n >/dev/null 2>&1; then\n ip6tables -N "$chain"\n ip6tables -A "$chain" -o "$br" -j RETURN\n ip6tables -A "$chain" -o tnc+ -j DROP\n if [ "$internal" = 1 ]; then ip6tables -A "$chain" -j DROP; fi\n ip6tables -A "$chain" -j RETURN\n fi\n ip6tables -C TNC-ISOLATION -i "$br" -j "$chain" 2>/dev/null || ip6tables -A TNC-ISOLATION -i "$br" -j "$chain"\n # outbound IPv6 through this machine's address; best effort (a kernel without IPv6 NAT keeps it inside)\n if [ "$internal" != 1 ]; then\n ip6tables -t nat -C POSTROUTING -s "$subnet6" ! -o "$br" -j MASQUERADE 2>/dev/null || ip6tables -t nat -A POSTROUTING -s "$subnet6" ! -o "$br" -j MASQUERADE 2>/dev/null || true\n fi\n fi\nfi\n`,BRIDGE_REMOVE='\nbr="$1"; subnet="$2"; chain="$3"; subnet6="$4"\niptables -D TNC-ISOLATION -i "$br" -j "$chain" 2>/dev/null\niptables -F "$chain" 2>/dev/null; iptables -X "$chain" 2>/dev/null\niptables -t nat -D POSTROUTING -s "$subnet" ! -o "$br" -j MASQUERADE 2>/dev/null\nif [ -n "$subnet6" ] && command -v ip6tables >/dev/null 2>&1; then\n ip6tables -D TNC-ISOLATION -i "$br" -j "$chain" 2>/dev/null\n ip6tables -F "$chain" 2>/dev/null; ip6tables -X "$chain" 2>/dev/null\n ip6tables -t nat -D POSTROUTING -s "$subnet6" ! -o "$br" -j MASQUERADE 2>/dev/null\nfi\nip link del "$br" 2>/dev/null\ntrue\n';function ipv6Subnet(e,t={}){const n=(t.Config||[]).map(e=>e.Subnet).find(e=>e&&e.includes(":"));if(n){const[e,t]=n.split("/");if(64!==Number(t))throw httpError(400,`IPv6 subnet ${n}: only /64 subnets are supported`);const i=expand6(e);if(!i)throw httpError(400,`invalid IPv6 subnet ${n}`);return i.slice(0,4).map(e=>e.replace(/^0+(?=.)/,"")).join(":")}return`fd${e.slice(0,2)}:${e.slice(2,6)}:${e.slice(6,10)}:0`}function expand6(e){if(!/^[0-9a-fA-F:]+$/.test(e)||(e.match(/::/g)||[]).length>1)return null;const[t,n]=e.split("::"),i=t?t.split(":"):[],r=void 0!==n&&n?n.split(":"):[],s=e.includes("::")?8-i.length-r.length:0,a=[...i,...Array(Math.max(0,s)).fill("0"),...r];return 8!==a.length||a.some(e=>!/^[0-9a-fA-F]{1,4}$/.test(e))?null:a.map(e=>e.toLowerCase().padStart(4,"0"))}const ATTACH=`\nset -e\nns="$1"; br="$2"; vh="$3"; vp="$4"; ifn="$5"; ip="$6"; plen="$7"; mac="$8"; gw="$9"; route="\${10}"\nmkdir -p /var/run/netns\n[ -e "/var/run/netns/$ns" ] || { ip netns add "$ns"; ip -n "$ns" link set lo up; ${NETNS_DEFAULTS.replace(/\$NS/g,"$ns")}; }\nip link del "$vh" 2>/dev/null || true\nip link add "$vh" type veth peer name "$vp"\nip link set "$vh" master "$br"\nip link set "$vh" up\nip link set "$vp" netns "$ns"\nip -n "$ns" link set "$vp" name "$ifn"\nip -n "$ns" link set "$ifn" address "$mac"\nip -n "$ns" addr add "$ip/$plen" dev "$ifn"\n# an address may have belonged to a container that just left: drop the bridge's\n# stale neighbour entry and announce the new MAC (gratuitous ARP on link up)\nip neigh flush to "$ip" dev "$br" 2>/dev/null || true\nip netns exec "$ns" sh -c "echo 1 > /proc/sys/net/ipv4/conf/$ifn/arp_notify" 2>/dev/null || true\nip -n "$ns" link set "$ifn" up\nif [ "$route" = 1 ]; then ip -n "$ns" route replace default via "$gw"; fi\n# \${13}: MTU of an overlay network (the tunnel takes 50 bytes of every packet)\nif [ -n "\${13}" ]; then ip link set "$vh" mtu "\${13}"; ip -n "$ns" link set "$ifn" mtu "\${13}"; fi\n# IPv6: \${11} address, \${12} gateway (empty: an IPv4-only network)\nip6="\${11}"; gw6="\${12}"\nif [ -n "$ip6" ]; then\n ip netns exec "$ns" sh -c "echo 0 > /proc/sys/net/ipv6/conf/$ifn/disable_ipv6"\n ip -n "$ns" -6 addr add "$ip6/64" dev "$ifn" nodad\n if [ "$route" = 1 ]; then ip -n "$ns" -6 route replace default via "$gw6" dev "$ifn"; fi\nfi\n`,LAN_ATTACH=`\nset -e\nns="$1"; parent="$2"; kind="$3"; mode="$4"; vp="$5"; ifn="$6"; ip="$7"; plen="$8"; gw="$9"; route="\${10}"; mac="\${11}"\nmkdir -p /var/run/netns\n[ -e "/var/run/netns/$ns" ] || { ip netns add "$ns"; ip -n "$ns" link set lo up; ${NETNS_DEFAULTS.replace(/\$NS/g,"$ns")}; }\n[ -n "$parent" ] || parent=$(ip -4 route show default | awk '{print $5; exit}')\nip link show "$parent" >/dev/null 2>&1 || { echo "parent interface \\"$parent\\" does not exist on this machine" >&2; exit 1; }\nip link del "$vp" 2>/dev/null || true\nif [ "$kind" = macvlan ]; then ip link add "$vp" link "$parent" type macvlan mode "$mode"; else ip link add "$vp" link "$parent" type ipvlan mode "$mode"; fi\nip link set "$vp" netns "$ns"\nip -n "$ns" link set "$vp" name "$ifn"\nif [ "$kind" = macvlan ]; then ip -n "$ns" link set "$ifn" address "$mac"; fi\nip -n "$ns" addr add "$ip/$plen" dev "$ifn"\nip -n "$ns" link set "$ifn" up\nif [ "$route" = 1 ] && [ -n "$gw" ]; then ip -n "$ns" route replace default via "$gw"; fi\n`,VXLAN_SETUP='\nset -e\nbr="$1"; vx="$2"; vni="$3"; port="$4"; local="$5"; mtu="$6"; shift 6\nif ! ip link show "$vx" >/dev/null 2>&1; then\n if [ -n "$local" ]; then ip link add "$vx" type vxlan id "$vni" dstport "$port" local "$local"; else ip link add "$vx" type vxlan id "$vni" dstport "$port"; fi\nfi\nip link set "$vx" mtu "$mtu"\nip link set "$br" mtu "$mtu"\nip link set "$vx" master "$br"\nip link set "$vx" up\nfor peer; do\n bridge fdb show dev "$vx" | grep -q "dst $peer " || bridge fdb append 00:00:00:00:00:00 dev "$vx" dst "$peer"\ndone\niptables -C INPUT -p udp --dport "$port" -j ACCEPT 2>/dev/null || iptables -I INPUT -p udp --dport "$port" -j ACCEPT\n# the engines\' name announcements on the overlay itself (runtime/overlaydns.js)\niptables -C INPUT -i "$br" -p udp --dport 7946 -j ACCEPT 2>/dev/null || iptables -I INPUT -i "$br" -p udp --dport 7946 -j ACCEPT\n',ROUTE_SOURCES='\nfor peer; do\n src=$(ip -4 route get "$peer" 2>/dev/null | sed -n \'s/.* src \\([0-9.]*\\).*/\\1/p\' | head -n1)\n [ -n "$src" ] || { echo "no route to overlay peer $peer" >&2; exit 1; }\n echo "$peer $src"\ndone\n';function ipsecBatch(e,t,{remove:n=!1,existing:i=new Map}={}){const r=Buffer.from(e.ipsecKey,"hex"),s=(t,n)=>{const i=Buffer.from(crypto.hkdfSync("sha256",r,Buffer.from("tnc-overlay-ipsec"),Buffer.from(`${e.vni}|${t}>${n}`),40));return{spi:`0x${(2147483647&i.readUInt32BE(36)|4096).toString(16)}`,aead:`0x${i.subarray(0,36).toString("hex")}`}},a=e.vni,o=[];for(const[r,p]of t){const t=s(p,r),l=s(r,p),d=(t,n)=>`src ${t}/32 dst ${n}/32 proto udp dport ${e.port}`;if(n)o.push(`xfrm policy delete ${d(p,r)} dir out`,`xfrm policy delete ${d(r,p)} dir in`,`xfrm state delete src ${p} dst ${r} proto esp spi ${t.spi}`,`xfrm state delete src ${r} dst ${p} proto esp spi ${l.spi}`);else{for(const[e,n,s]of[[p,r,t],[r,p,l]]){const t=i.get(parseInt(s.spi,16));t!==s.aead&&(void 0!==t&&o.push(`xfrm state delete src ${e} dst ${n} proto esp spi ${s.spi}`),o.push(`xfrm state add src ${e} dst ${n} proto esp spi ${s.spi} reqid ${a} mode transport aead 'rfc4106(gcm(aes))' ${s.aead} 128`))}o.push(`xfrm policy update ${d(p,r)} dir out tmpl src ${p} dst ${r} proto esp reqid ${a} mode transport`,`xfrm policy update ${d(r,p)} dir in tmpl src ${r} dst ${p} proto esp reqid ${a} mode transport`)}}return o.join("\n")+"\n"}function parseXfrmStates(e){const t=new Map;for(const n of String(e).split(/^(?=src )/m)){const e=/proto esp spi 0x([0-9a-f]+)/.exec(n),i=/aead \S+ (0x[0-9a-f]+) \d+/.exec(n);e&&i&&t.set(parseInt(e[1],16),i[1])}return t}function overlayKey(e,t=e=>require("fs").readFileSync(e,"utf8")){let n=e.key;if(!n&&e["key-file"])try{n=t(e["key-file"]).trim()}catch(t){throw httpError(400,`key-file ${e["key-file"]}: ${t.message}`)}if(!n)throw httpError(400,"an encrypted overlay needs the same key on every machine: -o key=$(openssl rand -hex 32) or -o key-file=PATH");const i=String(n).trim(),r=/^[0-9a-f]{64}$/i.test(i)?Buffer.from(i,"hex"):Buffer.from(i,"base64");if(32!==r.length||!/^[0-9a-f]{64}$/i.test(i)&&r.toString("base64").replace(/=+$/,"")!==i.replace(/=+$/,""))throw httpError(400,"the overlay key must be 32 bytes: 64 hex characters or base64");return r.toString("hex")}const isOn=e=>void 0!==e&&!["0","false","no","off"].includes(String(e).toLowerCase()),NETNS_ONLY=`set -e; mkdir -p /var/run/netns; [ -e "/var/run/netns/$1" ] || { ip netns add "$1"; ip -n "$1" link set lo up; ${NETNS_DEFAULTS.replace(/\$NS/g,"$1")}; }`,DETACH_ONE='ip -n "$1" link del "$2" 2>/dev/null; ip link del "$3" 2>/dev/null; true',DETACH_ALL='ip netns del "$1" 2>/dev/null; true',ISOLATE_HOST='\nset -e\nns="$1"; shift\ngw=$(ip route show default 2>/dev/null | awk \'/^default/ {print $3; exit}\')\nipt() { ip netns exec "$ns" iptables "$@"; }\nfor ip in "$@" $gw; do\n [ -n "$ip" ] || continue\n ipt -C OUTPUT -d "$ip" -j DROP 2>/dev/null && continue\n ipt -A OUTPUT -d "$ip" -p udp -j ACCEPT\n ipt -A OUTPUT -d "$ip" -j DROP\ndone\n';class Network{constructor({host:e,store:t}){this.host=e,this.store=t,this.reserved=new Set,this.lastIp=new Map}builtins(){return[DEFAULT_BRIDGE,HOST,NONE]}resolve(e){if(!e||"default"===e||"bridge"===e)return DEFAULT_BRIDGE;if("host"===e)return HOST;if("none"===e)return NONE;const t=this.store.findNetwork(e);if(!t)throw httpError(404,`network ${e} not found`);return{...t,dns:!0,prefixLen:t.prefixLen||24}}list(){return[...this.builtins(),...this.store.listNetworks().map(e=>({...e,dns:!0,prefixLen:e.prefixLen||24}))]}create({Name:e,Driver:t,Labels:n,Internal:i=!1,Options:r,EnableIPv6:s=!1,IPAM:a}){if(t=t||"bridge",n=n||{},r=r||{},!e)throw httpError(400,"network name is required");if(!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(e))throw httpError(400,`invalid network name "${e}", only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed`);if(["bridge","host","none","default"].includes(e))throw httpError(403,`${e} is a pre-defined network and cannot be created`);if(this.store.findNetwork(e)?.name===e)throw httpError(409,`network with name ${e} already exists`);if("macvlan"===t||"ipvlan"===t)return this._createLan({Name:e,Driver:t,Labels:n,Internal:i,Options:r,IPAM:a});if("overlay"===t)return this._createOverlay({Name:e,Labels:n,Internal:i,Options:r,IPAM:a});if("bridge"!==t)throw httpError(400,`network driver "${t}" is not supported; use bridge, macvlan, ipvlan or overlay`);const o=new Set(this.store.listNetworks().map(e=>e.subnet));let p=null;for(let e=USER_POOL.first;e<=USER_POOL.last;e++){const t=`${USER_POOL.base}.${e}.0/24`;if(!o.has(t)){p=t;break}}if(!p)throw httpError(500,"no free subnets left in 172.29.0.0/16");const l=crypto.randomBytes(32).toString("hex"),d=p.split("/")[0].split(".").slice(0,3).join("."),h=s||(a?.Config||[]).some(e=>e.Subnet?.includes(":"))?ipv6Subnet(l,a):null;return this.store.insertNetwork({id:l,name:e,driver:"bridge",subnet:p,gateway:`${d}.1`,poolStart:`${d}.2`,poolEnd:`${d}.254`,...h?{subnet6:`${h}::/64`,gateway6:`${h}::1`,prefix6:h}:{},bridgeIf:`tncb-${l.slice(0,8)}`,chain:`TNC-ISO-${l.slice(0,8)}`,labels:n,internal:!!i,options:r})}_needLinuxRoot(e,t){if("local"!==this.host.kind)throw httpError(400,`${e} networks need dock on Linux as root: here containers run behind ${{vm:"the Linux VM's NAT",wsl:"WSL's NAT",rootless:"rootless user-mode networking"}[this.host.kind]||"NAT"}, so ${t}`)}_createLan({Name:e,Driver:t,Labels:n,Internal:i,Options:r,IPAM:s}){this._needLinuxRoot(t,"there is no LAN interface to put them on");const a=(s?.Config||[]).find(e=>e.Subnet&&!e.Subnet.includes(":"));if(!a)throw httpError(400,`a ${t} network needs the LAN's subnet and gateway, e.g. --subnet 192.168.1.0/24 --gateway 192.168.1.1`);const{netInt:o,plen:p,lo:l,hi:d}=ipv4Range(a),h=a.Gateway||intToIp(o+1),$="macvlan"===t?r.macvlan_mode||"bridge":r.ipvlan_mode||"l2";if(!("macvlan"===t?["bridge","private","vepa","passthru"]:["l2","l3","l3s"]).includes($))throw httpError(400,`invalid ${t} mode ${$}`);if(r.parent&&!/^[A-Za-z0-9_.@-]{1,15}$/.test(r.parent))throw httpError(400,`invalid parent interface ${r.parent}`);const c=crypto.randomBytes(32).toString("hex");return this.store.insertNetwork({id:c,name:e,driver:t,subnet:`${intToIp(o)}/${p}`,gateway:h,prefixLen:p,poolStart:intToIp(l),poolEnd:intToIp(d),parent:r.parent||"",mode:$,labels:n,internal:!!i,options:r})}_createOverlay({Name:e,Labels:t,Internal:n,Options:i,IPAM:r}){this._needLinuxRoot("overlay","other machines cannot reach its tunnel");const s=(r?.Config||[]).find(e=>e.Subnet&&!e.Subnet.includes(":"));if(!s)throw httpError(400,"an overlay network needs its subnet (the same on every machine), e.g. --subnet 10.20.0.0/16 --ip-range 10.20.1.0/24");const a=String(i.peers||"").split(",").map(e=>e.trim()).filter(Boolean);for(const e of a)if(!require("net").isIPv4(e))throw httpError(400,`peer ${e} is not an IPv4 address`);if(a.length&&!s.IPRange)throw httpError(400,"an overlay network across machines needs --ip-range: a different part of the subnet on each machine, so their containers never get the same address");const{netInt:o,plen:p,hi:l}=ipv4Range(s);let{lo:d}=ipv4Range(s);if(255&d||d++,d>=l)throw httpError(400,`ip range ${s.IPRange||s.Subnet} is too small`);const h=s.Gateway||intToIp(d),$=2**(32-p);for(const e of[DEFAULT_BRIDGE,...this.store.listNetworks()]){const[t,n]=String(e.subnet||"").split("/");if(!t||t.includes(":"))continue;const i=ipToInt(t);if(o<i+2**(32-Number(n))&&i<o+$)throw httpError(400,`subnet ${s.Subnet} overlaps network ${e.name} (${e.subnet})`)}const c=void 0!==i.vni?Number(i.vni):parseInt(crypto.createHash("sha256").update(e).digest("hex").slice(0,6),16)||1;if(!Number.isInteger(c)||c<1||c>16777215)throw httpError(400,"vni must be a number from 1 to 16777215");const u=Number(i.port||4789),f=isOn(i.encrypted),v=f?overlayKey(i):null,b=Number(i.mtu||(f?1400:1450));if(!Number.isInteger(u)||u<1||u>65535)throw httpError(400,"port must be a UDP port number");if(!Number.isInteger(b)||b<1280||b>9e3)throw httpError(400,"mtu must be from 1280 to 9000");if(i.local&&!require("net").isIPv4(i.local))throw httpError(400,`local ${i.local} is not an IPv4 address`);const m=crypto.randomBytes(32).toString("hex");return this.store.insertNetwork({id:m,name:e,driver:"overlay",subnet:`${intToIp(o)}/${p}`,gateway:h,prefixLen:p,poolStart:intToIp(s.Gateway?d:d+1),poolEnd:intToIp(l),bridgeIf:`tnco-${m.slice(0,8)}`,vxlanIf:`tncx-${m.slice(0,8)}`,chain:`TNC-ISO-${m.slice(0,8)}`,vni:c,port:u,mtu:b,peers:a,local:i.local||"",...f?{encrypted:!0,ipsecKey:v}:{},labels:t,internal:!!n,options:Object.fromEntries(Object.entries(i).filter(([e])=>"key"!==e))})}_allocate6(e,t){if(!e.prefix6)return null;const n=new Set(this.reserved);for(const e of this.store.listContainers())if(e.id!==t)for(const t of Object.values(e.runtime.networks||{}))t.ip6&&n.add(t.ip6);for(let t=2;t<65535;t++){const i=`${e.prefix6}::${t.toString(16)}`;if(!n.has(i))return this.reserved.add(i),i}throw httpError(500,`no free IPv6 addresses left in network ${e.name}`)}async remove(e){if(e.builtin)throw httpError(403,`${e.name} is a pre-defined network and cannot be removed`);if(("bridge"===e.driver||"overlay"===e.driver)&&await this.host.isUp().catch(()=>!1)){if(e.vxlanIf&&await this.host.sh('ip link del "$1" 2>/dev/null; true',[e.vxlanIf]),e.encrypted&&e.peers?.length){const t=await this._routeSources(e).catch(()=>[]);t.length&&await this.host.sh("ip -force -batch - 2>/dev/null; true",[],{input:ipsecBatch(e,t,{remove:!0})})}await this.host.sh(BRIDGE_REMOVE,[e.bridgeIf,e.subnet,e.chain,e.subnet6||""])}this.store.deleteNetwork(e.id)}_allocate(e,t){const n=new Set(this.reserved);e.gateway&&n.add(e.gateway);for(const e of this.store.listContainers())if(e.id!==t){for(const t of Object.values(e.runtime.networks||{}))t.ip&&n.add(t.ip);e.runtime.ip&&n.add(e.runtime.ip)}const i=ipToInt(e.poolStart),r=ipToInt(e.poolEnd)-i+1,s=this.lastIp.get(e.id),a=s?(ipToInt(s)-i+1)%r:0;for(let t=0;t<r;t++){const s=intToIp(i+(a+t)%r);if(!n.has(s))return this.reserved.add(s),this.lastIp.set(e.id,s),s}throw httpError(500,`no free addresses left in network ${e.name}`)}names(e,t){const n=/^[0-9a-f]{12}/.test(e)?e:crypto.createHash("sha256").update(e).digest("hex");return{ns:`tnc-${n.slice(0,12)}`,vh:`vt${n.slice(0,8)}${t}`,vp:`vp${n.slice(0,8)}${t}`}}async _ensureBridge(e){const t=e.builtin?"TNC-ISO-default":e.chain;if(await this.host.sh(BRIDGE_SETUP,[e.bridgeIf,e.gateway,e.prefixLen,e.subnet,t,e.internal?1:0,e.subnet6||"",e.gateway6||""]),"overlay"===e.driver&&(await this.host.sh(VXLAN_SETUP,[e.bridgeIf,e.vxlanIf,String(e.vni),String(e.port),e.local||"",String(e.mtu),...e.peers||[]]).catch(t=>{throw httpError(500,`overlay network ${e.name}: ${/vxlan|Unknown device type|not supported/i.test(t.message)?"this kernel has no VXLAN support (modprobe vxlan)":t.message}`)}),e.encrypted&&e.peers?.length)){const t=await this._routeSources(e),{stdout:n}=await this.host.sh("ip xfrm state list proto esp 2>/dev/null; true"),i=ipsecBatch(e,t,{existing:parseXfrmStates(n)});await this.host.sh("ip -batch -",[],{input:i}).catch(t=>{throw httpError(500,`overlay network ${e.name}: cannot set up its encryption: ${/rfc4106|aead|not supported|Protocol not supported/i.test(t.message)?"this kernel has no IPsec ESP with AES-GCM (modprobe esp4)":t.message}`)})}}async _routeSources(e){if(e.local)return e.peers.map(t=>[t,e.local]);const{stdout:t}=await this.host.sh(ROUTE_SOURCES,e.peers);return t.trim().split("\n").filter(Boolean).map(e=>e.split(" "))}async attachOne(e,t,n,i=[]){const{ns:r,vh:s,vp:a}=this.names(e,n);if("null"===t.driver)return await this.host.sh(NETNS_ONLY,[r]),{networkId:"none",ifname:null,aliases:i};if("macvlan"===t.driver||"ipvlan"===t.driver){const s=this._allocate(t,e),o="02:42:"+crypto.createHash("sha1").update(`${e}:${t.id}`).digest("hex").slice(0,8).match(/../g).join(":");try{await this.host.sh(LAN_ATTACH,[r,t.parent||"",t.driver,t.mode,a,`eth${n}`,s,t.prefixLen,t.gateway,0===n?1:0,o])}catch(e){throw this.reserved.delete(s),e}return{networkId:t.id,ifname:`eth${n}`,idx:n,ip:s,mac:"macvlan"===t.driver?o:"",gateway:t.gateway,prefixLen:t.prefixLen,aliases:i}}await this._ensureBridge(t);const o=this._allocate(t,e),p=this._allocate6(t,e),l="02:42:"+crypto.createHash("sha1").update(`${e}:${t.id}`).digest("hex").slice(0,8).match(/../g).join(":");try{await this.host.sh(ATTACH,[r,t.bridgeIf,s,a,`eth${n}`,o,t.prefixLen,l,t.gateway,0===n?1:0,p||"",t.gateway6||"","overlay"===t.driver?String(t.mtu):""])}catch(e){throw this.reserved.delete(o),p&&this.reserved.delete(p),e}return{networkId:t.id,ifname:`eth${n}`,idx:n,ip:o,mac:l,gateway:t.gateway,prefixLen:t.prefixLen,aliases:i,...p?{ip6:p,gateway6:t.gateway6,prefixLen6:64}:{}}}async attachAll(e,t){const n=t.map(e=>({net:this.resolve(e.name),aliases:e.aliases||[]}));if("host"===n[0].net.driver)return{nsPath:null,endpoints:{host:{networkId:"host"}}};const i={};try{for(let t=0;t<n.length;t++)i[n[t].net.name]=await this.attachOne(e,n[t].net,t,n[t].aliases)}catch(t){throw await this.detachAll(e,i),t}return{nsPath:`/var/run/netns/${this.names(e,0).ns}`,endpoints:i}}async isolateHost(e,t){const n=[...new Set(t.filter(e=>e&&/^\d+\.\d+\.\d+\.\d+$/.test(e)))];await this.host.sh(ISOLATE_HOST,[this.names(e,0).ns,...n])}async detachOne(e,t){if(!t||void 0===t.idx)return;const{ns:n,vh:i}=this.names(e,t.idx);await this.host.sh(DETACH_ONE,[n,t.ifname,i]).catch(()=>{}),t.ip&&this.reserved.delete(t.ip),t.ip6&&this.reserved.delete(t.ip6)}async detachAll(e,t={}){if(!t.host){await this.host.sh(DETACH_ALL,[this.names(e,0).ns]).catch(()=>{});for(const e of Object.values(t))e.ip&&this.reserved.delete(e.ip),e.ip6&&this.reserved.delete(e.ip6)}}}module.exports={Network,DEFAULT_BRIDGE,ipToInt,ipsecBatch,overlayKey,parseXfrmStates};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),os=require("os"),path=require("path"),LABEL="tnc.sandbox",NAME_PREFIX="tnc-sb-",VALID_ID=/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,62}$/;function reinstallWhenChanged(t,e,n){const s=t.split(" ")[0],a=`$(cat ${t} 2>/dev/null | md5sum | cut -d' ' -f1)`;return`if [ -f ${s} ] && [ "$(cat ${e} 2>/dev/null)" != "${a}" ]; then ${n} && echo "${a}" > ${e}; fi`}function detectRuntime(t,{nodeVersion:e}={}){const n=e=>fs.existsSync(path.join(t,e));let s=null;try{const e=fs.readFileSync(path.join(t,"thinkncollab.yml"),"utf8");s=(/^\s*primary:\s*["']?([a-z+]+)/m.exec(e)||[])[1]||null}catch(t){}switch(s||(s=n("package.json")?"node":n("requirements.txt")||n("pyproject.toml")||n("Pipfile")?"python":n("go.mod")?"go":n("Cargo.toml")?"rust":n("CMakeLists.txt")||n("Makefile")||n("main.cpp")||n("main.c")?"cpp":"node"),s){case"python":return{runtime:"python",image:"python:3.12-slim",deps:{target:"/opt/tnc-venv",env:["VIRTUAL_ENV=/opt/tnc-venv","PATH=/opt/tnc-venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},install:"[ -x /opt/tnc-venv/bin/python ] || python -m venv /opt/tnc-venv; "+reinstallWhenChanged("requirements.txt","/opt/tnc-venv/.tnc-installed","pip install -q -r requirements.txt")};case"go":return{runtime:"go",image:"golang:1.23-alpine",deps:{target:"/go/pkg/mod",env:[]},install:""};case"cpp":case"c++":case"c":return{runtime:"cpp",image:"gcc:14",deps:{target:"/work/build",env:[]},install:n("CMakeLists.txt")?"command -v cmake >/dev/null || (apt-get update -qq && apt-get install -y -qq cmake >/dev/null)":""};case"rust":return{runtime:"rust",image:"rust:1-slim",deps:{target:"/usr/local/cargo/registry",env:[]},install:""};default:{const t=String(e||"22").replace(/^v/,"").split(".")[0];return{runtime:"node",image:`node:${/^\d+$/.test(t)?t:"22"}-alpine`,deps:{target:"/work/node_modules",env:[]},install:reinstallWhenChanged("package.json package-lock.json","node_modules/.tnc-installed","(if [ -f package-lock.json ]; then npm ci; else npm install; fi)")}}}}function httpError(t,e){const n=new Error(e);return n.statusCode=t,n}class DockSandbox{constructor({api:t=null}={}){this._api=t,this.baseDir=path.join(os.homedir(),".tnc","sandboxes")}async api(){if(!this._api){const{DockApi:t}=require("./cli/api");this._api=new t}return await this._api.ensureRunning({quiet:!0}),this._api}async running(){const{dockPaths:t}=require("./paths");return require("./client").ping(this._api?.socket||t().socket)}name(t){return`tnc-sb-${t}`}async _inspect(t){const e=await this.api();try{return await e.json("GET",`/containers/${encodeURIComponent(this.name(t))}/json`)}catch(t){if(404===t.status)return null;throw t}}_meta(t){const e=t.Config.Labels||{},n=Object.entries(t.HostConfig.PortBindings||{})[0],s=t.State.Running&&!t.State.Paused,a=!s&&"no"!==e["tnc.sandbox.frozen"]&&t.State.StartedAt&&!t.State.StartedAt.startsWith("0001"),i=t.HostConfig.Memory||0;return{id:e[LABEL],name:e[LABEL],container:t.Name.replace(/^\//,""),containerId:t.Id,image:t.Config.Image,runtime:e["tnc.sandbox.runtime"]||null,state:s?"RUNNING":a?"FROZEN":"CREATED",pid:t.State.Pid||null,cmd:e["tnc.sandbox.cmd"]||"",cwd:e["tnc.sandbox.cwd"]||"",hostPort:n&&Number(n[1]?.[0]?.HostPort)||null,containerPort:n?Number(n[0].split("/")[0]):null,isolationLevel:"dock-container",memoryLimitMB:i?Math.round(i/1048576):null,roomId:e["tnc.room"]||null,roomName:e["tnc.room.name"]||null,userId:e["tnc.user"]||null,userName:e["tnc.user.name"]||null,health:s?"HEALTHY":"STOPPED",createdAt:t.Created,startedAt:s?t.State.StartedAt:null,frozenAt:s||!t.State.FinishedAt||t.State.FinishedAt.startsWith("0001")?null:t.State.FinishedAt,exitCode:s?null:t.State.ExitCode}}async start(t,e={}){try{return await this._start(t,e)}catch(t){return{success:!1,message:t.message}}}async _start(t,e){if(!VALID_ID.test(String(t)))return{success:!1,message:`invalid sandbox name "${t}" (letters, digits, _ . -)`};const n=await this.api(),s=await this._inspect(t),a=e.cwd?path.resolve(e.cwd):null;if(s&&a&&s.Config.Labels?.["tnc.sandbox.cwd"]&&s.Config.Labels["tnc.sandbox.cwd"]!==a)return{success:!1,message:`a sandbox "${t}" already runs another folder (${s.Config.Labels["tnc.sandbox.cwd"]}); pick another name with --name, or remove it: thinknsh sandbox destroy ${t}`};if(s)return s.State.Running?{success:!1,message:`Sandbox "${t}" is already running.`,meta:this._meta(s)}:(await n.json("POST",`/containers/${s.Id}/start`),{success:!0,message:`Sandbox "${t}" resumed.`,meta:this._meta(await this._inspect(t))});const i=path.resolve(e.cwd||process.cwd());if(i===path.parse(i).root||i===os.homedir())return{success:!1,message:`refusing to run ${i} as a sandbox (the whole folder is shared with the container); cd into a project folder`};const o=detectRuntime(i,e),r=e.image||o.image,c=e.cmd||"npm start",u=Number(e.containerPort||e.hostPort)||null,d=Number(e.hostPort||u)||null,m=`${this.name(t)}-deps`;await this._ensureImage(r);const l=[...Object.entries(e.env||{}).map(([t,e])=>`${t}=${e}`),...o.deps.env,"HOSTNAME=0.0.0.0",...u?[`PORT=${u}`]:[],...e.production?["NODE_ENV=production"]:[]],p={Image:r,Cmd:["sh","-c",[o.install,c].filter(Boolean).join(" && ")],WorkingDir:"/work",Env:l,Labels:{[LABEL]:String(t),"tnc.sandbox.cmd":c,"tnc.sandbox.cwd":i,"tnc.sandbox.runtime":o.runtime,...e.roomId?{"tnc.room":String(e.roomId)}:{},...e.roomName?{"tnc.room.name":String(e.roomName)}:{},...e.userId?{"tnc.user":String(e.userId)}:{},...e.userName?{"tnc.user.name":String(e.userName)}:{}},...u?{ExposedPorts:{[`${u}/tcp`]:{}}}:{},HostConfig:{Binds:[`${i}:/work`],Mounts:[{Type:"volume",Source:m,Target:o.deps.target}],...u?{PortBindings:{[`${u}/tcp`]:[{HostIp:"127.0.0.1",HostPort:String(d)}]}}:{},...e.memoryLimit?{Memory:parseMemory(e.memoryLimit)}:{},...e.restart?{RestartPolicy:{Name:e.restart}}:{},Init:!0}},h=await n.json("POST",`/containers/create?name=${encodeURIComponent(this.name(t))}`,p);try{await n.json("POST",`/containers/${h.Id}/start`)}catch(t){throw await n.json("DELETE",`/containers/${h.Id}?force=1`).catch(()=>{}),t}return{success:!0,message:`Sandbox "${t}" started in dock (${r}).`,meta:this._meta(await this._inspect(t))}}async _ensureImage(t){const e=await this.api();try{return void await e.json("GET",`/images/${encodeURIComponent(t)}/json`)}catch(t){}const[n,s]=t.split(/:(?=[^/:]+$)/);await e.progress("POST",`/images/create?fromImage=${encodeURIComponent(n)}&tag=${encodeURIComponent(s||"latest")}`,null,()=>{})}async freeze(t){const e=await this._inspect(t);if(!e)return{success:!1,message:`Sandbox "${t}" not found.`};if(!e.State.Running)return{success:!0,message:`Sandbox "${t}" is already frozen.`,meta:this._meta(e)};const n=await this.api();return await n.json("POST",`/containers/${e.Id}/stop?t=10`),{success:!0,message:`Sandbox "${t}" frozen (0 RAM; files and dependencies kept).`,meta:this._meta(await this._inspect(t))}}async stop(t){return this.freeze(t)}async resume(t){const e=await this._inspect(t);if(!e)return{success:!1,message:`Sandbox "${t}" not found.`};if(e.State.Running)return{success:!0,message:`Sandbox "${t}" is already running.`,meta:this._meta(e)};const n=await this.api();return await n.json("POST",`/containers/${e.Id}/start`),{success:!0,message:`Sandbox "${t}" resumed.`,meta:this._meta(await this._inspect(t))}}async destroy(t){const e=await this._inspect(t),n=await this.api();return e&&await n.json("DELETE",`/containers/${e.Id}?force=1`),await n.json("DELETE",`/volumes/${encodeURIComponent(`${this.name(t)}-deps`)}?force=1`).catch(()=>{}),{success:!!e,message:e?`Sandbox "${t}" destroyed.`:`Sandbox "${t}" not found.`}}async kill(t,e="SIGKILL"){const n=await this._inspect(t);if(!n)return{success:!1,message:`Sandbox "${t}" not found.`};const s=await this.api();return await s.json("POST",`/containers/${n.Id}/kill?signal=${encodeURIComponent(e)}`),{success:!0,message:`Sandbox "${t}" sent ${e}.`}}async list({start:t=!0}={}){if(!t&&!await this.running())return[];const e=await this.api(),n=await e.json("GET",`/containers/json?all=1&filters=${encodeURIComponent(JSON.stringify({label:[LABEL]}))}`),s=[];for(const t of n){const n=await e.json("GET",`/containers/${t.Id}/json`).catch(()=>null);n&&s.push(this._meta(n))}return s.sort((t,e)=>String(t.id).localeCompare(String(e.id)))}async loadMeta(t){const e=await this._inspect(t);return e?this._meta(e):null}async inspect(t){return this.loadMeta(t)}async logs(t,{tail:e=200}={}){const n=await this._inspect(t);if(!n)throw httpError(404,`Sandbox "${t}" not found.`);const s=await this.api(),a=await s.stream("GET",`/containers/${n.Id}/logs?stdout=1&stderr=1&tail=${e}`),{demuxer:i}=require("./cli/api");let o="";const r=n.Config.Tty?t=>{o+=t}:i(t=>{o+=t},t=>{o+=t});for await(const t of a)r(t);return o}async exec(t,e=["/bin/sh"],{tty:n=process.stdin.isTTY}={}){const{runCommand:s}=require("./cli/commands"),a=await s("exec",[...n?["-it"]:["-i"],this.name(t),...e]);return{success:0===a,code:a}}async prune(){const t=[];for(const e of await this.list())"RUNNING"!==e.state&&(await this.destroy(e.id),t.push(e.id));return{success:!0,removed:t}}startTunnelDaemon(t,e,n=null,s=null){return require("../core/tunnel").startDaemon(t,e,n,s)}stopTunnelDaemon(t){return require("../core/tunnel").stopDaemon(t)}}function parseMemory(t){const e=/^(\d+(?:\.\d+)?)\s*([kmg]?)b?$/i.exec(String(t).trim());if(!e)return 0;const n={"":1,k:1024,m:1048576,g:1024**3}[e[2].toLowerCase()];return Math.round(Number(e[1])*n)}module.exports={DockSandbox,detectRuntime,parseMemory,LABEL,NAME_PREFIX:"tnc-sb-"};
@@ -4,4 +4,4 @@
4
4
  * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
5
  * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
6
  */
7
- "use strict";const fs=require("fs"),os=require("os"),path=require("path"),crypto=require("crypto"),{spawnSync}=require("child_process"),TNC_DIR=path.join(os.homedir(),".tnc"),BUNDLE_VERSION="0.2.3",DEFAULT_BASE_URL=process.env.TNC_DOCK_BUNDLE_URL||"https://thinkncollab.com/downloads/dock/0.2.3",WSL_DISTRO="tnc-dock",linuxArch=(n=process.arch)=>({arm64:"aarch64",x64:"x86_64"}[n]);function setupError(n){const t=new Error(n);return t.setup=!0,t}function sha256File(n){const t=crypto.createHash("sha256"),e=fs.openSync(n,"r"),s=Buffer.alloc(1<<20);let r;for(;(r=fs.readSync(e,s,0,s.length,null))>0;)t.update(s.subarray(0,r));return fs.closeSync(e),t.digest("hex")}const OPTIONAL_FILES=/^tnc-busybox-linux-|^tnc-tray-/;function wantedFiles(n=process.platform,t=process.arch){const e=linuxArch(t);if(!e)throw setupError(`unsupported CPU architecture ${t}`);if("darwin"===n)return[`tnc-dock-vm-${e}.tar.gz`,"tnc-vz-darwin-universal","tnc-tray-darwin-universal.tar.gz"];if("win32"===n)return[`tnc-dock-wsl-${e}.tar.gz`];if("linux"===n)return[`tnc-shim-linux-${e}`,`crun-linux-${e}`,`tnc-busybox-linux-${e}`];throw setupError(`unsupported platform ${n}`)}async function download(n,t){const e=await fetch(n,{redirect:"follow"});if(!e.ok)throw setupError(`download failed: ${n} (HTTP ${e.status})`);fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.writeFileSync(`${t}.part`,Buffer.from(await e.arrayBuffer())),fs.renameSync(`${t}.part`,t)}async function fetchBundle({from:n,baseUrl:t=DEFAULT_BASE_URL,log:e}){let s,r;if(n)r=path.resolve(n),s=JSON.parse(fs.readFileSync(path.join(r,"manifest.json"),"utf8"));else{let n=await fetch(`${t}/manifest.json`,{redirect:"follow"});if(!n.ok){const e="https://thinkncollab.com/downloads/dock",r=await fetch(`${e}/manifest.json`,{redirect:"follow"});r.ok&&(s=await r.json(),t=`${e}/${s.version}`,n=r)}if(!n.ok)throw setupError(`cannot fetch bundle manifest from ${t} (HTTP ${n.status})`);s||(s=await n.json()),r=path.join(TNC_DIR,"cache","bundles",s.version)}const i={};for(const o of wantedFiles()){const a=s.files[o];if(!a&&OPTIONAL_FILES.test(o)){e(`bundle ${s.version} has no ${o}; ${/tray/.test(o)?"the menu-bar app":"cp"} needs a newer bundle`);continue}if(!a)throw setupError(`bundle ${s.version} has no ${o}`);const c=path.join(r,o);if(n||fs.existsSync(c)&&sha256File(c)===a.sha256||(e(`downloading ${o} (${(a.size/1024/1024).toFixed(1)} MB)`),await download(`${t}/${o}`,c)),!fs.existsSync(c))throw setupError(`missing ${c}`);if(sha256File(c)!==a.sha256)throw setupError(`sha256 mismatch for ${o}; refusing to install`);i[o]=c}return{manifest:s,files:i}}function run(n,t,e={}){const s=spawnSync(n,t,{encoding:"utf8",...e});return{code:s.status,out:`${s.stdout||""}${s.stderr||""}`.replace(/\0/g,"")}}function installDarwin(n,t){const e=linuxArch(),s=path.join(TNC_DIR,"kernel"),r=path.join(TNC_DIR,"bin");fs.mkdirSync(s,{recursive:!0}),fs.mkdirSync(r,{recursive:!0}),run(path.join(r,"tnc-vz"),["stop"]);for(const n of["vmlinuz","initramfs.img"]){const t=path.join(s,n);fs.existsSync(t)&&!fs.existsSync(`${t}.pre-bundle`)&&fs.copyFileSync(t,`${t}.pre-bundle`)}const i=run("tar",["-xzf",n[`tnc-dock-vm-${e}.tar.gz`],"-C",s]);if(0!==i.code)throw setupError(`cannot unpack VM image: ${i.out}`);const o=path.join(r,"tnc-vz");fs.existsSync(o)&&!fs.existsSync(`${o}.pre-bundle`)&&fs.copyFileSync(o,`${o}.pre-bundle`),fs.copyFileSync(n["tnc-vz-darwin-universal"],`${o}.new`),fs.chmodSync(`${o}.new`,493),fs.renameSync(`${o}.new`,o),t(`MicroVM image (${e}) and tnc-vz installed`);const a=n["tnc-tray-darwin-universal.tar.gz"];if(a){fs.rmSync(path.join(r,"Thinknsh.app"),{recursive:!0,force:!0});const n=run("tar",["-xzf",a,"-C",r]);if(0!==n.code)throw setupError(`cannot unpack the menu-bar app: ${n.out}`);t("menu-bar app unpacked (install it: thinknsh dock app install)")}}function wslDistros(){const n=run("wsl.exe",["--list","--quiet"]);return 0===n.code?n.out.split(/\r?\n/).map(n=>n.trim()).filter(Boolean):null}function sanitizeWslTar(n){const t=require("zlib");let e;try{e=t.gunzipSync(fs.readFileSync(n))}catch(t){return{path:n,cleanup:()=>{}}}let s=0;const r=[];let i=!1;for(;s<e.length;){const n=e.subarray(s,s+512);if(n.length<512||n.every(n=>0===n)){r.push(e.subarray(s));break}const t=n.subarray(124,136).toString("latin1").replace(/\0.*$/,"").trim(),o=parseInt(t,8)||0,a=String.fromCharCode(n[156]),c=512+512*Math.ceil(o/512);"x"===a||"g"===a?i=!0:r.push(e.subarray(s,s+c)),s+=c}if(!i)return{path:n,cleanup:()=>{}};const o=path.join(os.tmpdir(),`tnc-dock-wsl-clean-${Date.now()}.tar.gz`);return fs.writeFileSync(o,t.gzipSync(Buffer.concat(r))),{path:o,cleanup:()=>{try{fs.unlinkSync(o)}catch(n){}}}}function wslMissingMessage(n=""){return["Linux containers on Windows run in WSL2, and WSL2 is not ready on this PC yet.","","Set it up once (about 5 minutes):",' 1. Open PowerShell as administrator: Start, type "PowerShell", right-click it, "Run as administrator"'," 2. Run: wsl --install --no-distribution"," 3. Restart Windows"," 4. Run: thinknsh dock setup",.../BIOS|UEFI|virtuali[sz]ation (is )?(not enabled|disabled)|enable virtuali[sz]ation/i.test(n)?["","This PC also has virtualization switched off. Turn it on in the BIOS/UEFI settings",'(often called "Intel VT-x", "AMD-V" or "SVM"), then do the steps above.']:[],"","Needs Windows 10 version 2004 or newer, or Windows 11."].join("\n")}function installWindows(n,t,{force:e=!1}={}){const s=linuxArch(),r=run("wsl.exe",["--status"]);if(0!==r.code)throw setupError(wslMissingMessage(r.out));const i=wslDistros()||[],o=n[`tnc-dock-wsl-${s}.tar.gz`],{path:a,cleanup:c}=sanitizeWslTar(o);try{if(i.includes("tnc-dock")&&!e){const n=run("wsl.exe",["-d","tnc-dock","-u","root","--","tar","-xzf","-","-C","/","./usr/bin/tnc-shim","./usr/bin/crun","./usr/local/bin/tnc-wsl-init","./etc/wsl.conf","./bin/busybox.static"],{input:fs.readFileSync(a)});if(0!==n.code)throw setupError(`cannot upgrade WSL distro: ${n.out}`);return run("wsl.exe",["--terminate","tnc-dock"]),void t("WSL distro tnc-dock upgraded in place (layers and volumes kept)")}i.includes("tnc-dock")&&run("wsl.exe",["--unregister","tnc-dock"]);const n=path.join(TNC_DIR,"wsl","tnc-dock");fs.mkdirSync(n,{recursive:!0});const s=run("wsl.exe",["--import","tnc-dock",n,a,"--version","2"]);if(0!==s.code)throw setupError(`wsl --import failed: ${s.out.trim()}`);t(`WSL2 distro tnc-dock created in ${n}`)}finally{c()}}function installLinux(n,t){const e=linuxArch(),s=path.join(TNC_DIR,"bin");fs.mkdirSync(s,{recursive:!0});for(const t of[`tnc-shim-linux-${e}`,`crun-linux-${e}`,`tnc-busybox-linux-${e}`]){const e=t;n[t]&&(fs.copyFileSync(n[t],path.join(s,`${e}.new`)),fs.chmodSync(path.join(s,`${e}.new`),493),fs.renameSync(path.join(s,`${e}.new`),path.join(s,e)))}if(t(`tnc-shim, crun and busybox installed in ${s}`),0!==process.getuid?.()){const n=require("../linux/host").rootlessProblems();t(n.length?`rootless mode needs:\n - ${n.join("\n - ")}`:"rootless mode is ready (dock runs as you, no sudo)")}}async function prefetchBuildKit(n){if("0"===process.env.TNC_DOCK_BUILDKIT)return;const{installBuildKit:t,buildKitDir:e,buildKitInstalled:s,VERSION:r}=require("../build/buildkit"),i={arm64:"arm64",x64:"amd64"}[process.arch];if(!i)return;const o=e(process.env.TNC_DOCK_HOME||path.join(TNC_DIR,"dock"),i);if(s(o))return n(`BuildKit ${r} already installed`);try{n(`downloading BuildKit ${r} (linux/${i})`),await t(o,i),n(`BuildKit ${r} installed in ${o}`)}catch(t){n(`warning: ${t.message}; it will be downloaded on the first BuildKit build`)}}async function setup({from:n,baseUrl:t,force:e=!1,log:s=()=>{}}={}){const{manifest:r,files:i}=await fetchBundle({from:n,baseUrl:t,log:s});return"darwin"===process.platform?installDarwin(i,s):"win32"===process.platform?installWindows(i,s,{force:e}):installLinux(i,s),await prefetchBuildKit(s),fs.mkdirSync(path.join(TNC_DIR,"dock"),{recursive:!0}),fs.writeFileSync(path.join(TNC_DIR,"dock","runtime.json"),JSON.stringify({version:r.version,platform:process.platform,arch:process.arch,installedAt:(new Date).toISOString()},null,2)),r.version}module.exports={wslMissingMessage,setup,prefetchBuildKit,wantedFiles,fetchBundle,linuxArch,WSL_DISTRO:"tnc-dock",sha256File,BUNDLE_VERSION:"0.2.3",sanitizeWslTar};
7
+ "use strict";const fs=require("fs"),os=require("os"),path=require("path"),crypto=require("crypto"),{spawnSync}=require("child_process"),TNC_DIR=path.join(os.homedir(),".tnc"),BUNDLE_VERSION="0.2.6",DEFAULT_BASE_URL=process.env.TNC_DOCK_BUNDLE_URL||"https://thinkncollab.com/downloads/dock/0.2.6",WSL_DISTRO="tnc-dock",linuxArch=(n=process.arch)=>({arm64:"aarch64",x64:"x86_64"}[n]);function setupError(n){const t=new Error(n);return t.setup=!0,t}function sha256File(n){const t=crypto.createHash("sha256"),e=fs.openSync(n,"r"),s=Buffer.alloc(1<<20);let r;for(;(r=fs.readSync(e,s,0,s.length,null))>0;)t.update(s.subarray(0,r));return fs.closeSync(e),t.digest("hex")}const OPTIONAL_FILES=/^tnc-busybox-linux-|^tnc-tray-/;function wantedFiles(n=process.platform,t=process.arch){const e=linuxArch(t);if(!e)throw setupError(`unsupported CPU architecture ${t}`);if("darwin"===n)return[`tnc-dock-vm-${e}.tar.gz`,"tnc-vz-darwin-universal","tnc-tray-darwin-universal.tar.gz"];if("win32"===n)return[`tnc-dock-wsl-${e}.tar.gz`];if("linux"===n)return[`tnc-shim-linux-${e}`,`crun-linux-${e}`,`tnc-busybox-linux-${e}`];throw setupError(`unsupported platform ${n}`)}async function download(n,t){const e=await fetch(n,{redirect:"follow"});if(!e.ok)throw setupError(`download failed: ${n} (HTTP ${e.status})`);fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.writeFileSync(`${t}.part`,Buffer.from(await e.arrayBuffer())),fs.renameSync(`${t}.part`,t)}async function fetchBundle({from:n,baseUrl:t=DEFAULT_BASE_URL,log:e}){let s,r;if(n)r=path.resolve(n),s=JSON.parse(fs.readFileSync(path.join(r,"manifest.json"),"utf8"));else{let n=await fetch(`${t}/manifest.json`,{redirect:"follow"});if(!n.ok){const e="https://thinkncollab.com/downloads/dock",r=await fetch(`${e}/manifest.json`,{redirect:"follow"});r.ok&&(s=await r.json(),t=`${e}/${s.version}`,n=r)}if(!n.ok)throw setupError(`cannot fetch bundle manifest from ${t} (HTTP ${n.status})`);s||(s=await n.json()),r=path.join(TNC_DIR,"cache","bundles",s.version)}const i={};for(const o of wantedFiles()){const a=s.files[o];if(!a&&OPTIONAL_FILES.test(o)){e(`bundle ${s.version} has no ${o}; ${/tray/.test(o)?"the menu-bar app":"cp"} needs a newer bundle`);continue}if(!a)throw setupError(`bundle ${s.version} has no ${o}`);const c=path.join(r,o);if(n||fs.existsSync(c)&&sha256File(c)===a.sha256||(e(`downloading ${o} (${(a.size/1024/1024).toFixed(1)} MB)`),await download(`${t}/${o}`,c)),!fs.existsSync(c))throw setupError(`missing ${c}`);if(sha256File(c)!==a.sha256)throw setupError(`sha256 mismatch for ${o}; refusing to install`);i[o]=c}return{manifest:s,files:i}}function run(n,t,e={}){const s=spawnSync(n,t,{encoding:"utf8",...e});return{code:s.status,out:`${s.stdout||""}${s.stderr||""}`.replace(/\0/g,"")}}function installDarwin(n,t){const e=linuxArch(),s=path.join(TNC_DIR,"kernel"),r=path.join(TNC_DIR,"bin");fs.mkdirSync(s,{recursive:!0}),fs.mkdirSync(r,{recursive:!0}),run(path.join(r,"tnc-vz"),["stop"]);for(const n of["vmlinuz","initramfs.img"]){const t=path.join(s,n);fs.existsSync(t)&&!fs.existsSync(`${t}.pre-bundle`)&&fs.copyFileSync(t,`${t}.pre-bundle`)}const i=run("tar",["-xzf",n[`tnc-dock-vm-${e}.tar.gz`],"-C",s]);if(0!==i.code)throw setupError(`cannot unpack VM image: ${i.out}`);const o=path.join(r,"tnc-vz");fs.existsSync(o)&&!fs.existsSync(`${o}.pre-bundle`)&&fs.copyFileSync(o,`${o}.pre-bundle`),fs.copyFileSync(n["tnc-vz-darwin-universal"],`${o}.new`),fs.chmodSync(`${o}.new`,493),fs.renameSync(`${o}.new`,o),t(`MicroVM image (${e}) and tnc-vz installed`);const a=n["tnc-tray-darwin-universal.tar.gz"];if(a){fs.rmSync(path.join(r,"Thinknsh.app"),{recursive:!0,force:!0});const n=run("tar",["-xzf",a,"-C",r]);if(0!==n.code)throw setupError(`cannot unpack the menu-bar app: ${n.out}`);t("menu-bar app unpacked (install it: thinknsh dock app install)")}}function wslDistros(){const n=run("wsl.exe",["--list","--quiet"]);return 0===n.code?n.out.split(/\r?\n/).map(n=>n.trim()).filter(Boolean):null}function sanitizeWslTar(n){const t=require("zlib");let e;try{e=t.gunzipSync(fs.readFileSync(n))}catch(t){return{path:n,cleanup:()=>{}}}let s=0;const r=[];let i=!1;for(;s<e.length;){const n=e.subarray(s,s+512);if(n.length<512||n.every(n=>0===n)){r.push(e.subarray(s));break}const t=n.subarray(124,136).toString("latin1").replace(/\0.*$/,"").trim(),o=parseInt(t,8)||0,a=String.fromCharCode(n[156]),c=512+512*Math.ceil(o/512);"x"===a||"g"===a?i=!0:r.push(e.subarray(s,s+c)),s+=c}if(!i)return{path:n,cleanup:()=>{}};const o=path.join(os.tmpdir(),`tnc-dock-wsl-clean-${Date.now()}.tar.gz`);return fs.writeFileSync(o,t.gzipSync(Buffer.concat(r))),{path:o,cleanup:()=>{try{fs.unlinkSync(o)}catch(n){}}}}function wslMissingMessage(n=""){return["Linux containers on Windows run in WSL2, and WSL2 is not ready on this PC yet.","","Set it up once (about 5 minutes):",' 1. Open PowerShell as administrator: Start, type "PowerShell", right-click it, "Run as administrator"'," 2. Run: wsl --install --no-distribution"," 3. Restart Windows"," 4. Run: thinknsh dock setup",.../BIOS|UEFI|virtuali[sz]ation (is )?(not enabled|disabled)|enable virtuali[sz]ation/i.test(n)?["","This PC also has virtualization switched off. Turn it on in the BIOS/UEFI settings",'(often called "Intel VT-x", "AMD-V" or "SVM"), then do the steps above.']:[],"","Needs Windows 10 version 2004 or newer, or Windows 11."].join("\n")}function installWindows(n,t,{force:e=!1}={}){const s=linuxArch(),r=run("wsl.exe",["--status"]);if(0!==r.code)throw setupError(wslMissingMessage(r.out));const i=wslDistros()||[],o=n[`tnc-dock-wsl-${s}.tar.gz`],{path:a,cleanup:c}=sanitizeWslTar(o);try{if(i.includes("tnc-dock")&&!e){const n=run("wsl.exe",["-d","tnc-dock","-u","root","--","tar","-xzf","-","-C","/","./usr/bin/tnc-shim","./usr/bin/crun","./usr/local/bin/tnc-wsl-init","./etc/wsl.conf","./bin/busybox.static"],{input:fs.readFileSync(a)});if(0!==n.code)throw setupError(`cannot upgrade WSL distro: ${n.out}`);return run("wsl.exe",["--terminate","tnc-dock"]),void t("WSL distro tnc-dock upgraded in place (layers and volumes kept)")}i.includes("tnc-dock")&&run("wsl.exe",["--unregister","tnc-dock"]);const n=path.join(TNC_DIR,"wsl","tnc-dock");fs.mkdirSync(n,{recursive:!0});const s=run("wsl.exe",["--import","tnc-dock",n,a,"--version","2"]);if(0!==s.code)throw setupError(`wsl --import failed: ${s.out.trim()}`);t(`WSL2 distro tnc-dock created in ${n}`)}finally{c()}}function installLinux(n,t){const e=linuxArch(),s=path.join(TNC_DIR,"bin");fs.mkdirSync(s,{recursive:!0});for(const t of[`tnc-shim-linux-${e}`,`crun-linux-${e}`,`tnc-busybox-linux-${e}`]){const e=t;n[t]&&(fs.copyFileSync(n[t],path.join(s,`${e}.new`)),fs.chmodSync(path.join(s,`${e}.new`),493),fs.renameSync(path.join(s,`${e}.new`),path.join(s,e)))}if(t(`tnc-shim, crun and busybox installed in ${s}`),0!==process.getuid?.()){const n=require("../linux/host").rootlessProblems();t(n.length?`rootless mode needs:\n - ${n.join("\n - ")}`:"rootless mode is ready (dock runs as you, no sudo)")}}async function prefetchBuildKit(n){if("0"===process.env.TNC_DOCK_BUILDKIT)return;const{installBuildKit:t,buildKitDir:e,buildKitInstalled:s,VERSION:r}=require("../build/buildkit"),i={arm64:"arm64",x64:"amd64"}[process.arch];if(!i)return;const o=e(process.env.TNC_DOCK_HOME||path.join(TNC_DIR,"dock"),i);if(s(o))return n(`BuildKit ${r} already installed`);try{n(`downloading BuildKit ${r} (linux/${i})`),await t(o,i),n(`BuildKit ${r} installed in ${o}`)}catch(t){n(`warning: ${t.message}; it will be downloaded on the first BuildKit build`)}}async function setup({from:n,baseUrl:t,force:e=!1,log:s=()=>{}}={}){const{manifest:r,files:i}=await fetchBundle({from:n,baseUrl:t,log:s});return"darwin"===process.platform?installDarwin(i,s):"win32"===process.platform?installWindows(i,s,{force:e}):installLinux(i,s),await prefetchBuildKit(s),fs.mkdirSync(path.join(TNC_DIR,"dock"),{recursive:!0}),fs.writeFileSync(path.join(TNC_DIR,"dock","runtime.json"),JSON.stringify({version:r.version,platform:process.platform,arch:process.arch,installedAt:(new Date).toISOString()},null,2)),r.version}module.exports={wslMissingMessage,setup,prefetchBuildKit,wantedFiles,fetchBundle,linuxArch,WSL_DISTRO:"tnc-dock",sha256File,BUNDLE_VERSION:"0.2.6",sanitizeWslTar};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),os=require("os"),net=require("net"),path=require("path"),crypto=require("crypto"),{spawnSync}=require("child_process"),ws=require("./workspace"),{connectPlatform,request}=require("../../core/platform-socket"),MAX_FRAME=262144,SHARE_ID=/^[a-z0-9]{10}$/,PUBKEY=/^ssh-ed25519 [A-Za-z0-9+/]+={0,2}( [^\n]{0,100})?$/,HOSTKEY=/^ssh-ed25519 [A-Za-z0-9+/]+={0,2}$/,MAX_BAD_PROOFS=5,MAX_STREAMS=32,clean=(e,t=64)=>String(e??"").replace(/[\u0000-\u001f\u007f-\u009f]/g,"").slice(0,t),safeFolder=e=>!/^\/work(\/[\w.-]+)*$/.test(e)&&!/^\/run\/tnc-workspace\/[\w.-]+\.code-workspace$/.test(e)||e.includes("..")?"/work":e,isBytes=e=>Buffer.isBuffer(e)||e instanceof ArrayBuffer||ArrayBuffer.isView(e),joinHost=e=>`tnc-join-${e}`,guestContainer=e=>`${ws.containerName(e)}-guest`,guestHome=e=>`${ws.containerName(e)}-guest-home`,guestNet=e=>`${ws.containerName(e)}-guest-net`,GUEST_LABEL=`${ws.LABEL}.guest`;function newCode(){return{shareId:Array.from(crypto.randomBytes(10),e=>"abcdefghijkmnpqrstuvwxyz23456789"[e%32]).join(""),secret:crypto.randomBytes(12).toString("base64url")}}function fingerprint(e){const t=Buffer.from(String(e).split(" ")[1]||"","base64");return crypto.createHash("sha256").update(t).digest("base64url").slice(0,16)}const keyOnly=e=>String(e).trim().split(" ").slice(0,2).join(" "),proofFor=(e,t)=>crypto.createHmac("sha256",e).update(keyOnly(t)).digest("hex");function parseCode(e){const t=/^([a-z0-9]{10})-([A-Za-z0-9_-]{16})-([A-Za-z0-9_-]{16})$/.exec(String(e||"").trim());if(!t)throw ws.workspaceError("that is not a join code (it looks like abcd2345ef-XXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXX)");return{shareId:t[1],secret:t[2],fp:t[3]}}function isPrivate(e){const t=String(e).replace(/^::ffff:/,""),[r,o]=t.split(".").map(Number);return 10===r||172===r&&o>=16&&o<=31||192===r&&168===o||100===r&&o>=64&&o<=127}function lanAddresses(){return Object.values(os.networkInterfaces()).flat().filter(e=>e&&"IPv4"===e.family&&!e.internal&&isPrivate(e.address)).map(e=>e.address)}function guestDir(e,t){return path.join(ws.stateDir(e,t),"guest")}function guestKeys(e,t){const r=guestDir(e,t);fs.mkdirSync(r,{recursive:!0,mode:448});const o=path.join(r,"ssh_host_ed25519_key");fs.existsSync(o)||(ws.keygen(o,`tnc-workspace-guest-${t}`),fs.rmSync(`${o}.pub`,{force:!0})),fs.writeFileSync(path.join(r,"authorized_keys"),"",{mode:384});const s=spawnSync("ssh-keygen",["-y","-f",o],{encoding:"utf8"});if(0!==s.status)throw ws.workspaceError(`cannot read the guest host key (${(s.stderr||"").trim()})`);return{dir:r,hostKey:keyOnly(s.stdout)}}function guestKeyLine(e,t,r){const o=String(r||"guest").replace(/[^\w.@-]/g,"_").slice(0,40);return`no-agent-forwarding,no-X11-forwarding ${keyOnly(t)} tnc-share:${e}:${o}`}function addGuestKey(e,t,r,o){fs.appendFileSync(path.join(e,"authorized_keys"),guestKeyLine(t,r,o)+"\n",{mode:384})}async function execRoot(e,t,r){const{execIn:o}=require("../room-data");return o(e,t,r,{user:"root"})}const sleep=e=>new Promise(t=>setTimeout(t,e));async function waitPort(e,t=3e4){const r=Date.now()+t;for(;Date.now()<r;){if(await new Promise(t=>{const r=net.connect({host:"127.0.0.1",port:e},()=>{r.once("data",e=>{r.destroy(),t(e.toString().startsWith("SSH-"))})});r.on("error",()=>t(!1)),r.setTimeout(1500,()=>{r.destroy(),t(!1)})}))return;await sleep(500)}throw ws.workspaceError("the guest container did not start its SSH server")}async function startGuestContainer(e,t,r,o,s){const n=guestContainer(r.name);await removeGuestContainer(e,r.name),await e.json("POST","/networks/create",{Name:guestNet(r.name),Labels:{[GUEST_LABEL]:r.name}});const a=path.join(ws.stateDir(t,r.name),"server");for(const e of fs.readdirSync(a).filter(e=>e.endsWith(".code-workspace")))fs.copyFileSync(path.join(a,e),path.join(s,e));const i=(o.HostConfig.Binds||[]).filter(e=>!/:\/run\/tnc-workspace(:|$)/.test(e)&&!e.startsWith(`${ws.homeVolume(r.name)}:`)),c=ws.listMeta(t).map(e=>e.port),d=await ws.pickPort(c),u=ws.containerBody({name:r.name,image:o.Config.Image,mounts:[],serverDir:s,port:d,memory:o.HostConfig.Memory,nanoCpus:o.HostConfig.NanoCpus,pidsLimit:o.HostConfig.PidsLimit||4096});u.Hostname=`tnc-${r.name}-guest`.slice(0,63),u.Labels={[GUEST_LABEL]:r.name,"tnc.host-access":"none"},u.HostConfig.NetworkMode=guestNet(r.name),u.HostConfig.Binds=[...i,`${s}:/run/tnc-workspace:ro`,`${guestHome(r.name)}:/home/${ws.USER}`],u.HostConfig.RestartPolicy={Name:"no"},await e.json("POST",`/containers/create?name=${encodeURIComponent(n)}`,u);for(const t of Object.keys(o.NetworkSettings?.Networks||{}))["bridge","default","host","none"].includes(t)||await e.json("POST",`/networks/${encodeURIComponent(t)}/connect`,{Container:n}).catch(()=>{});await e.json("POST",`/containers/${encodeURIComponent(n)}/start`);try{await waitPort(d)}catch(e){throw ws.workspaceError(`${e.message}; see: thinknsh logs ${n}`)}try{await verifyHostIsolation(e,n)}catch(t){throw await removeGuestContainer(e,r.name),t}return{port:d,container:n}}async function verifyHostIsolation(e,t){const{execIn:r}=require("../room-data");let o=!1;const s=net.createServer(e=>{o=!0,e.destroy()});await new Promise((e,t)=>{s.once("error",t),s.listen(0,"0.0.0.0",e)});try{const o=s.address().port;await r(e,t,["sh","-c",`timeout 4 bash -c 'exec 3<>/dev/tcp/host.tnc.internal/${o}' 2>/dev/null; true`])}finally{s.close()}if(o)throw ws.workspaceError("the guest container can still reach this machine: this dock does not isolate it yet. Restart dock with this version (thinknsh dock stop && thinknsh dock start), then share again")}async function removeGuestContainer(e,t){await e.json("DELETE",`/containers/${encodeURIComponent(guestContainer(t))}?force=1`).catch(()=>{}),await e.json("DELETE",`/volumes/${encodeURIComponent(guestHome(t))}?force=1`).catch(()=>{}),await e.json("DELETE",`/networks/${encodeURIComponent(guestNet(t))}`).catch(()=>{})}function bridge(e,t,r,o=()=>{}){let s=!0;const n=e=>{e?.streamId===t&&isBytes(e.data)&&r.write(Buffer.from(e.data.buffer?new Uint8Array(e.data.buffer,e.data.byteOffset,e.data.byteLength):e.data))},a=e=>{e?.streamId===t&&i(!1)},i=i=>{if(s){s=!1,e.off("workspace:data",n),e.off("workspace:close",a),i&&e.emit("workspace:close",{streamId:t});try{r.end()}catch(e){}o()}};return e.on("workspace:data",n),e.on("workspace:close",a),r.on("data",r=>{for(let o=0;o<r.length;o+=262144)e.emit("workspace:data",{streamId:t,data:r.subarray(o,o+262144)})}),r.on("end",()=>i(!0)),r.on("error",()=>i(!0)),e.on("disconnect",()=>i(!1)),i}async function listenLan(e){const t=lanAddresses(),r=[];let o=0;for(const s of t){const t=net.createServer(t=>{if(!isPrivate(t.remoteAddress))return t.destroy();const r=net.connect(e,()=>{t.pipe(r),r.pipe(t)});r.on("error",()=>t.destroy()),t.on("error",()=>r.destroy())});await new Promise(e=>{t.once("error",()=>e(!1)),t.listen(o,s,()=>e(!0))})&&(o=t.address().port,r.push(t))}return{port:r.length?o:null,addrs:r.map(e=>e.address().address),close:()=>r.forEach(e=>e.close())}}async function share(e,t,{api:r,owner:o,roomId:s=null,lan:n=!0,approve:a,log:i,onStop:c=()=>{},guest:d=null}){const{shareId:u,secret:p}=newCode(),f=d?{dir:d.dir,hostKey:d.hostKey}:guestKeys(t,e.name);i("starting the guest container (your folders, its own home)…");const w=d||await startGuestContainer(r,t,e,o,f.dir),l={host:"127.0.0.1",port:w.port},h=n?await listenLan(l):{port:null,addrs:[],close(){}};let y,m;const g=new Map;let k=!1;const S=async()=>{if(!k){k=!0;for(const e of g.values())e(!0);h.close();try{y?.emit("workspace:share-end",{shareId:u}),y?.close()}catch(e){}d||await removeGuestContainer(r,e.name);try{fs.writeFileSync(path.join(f.dir,"authorized_keys"),"",{mode:384})}catch(e){}}};try{({socket:y,user:m}=await connectPlatform());const t=()=>request(y,"workspace:share-register",{shareId:u,roomId:s,workspace:e.name});await t(),y.io.on("reconnect",()=>{t().catch(e=>i(`re-register failed: ${e.message}`))})}catch(e){throw await S(),e}let $=0;return y.on("workspace:join-request",async(t={})=>{const o=e=>y.emit("workspace:join-reply",{requestId:t.requestId,...e}),s=clean(t.fromName)||"someone",n=clean(t.fromUserId,40),l=`${s}${n?` (${n})`:""}`;if(k||$>=5)return o({ok:!1,error:"this share has stopped"});if(!PUBKEY.test(String(t.publicKey||"")))return o({ok:!1,error:"the join did not send an ed25519 key"});const g=Buffer.from(proofFor(p,t.publicKey)),j=Buffer.from(String(t.proof||""));if(j.length!==g.length||!crypto.timingSafeEqual(j,g))return i(`refused a join with a wrong code from ${l}`),++$>=5?(i("5 wrong codes: sharing stopped (start again for a new code)"),o({ok:!1,error:"wrong join code"}),await sleep(300),S().then(c,e=>{i(`stopping failed: ${e.message}`),c()})):o({ok:!1,error:"wrong join code"});if(!await a({name:s,userId:n}))return i(`declined ${l}`),o({ok:!1,error:"the workspace owner declined"});if(addGuestKey(f.dir,u,t.publicKey,s),!d){const e=guestKeyLine(u,t.publicKey,s),n=await execRoot(r,w.container,["sh","-c",'printf "%s\\n" "$1" >> /etc/tnc-workspace/authorized_keys',"sh",e]);if(0!==n.code)return i(`could not add ${l}'s key: ${n.stderr}`),o({ok:!1,error:"the workspace could not add your key"})}i(`let in ${l}`),o({ok:!0,user:ws.USER,hostKey:f.hostKey,workspace:e.name,folder:(e.paths||[]).length>1?`/run/tnc-workspace/${e.name}.code-workspace`:"/work",lan:h.port?h.addrs.map(e=>({address:e,port:h.port})):[],owner:m.name})}),y.on("workspace:open",(e={})=>{const{streamId:t}=e;if(!/^[a-f0-9]{24}$/.test(String(t||""))||g.has(t))return;if(g.size>=32)return y.emit("workspace:close",{streamId:t});const r=net.connect(l);g.set(t,bridge(y,t,r,()=>g.delete(t)))}),{code:`${u}-${p}-${fingerprint(f.hostKey)}`,shareId:u,lanPort:h.port,lanAddrs:h.addrs,cleanup:S}}async function join(e,t,{cli:r=process.argv[1]}={}){const{shareId:o,secret:s,fp:n}=parseCode(e),a=path.join(ws.joinedDir(t),o);fs.mkdirSync(a,{recursive:!0,mode:448});const i=path.join(a,"id_ed25519");fs.existsSync(i)||ws.keygen(i,`thinknsh-join-${o}`);const c=keyOnly(fs.readFileSync(`${i}.pub`,"utf8")),{socket:d}=await connectPlatform();try{const e=await request(d,"workspace:join",{shareId:o,proof:proofFor(s,c),publicKey:c},18e4);if(!HOSTKEY.test(e.hostKey||""))throw ws.workspaceError("the workspace sent no usable host key");if(fingerprint(e.hostKey)!==n)throw ws.workspaceError("the workspace key does not match the join code: someone between you and the owner may be posing as it. Not connecting.");const i=joinHost(o);fs.writeFileSync(path.join(a,"known_hosts"),`${i} ${e.hostKey}\n`,{mode:384});const u=(Array.isArray(e.lan)?e.lan:[]).filter(e=>e&&net.isIPv4(String(e.address))&&isPrivate(e.address)&&Number.isInteger(e.port)&&e.port>0&&e.port<65536).slice(0,8).map(e=>({address:e.address,port:e.port})),p={shareId:o,host:i,workspace:clean(e.workspace),owner:clean(e.owner),folder:safeFolder(String(e.folder||"/work")),lan:u,cli:r,joinedAt:(new Date).toISOString()};return fs.writeFileSync(path.join(a,"join.json"),JSON.stringify(p,null,2)+"\n",{mode:384}),ws.writeSshConfig(t),ws.syncUserSshConfig(t),p}catch(e){throw fs.rmSync(a,{recursive:!0,force:!0}),e}finally{d.close()}}function leave(e,t){fs.rmSync(path.join(ws.joinedDir(t),e),{recursive:!0,force:!0}),ws.writeSshConfig(t),ws.syncUserSshConfig(t)}function tryLan(e,t=800){return new Promise(r=>{let o=e.length,s=!1;if(!o)return r(null);for(const n of e){const e=net.connect({host:n.address,port:n.port}),a=setTimeout(()=>e.destroy(),t);e.once("connect",()=>{clearTimeout(a),s?e.destroy():(s=!0,r(e))}),e.once("close",()=>{clearTimeout(a),0!==--o||s||r(null)}),e.on("error",()=>{})}})}async function proxy(e,t,{stdin:r=process.stdin,stdout:o=process.stdout,stderr:s=process.stderr}={}){if(!SHARE_ID.test(e))throw ws.workspaceError(`bad share id "${e}"`);let n;try{n=JSON.parse(fs.readFileSync(path.join(ws.joinedDir(t),e,"join.json"),"utf8"))}catch(t){throw ws.workspaceError(`not joined to share ${e}; run: thinknsh workspace join CODE`)}const a=process.env.TNC_SHARE_RELAY_ONLY?null:await tryLan(n.lan.filter(e=>isPrivate(e.address)));if(a)return r.pipe(a),a.pipe(o),await new Promise(e=>a.on("close",e)),0;const{socket:i}=await connectPlatform(),c=crypto.randomBytes(12).toString("hex");await request(i,"workspace:open",{shareId:e,streamId:c}).catch(e=>{throw s.write(`thinknsh: ${e.message} (is the owner still sharing? ask them to run thinknsh workspace share again)\n`),e});const{Duplex:d}=require("stream");return await new Promise(e=>{const t=new d({read(){},write(e,t,r){o.write(e,r)}});r.on("data",e=>t.push(e)),r.on("end",()=>t.push(null)),bridge(i,c,t,e)}),i.close(),0}module.exports={share,join,leave,proxy,parseCode,newCode,fingerprint,proofFor,isPrivate,lanAddresses,clean,safeFolder,addGuestKey,guestKeyLine,guestKeys,guestContainer,guestHome,joinHost,bridge,removeGuestContainer};
@@ -4,4 +4,4 @@
4
4
  * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
5
  * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
6
  */
7
- "use strict";const fs=require("fs"),os=require("os"),net=require("net"),path=require("path"),crypto=require("crypto"),{spawnSync}=require("child_process"),{BUILD_FILE}=require("../api/wire"),USER="tnc",SSH_PORT_IN=2222,FIRST_HOST_PORT=22220,LABEL="tnc.workspace",DEFAULT_AGENTS=["@anthropic-ai/claude-code","@openai/codex"],AGENT_ALIASES={claude:"@anthropic-ai/claude-code",codex:"@openai/codex",gemini:"@google/gemini-cli"},IMAGE_RECIPE=`FROM node:22-bookworm-slim\nARG AGENTS=""\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends openssh-server git ca-certificates curl wget less procps ripgrep python3 python3-venv openssh-client \\\n && rm -rf /var/lib/apt/lists/* \\\n && groupmod -n ${USER} node && usermod -l ${USER} -d /home/${USER} -m -s /bin/bash node \\\n && mkdir -p /run/sshd /etc/tnc-workspace\nRUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + ; true\nRUN if [ -n "$AGENTS" ]; then npm install -g $AGENTS && npm cache clean --force; fi\nCOPY profile.sh /etc/profile.d/tnc-workspace.sh\nCOPY sshd_config /etc/tnc-workspace/sshd_config\nCOPY entrypoint.sh /usr/local/bin/tnc-workspace-entry\nRUN chmod 755 /usr/local/bin/tnc-workspace-entry && chmod 644 /etc/tnc-workspace/sshd_config\nENV TNC_WORKSPACE=1 NPM_CONFIG_PREFIX=/home/${USER}/.local PATH=/home/${USER}/.local/bin:$PATH\nWORKDIR /work\nEXPOSE 2222\nCMD ["/usr/local/bin/tnc-workspace-entry"]\n`,PROFILE='export NPM_CONFIG_PREFIX="$HOME/.local"\nexport PATH="$HOME/.local/bin:$PATH"\nexport TNC_WORKSPACE=1\ncd /work 2>/dev/null || true\n',SSHD_CONFIG=`Port 2222\nHostKey /etc/tnc-workspace/ssh_host_ed25519_key\nPidFile /tmp/tnc-sshd.pid\nAuthorizedKeysFile /etc/tnc-workspace/authorized_keys\nAllowUsers ${USER}\nPubkeyAuthentication yes\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nPermitRootLogin no\nPermitEmptyPasswords no\nUsePAM no\nStrictModes yes\nPermitUserEnvironment no\nAllowAgentForwarding no\nAllowTcpForwarding local\nGatewayPorts no\nPermitTunnel no\nX11Forwarding no\nAcceptEnv LANG LC_*\nSetEnv TNC_WORKSPACE=1 NPM_CONFIG_PREFIX=/home/${USER}/.local PATH=/home/${USER}/.local/bin:/usr/local/bin:/usr/bin:/bin\nSubsystem sftp internal-sftp\nPrintMotd no\n# loopback only: editors and probes open many short connections; don't drop new ones early\nMaxStartups 100:30:200\nLoginGraceTime 30\n`,DROP=`setpriv --reuid=${USER} --regid=${USER} --init-groups --inh-caps=-all --bounding-set=-all env HOME=/home/${USER} USER=${USER} LOGNAME=${USER}`,ENTRYPOINT=`#!/bin/sh\nset -eu\nsrc=/run/tnc-workspace\n[ -f "$src/ssh_host_ed25519_key" ] && [ -f "$src/authorized_keys" ] || { echo "tnc workspace: keys missing in $src" >&2; exit 1; }\ninstall -o ${USER} -g ${USER} -m 600 "$src/ssh_host_ed25519_key" /etc/tnc-workspace/ssh_host_ed25519_key\ninstall -o root -g root -m 644 "$src/authorized_keys" /etc/tnc-workspace/authorized_keys\n[ "$(stat -c %U /home/${USER})" = "${USER}" ] || chown ${USER}:${USER} /home/${USER}\nchmod 700 /home/${USER}\nexec ${DROP} /usr/sbin/sshd -D -e -f /etc/tnc-workspace/sshd_config\n`;function workspaceError(e){const n=new Error(e);return n.workspace=!0,n}function workspaceName(e){const n=String(e||"").toLowerCase().replace(/[^a-z0-9_.-]+/g,"-").replace(/^[-_.]+|[-_.]+$/g,"").slice(0,40);if(!n)throw workspaceError(`cannot make a workspace name from "${e}"; pass --name`);return n}const containerName=e=>`tnc-ws-${e}`,homeVolume=e=>`tnc-ws-${e}-home`,sshHost=e=>`tnc-${e}`;function agentPackages(e){return null==e||""===e?DEFAULT_AGENTS:"none"===e?[]:[...new Set(String(e).split(/[\s,]+/).filter(Boolean).map(e=>AGENT_ALIASES[e]||e))].map(e=>{if(!/^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*(@[\w.^~<>=-]+)?$/i.test(e))throw workspaceError(`invalid agent package "${e}"`);return e})}function imageTag(e){return`tnc-workspace:${crypto.createHash("sha256").update(IMAGE_RECIPE).update(SSHD_CONFIG).update(ENTRYPOINT).update(PROFILE).update(e.join(" ")).digest("hex").slice(0,12)}`}function writeImageContext(e){fs.mkdirSync(e,{recursive:!0}),fs.writeFileSync(path.join(e,BUILD_FILE),IMAGE_RECIPE),fs.writeFileSync(path.join(e,"sshd_config"),SSHD_CONFIG),fs.writeFileSync(path.join(e,"profile.sh"),PROFILE),fs.writeFileSync(path.join(e,"entrypoint.sh"),ENTRYPOINT,{mode:493})}function workspacesDir(e){return path.join(e,"workspaces")}function stateDir(e,n){return path.join(workspacesDir(e),n)}function readMeta(e,n){try{return JSON.parse(fs.readFileSync(path.join(stateDir(e,n),"workspace.json"),"utf8"))}catch(e){return null}}function listMeta(e){let n=[];try{n=fs.readdirSync(workspacesDir(e))}catch(e){}return n.map(n=>readMeta(e,n)).filter(Boolean).sort((e,n)=>e.name.localeCompare(n.name))}function keygen(e,n){const t=spawnSync("ssh-keygen",["-q","-t","ed25519","-N","","-C",n,"-f",e],{encoding:"utf8"});if(t.error)throw workspaceError("ssh-keygen was not found; install OpenSSH (it ships with macOS, Linux and Windows 10+)");if(0!==t.status)throw workspaceError(`ssh-keygen failed: ${(t.stderr||"").trim()}`)}function ensureKeys(e,n,t){const o=stateDir(e,n),r=path.join(o,"client"),s=path.join(o,"server");for(const n of[workspacesDir(e),o,r,s])fs.mkdirSync(n,{recursive:!0,mode:448});for(const e of[o,r,s])fs.chmodSync(e,448);const i=path.join(r,"id_ed25519"),a=path.join(s,"ssh_host_ed25519_key");fs.existsSync(i)||keygen(i,`thinknsh-workspace-${n}`),fs.existsSync(a)||keygen(a,`tnc-workspace-host-${n}`);const c=fs.readFileSync(`${i}.pub`,"utf8").trim();fs.writeFileSync(path.join(s,"authorized_keys"),`no-agent-forwarding,no-X11-forwarding ${c}\n`,{mode:384}),fs.rmSync(`${a}.pub`,{force:!0});const p=spawnSync("ssh-keygen",["-y","-f",a],{encoding:"utf8"}).stdout.trim(),h=path.join(o,"known_hosts");return fs.writeFileSync(h,`[127.0.0.1]:${t} ${p}\n`,{mode:384}),{dir:o,id:i,server:s,knownHosts:h}}function saveMeta(e,n){const t=path.join(stateDir(e,n.name),"workspace.json");fs.writeFileSync(t,JSON.stringify(n,null,2)+"\n",{mode:384})}function removeState(e,n){fs.rmSync(stateDir(e,n),{recursive:!0,force:!0})}const quote=e=>/[\s"]/.test(e)?`"${e.replace(/"/g,'\\"')}"`:e;function hostEntry(e,n){const t=stateDir(n,e.name);return[`Host ${sshHost(e.name)}`," HostName 127.0.0.1",` Port ${e.port}`,` User ${USER}`,` IdentityFile ${quote(path.join(t,"client","id_ed25519"))}`," IdentitiesOnly yes",` UserKnownHostsFile ${quote(path.join(t,"known_hosts"))}`," StrictHostKeyChecking yes"," ForwardAgent no"," ForwardX11 no",""].join("\n")}function writeSshConfig(e){const n=path.join(workspacesDir(e),"ssh_config");fs.mkdirSync(path.dirname(n),{recursive:!0,mode:448});const t=["# thinknsh workspaces; generated, edits are overwritten","",...listMeta(e).map(n=>hostEntry(n,e))].join("\n");return fs.writeFileSync(n,t,{mode:384}),n}function userSshConfig(){return process.env.TNC_SSH_CONFIG||path.join(os.homedir(),".ssh","config")}const INCLUDE_MARK="# thinknsh workspaces (thinknsh workspace up)",BLOCK_START="# >>> thinknsh workspaces (managed by thinknsh workspace; edits here are overwritten) >>>",BLOCK_END="# <<< thinknsh workspaces <<<";function syncUserSshConfig(e,n=userSshConfig()){let t="";try{t=fs.readFileSync(n,"utf8")}catch(e){if("ENOENT"!==e.code)throw e}const o=`Include ${quote(path.join(workspacesDir(e),"ssh_config"))}`,r=t.split(/\r?\n/),s=[];let i=!1;for(const e of r)e.trim()!==BLOCK_START?e.trim()!==BLOCK_END?i||e.trim()===INCLUDE_MARK||e.trim()===o||s.push(e):i=!1:i=!0;for(;s.length&&""===s[s.length-1].trim();)s.pop();for(;s.length&&""===s[0].trim();)s.shift();const a=listMeta(e).map(n=>hostEntry(n,e).trimEnd()),c=a.length?[BLOCK_START,...a.flatMap(e=>[e,""]),BLOCK_END]:[],p=[...s,...s.length&&c.length?[""]:[],...c].join("\n")+(s.length||c.length?"\n":"");if(p===t)return!1;fs.mkdirSync(path.dirname(n),{recursive:!0,mode:448}),fs.writeFileSync(n,p,{mode:384});try{fs.chmodSync(n,384)}catch(e){}return!0}function folderMounts(e){if(!e.length)throw workspaceError("no project folder given");if(1===e.length)return[{host:e[0],target:"/work"}];const n=new Map;return e.map(e=>{const t=path.basename(e);if(!/^[\w.@+-]+$/.test(t))throw workspaceError(`folder name "${t}" has characters that cannot be a path in the workspace; rename or pick another folder`);if(n.has(t))throw workspaceError(`two folders are both named "${t}" (${n.get(t)} and ${e}); use separate workspaces`);return n.set(t,e),{host:e,target:`/work/${t}`}})}function writeCodeWorkspace(e,n,t){const o=path.join(e,`${n}.code-workspace`),r={folders:t.map(e=>({name:path.basename(e.host),path:e.target})),settings:{}};return fs.writeFileSync(o,JSON.stringify(r,null,2)+"\n",{mode:420}),`/run/tnc-workspace/${n}.code-workspace`}function networkChoice(e,{network:n=[],compose:t,noCompose:o=!1}={}){let r=e?.networks||[];if(n.length){const e=n.flatMap(e=>String(e).split(",")).map(e=>e.trim()).filter(Boolean);r=e.includes("none")?[]:[...new Set(e)];for(const e of r){if(!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(e))throw workspaceError(`invalid network name "${e}"`);if(["host","none","null"].includes(e))throw workspaceError(`a workspace cannot join the "${e}" network`)}}let s=e&&"compose"in e?e.compose:"auto";if(o&&void 0!==t)throw workspaceError("--compose and --no-compose together");return o?s=!1:void 0!==t&&(s=""===t?"auto":t),{networks:r,compose:s}}function wantedNetworks(e,n=[]){return[...new Set([...e,...n.map(e=>e.name)])]}function portFree(e){return new Promise(n=>{const t=net.createServer();t.once("error",()=>n(!1)),t.listen(e,"127.0.0.1",()=>t.close(()=>n(!0)))})}async function pickPort(e=[]){for(let n=22220;n<22720;n++)if(!e.includes(n)&&await portFree(n))return n;throw workspaceError("no free port in 22220-22719; pass --port")}function containerBody({name:e,image:n,mounts:t,serverDir:o,port:r,memory:s,nanoCpus:i,pidsLimit:a=4096}){return{Image:n,Hostname:`tnc-${e}`.slice(0,63),Labels:{[LABEL]:e,[`${LABEL}.path`]:t.map(e=>e.host).join("\n"),[`${LABEL}.port`]:String(r)},ExposedPorts:{"2222/tcp":{}},HostConfig:{Binds:[...t.map(e=>`${e.host}:${e.target}`),`${o}:/run/tnc-workspace:ro`,`${homeVolume(e)}:/home/${USER}`],PortBindings:{"2222/tcp":[{HostIp:"127.0.0.1",HostPort:String(r)}]},Memory:s,NanoCpus:i,PidsLimit:a,CapDrop:["ALL"],CapAdd:["CHOWN","DAC_OVERRIDE","FOWNER","SETUID","SETGID","SETPCAP"],SecurityOpt:["no-new-privileges"],RestartPolicy:{Name:"unless-stopped"}}}}module.exports={USER,SSH_PORT_IN:2222,FIRST_HOST_PORT:22220,LABEL,DEFAULT_AGENTS,IMAGE_RECIPE,SSHD_CONFIG,ENTRYPOINT,PROFILE,DROP,workspaceName,containerName,homeVolume,sshHost,agentPackages,imageTag,writeImageContext,stateDir,readMeta,listMeta,saveMeta,removeState,ensureKeys,hostEntry,writeSshConfig,syncUserSshConfig,userSshConfig,INCLUDE_MARK,BLOCK_START,BLOCK_END,folderMounts,writeCodeWorkspace,networkChoice,wantedNetworks,pickPort,portFree,containerBody,workspaceError};
7
+ "use strict";const fs=require("fs"),os=require("os"),net=require("net"),path=require("path"),crypto=require("crypto"),{spawnSync}=require("child_process"),{BUILD_FILE}=require("../api/wire"),USER="tnc",SSH_PORT_IN=2222,FIRST_HOST_PORT=22220,LABEL="tnc.workspace",DEFAULT_AGENTS=["@anthropic-ai/claude-code","@openai/codex"],AGENT_ALIASES={claude:"@anthropic-ai/claude-code",codex:"@openai/codex",gemini:"@google/gemini-cli"},IMAGE_RECIPE=`FROM node:22-bookworm-slim\nARG AGENTS=""\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends openssh-server git ca-certificates curl wget less procps ripgrep python3 python3-venv openssh-client \\\n && rm -rf /var/lib/apt/lists/* \\\n && groupmod -n ${USER} node && usermod -l ${USER} -d /home/${USER} -m -s /bin/bash node \\\n && mkdir -p /run/sshd /etc/tnc-workspace\nRUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + ; true\nRUN if [ -n "$AGENTS" ]; then npm install -g $AGENTS && npm cache clean --force; fi\nCOPY profile.sh /etc/profile.d/tnc-workspace.sh\nCOPY sshd_config /etc/tnc-workspace/sshd_config\nCOPY entrypoint.sh /usr/local/bin/tnc-workspace-entry\nRUN chmod 755 /usr/local/bin/tnc-workspace-entry && chmod 644 /etc/tnc-workspace/sshd_config\nENV TNC_WORKSPACE=1 NPM_CONFIG_PREFIX=/home/${USER}/.local PATH=/home/${USER}/.local/bin:$PATH\nWORKDIR /work\nEXPOSE 2222\nCMD ["/usr/local/bin/tnc-workspace-entry"]\n`,PROFILE='export NPM_CONFIG_PREFIX="$HOME/.local"\nexport PATH="$HOME/.local/bin:$PATH"\nexport TNC_WORKSPACE=1\ncd /work 2>/dev/null || true\n',SSHD_CONFIG=`Port 2222\nHostKey /etc/tnc-workspace/ssh_host_ed25519_key\nPidFile /tmp/tnc-sshd.pid\nAuthorizedKeysFile /etc/tnc-workspace/authorized_keys\nAllowUsers ${USER}\nPubkeyAuthentication yes\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nPermitRootLogin no\nPermitEmptyPasswords no\nUsePAM no\nStrictModes yes\nPermitUserEnvironment no\nAllowAgentForwarding no\nAllowTcpForwarding local\nGatewayPorts no\nPermitTunnel no\nX11Forwarding no\nAcceptEnv LANG LC_*\nSetEnv TNC_WORKSPACE=1 NPM_CONFIG_PREFIX=/home/${USER}/.local PATH=/home/${USER}/.local/bin:/usr/local/bin:/usr/bin:/bin\nSubsystem sftp internal-sftp\nPrintMotd no\n# loopback only: editors and probes open many short connections; don't drop new ones early\nMaxStartups 100:30:200\nLoginGraceTime 30\n`,DROP=`setpriv --reuid=${USER} --regid=${USER} --init-groups --inh-caps=-all --bounding-set=-all env HOME=/home/${USER} USER=${USER} LOGNAME=${USER}`,ENTRYPOINT=`#!/bin/sh\nset -eu\nsrc=/run/tnc-workspace\n[ -f "$src/ssh_host_ed25519_key" ] && [ -f "$src/authorized_keys" ] || { echo "tnc workspace: keys missing in $src" >&2; exit 1; }\ninstall -o ${USER} -g ${USER} -m 600 "$src/ssh_host_ed25519_key" /etc/tnc-workspace/ssh_host_ed25519_key\ninstall -o root -g root -m 644 "$src/authorized_keys" /etc/tnc-workspace/authorized_keys\n[ "$(stat -c %U /home/${USER})" = "${USER}" ] || chown ${USER}:${USER} /home/${USER}\nchmod 700 /home/${USER}\nexec ${DROP} /usr/sbin/sshd -D -e -f /etc/tnc-workspace/sshd_config\n`;function workspaceError(e){const n=new Error(e);return n.workspace=!0,n}function workspaceName(e){const n=String(e||"").toLowerCase().replace(/[^a-z0-9_.-]+/g,"-").replace(/^[-_.]+|[-_.]+$/g,"").slice(0,40);if(!n)throw workspaceError(`cannot make a workspace name from "${e}"; pass --name`);return n}const containerName=e=>`tnc-ws-${e}`,homeVolume=e=>`tnc-ws-${e}-home`,sshHost=e=>`tnc-${e}`;function agentPackages(e){return null==e||""===e?DEFAULT_AGENTS:"none"===e?[]:[...new Set(String(e).split(/[\s,]+/).filter(Boolean).map(e=>AGENT_ALIASES[e]||e))].map(e=>{if(!/^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*(@[\w.^~<>=-]+)?$/i.test(e))throw workspaceError(`invalid agent package "${e}"`);return e})}function imageTag(e){return`tnc-workspace:${crypto.createHash("sha256").update(IMAGE_RECIPE).update(SSHD_CONFIG).update(ENTRYPOINT).update(PROFILE).update(e.join(" ")).digest("hex").slice(0,12)}`}function writeImageContext(e){fs.mkdirSync(e,{recursive:!0}),fs.writeFileSync(path.join(e,BUILD_FILE),IMAGE_RECIPE),fs.writeFileSync(path.join(e,"sshd_config"),SSHD_CONFIG),fs.writeFileSync(path.join(e,"profile.sh"),PROFILE),fs.writeFileSync(path.join(e,"entrypoint.sh"),ENTRYPOINT,{mode:493})}function workspacesDir(e){return path.join(e,"workspaces")}function stateDir(e,n){return path.join(workspacesDir(e),n)}function readMeta(e,n){try{return JSON.parse(fs.readFileSync(path.join(stateDir(e,n),"workspace.json"),"utf8"))}catch(e){return null}}function listMeta(e){let n=[];try{n=fs.readdirSync(workspacesDir(e))}catch(e){}return n.map(n=>readMeta(e,n)).filter(Boolean).sort((e,n)=>e.name.localeCompare(n.name))}function keygen(e,n){const t=spawnSync("ssh-keygen",["-q","-t","ed25519","-N","","-C",n,"-f",e],{encoding:"utf8"});if(t.error)throw workspaceError("ssh-keygen was not found; install OpenSSH (it ships with macOS, Linux and Windows 10+)");if(0!==t.status)throw workspaceError(`ssh-keygen failed: ${(t.stderr||"").trim()}`)}function ensureKeys(e,n,t){const o=stateDir(e,n),r=path.join(o,"client"),s=path.join(o,"server");for(const n of[workspacesDir(e),o,r,s])fs.mkdirSync(n,{recursive:!0,mode:448});for(const e of[o,r,s])fs.chmodSync(e,448);const i=path.join(r,"id_ed25519"),a=path.join(s,"ssh_host_ed25519_key");fs.existsSync(i)||keygen(i,`thinknsh-workspace-${n}`),fs.existsSync(a)||keygen(a,`tnc-workspace-host-${n}`);const c=fs.readFileSync(`${i}.pub`,"utf8").trim();fs.writeFileSync(path.join(s,"authorized_keys"),`no-agent-forwarding,no-X11-forwarding ${c}\n`,{mode:384}),fs.rmSync(`${a}.pub`,{force:!0});const p=spawnSync("ssh-keygen",["-y","-f",a],{encoding:"utf8"}).stdout.trim(),h=path.join(o,"known_hosts");return fs.writeFileSync(h,`[127.0.0.1]:${t} ${p}\n`,{mode:384}),{dir:o,id:i,server:s,knownHosts:h}}function saveMeta(e,n){const t=path.join(stateDir(e,n.name),"workspace.json");fs.writeFileSync(t,JSON.stringify(n,null,2)+"\n",{mode:384})}function removeState(e,n){fs.rmSync(stateDir(e,n),{recursive:!0,force:!0})}const quote=e=>/[\s"]/.test(e)?`"${e.replace(/"/g,'\\"')}"`:e;function hostEntry(e,n){const t=stateDir(n,e.name);return[`Host ${sshHost(e.name)}`," HostName 127.0.0.1",` Port ${e.port}`,` User ${USER}`,` IdentityFile ${quote(path.join(t,"client","id_ed25519"))}`," IdentitiesOnly yes",` UserKnownHostsFile ${quote(path.join(t,"known_hosts"))}`," StrictHostKeyChecking yes"," ForwardAgent no"," ForwardX11 no",""].join("\n")}function joinedDir(e){return path.join(workspacesDir(e),"joined")}function listJoined(e){let n=[];try{n=fs.readdirSync(joinedDir(e))}catch(e){}return n.map(n=>{try{return JSON.parse(fs.readFileSync(path.join(joinedDir(e),n,"join.json"),"utf8"))}catch(e){return null}}).filter(Boolean).sort((e,n)=>e.host.localeCompare(n.host))}function joinedEntry(e,n){const t=path.join(joinedDir(n),e.shareId);return[`Host ${e.host}`,` HostName ${e.host}`,` User ${USER}`,` ProxyCommand ${quote(process.execPath)} ${quote(e.cli)} workspace proxy ${e.shareId}`,` IdentityFile ${quote(path.join(t,"id_ed25519"))}`," IdentitiesOnly yes",` UserKnownHostsFile ${quote(path.join(t,"known_hosts"))}`,` HostKeyAlias ${e.host}`," StrictHostKeyChecking yes"," ForwardAgent no"," ForwardX11 no",""].join("\n")}const allEntries=e=>[...listMeta(e).map(n=>hostEntry(n,e)),...listJoined(e).map(n=>joinedEntry(n,e))];function writeSshConfig(e){const n=path.join(workspacesDir(e),"ssh_config");fs.mkdirSync(path.dirname(n),{recursive:!0,mode:448});const t=["# thinknsh workspaces; generated, edits are overwritten","",...allEntries(e)].join("\n");return fs.writeFileSync(n,t,{mode:384}),n}function userSshConfig(){return process.env.TNC_SSH_CONFIG||path.join(os.homedir(),".ssh","config")}const INCLUDE_MARK="# thinknsh workspaces (thinknsh workspace up)",BLOCK_START="# >>> thinknsh workspaces (managed by thinknsh workspace; edits here are overwritten) >>>",BLOCK_END="# <<< thinknsh workspaces <<<";function syncUserSshConfig(e,n=userSshConfig()){let t="";try{t=fs.readFileSync(n,"utf8")}catch(e){if("ENOENT"!==e.code)throw e}const o=`Include ${quote(path.join(workspacesDir(e),"ssh_config"))}`,r=t.split(/\r?\n/),s=[];let i=!1;for(const e of r)e.trim()!==BLOCK_START?e.trim()!==BLOCK_END?i||e.trim()===INCLUDE_MARK||e.trim()===o||s.push(e):i=!1:i=!0;for(;s.length&&""===s[s.length-1].trim();)s.pop();for(;s.length&&""===s[0].trim();)s.shift();const a=allEntries(e).map(e=>e.trimEnd()),c=a.length?[BLOCK_START,...a.flatMap(e=>[e,""]),BLOCK_END]:[],p=[...s,...s.length&&c.length?[""]:[],...c].join("\n")+(s.length||c.length?"\n":"");if(p===t)return!1;fs.mkdirSync(path.dirname(n),{recursive:!0,mode:448}),fs.writeFileSync(n,p,{mode:384});try{fs.chmodSync(n,384)}catch(e){}return!0}function folderMounts(e){if(!e.length)throw workspaceError("no project folder given");if(1===e.length)return[{host:e[0],target:"/work"}];const n=new Map;return e.map(e=>{const t=path.basename(e);if(!/^[\w.@+-]+$/.test(t))throw workspaceError(`folder name "${t}" has characters that cannot be a path in the workspace; rename or pick another folder`);if(n.has(t))throw workspaceError(`two folders are both named "${t}" (${n.get(t)} and ${e}); use separate workspaces`);return n.set(t,e),{host:e,target:`/work/${t}`}})}function writeCodeWorkspace(e,n,t){const o=path.join(e,`${n}.code-workspace`),r={folders:t.map(e=>({name:path.basename(e.host),path:e.target})),settings:{}};return fs.writeFileSync(o,JSON.stringify(r,null,2)+"\n",{mode:420}),`/run/tnc-workspace/${n}.code-workspace`}function networkChoice(e,{network:n=[],compose:t,noCompose:o=!1}={}){let r=e?.networks||[];if(n.length){const e=n.flatMap(e=>String(e).split(",")).map(e=>e.trim()).filter(Boolean);r=e.includes("none")?[]:[...new Set(e)];for(const e of r){if(!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(e))throw workspaceError(`invalid network name "${e}"`);if(["host","none","null"].includes(e))throw workspaceError(`a workspace cannot join the "${e}" network`)}}let s=e&&"compose"in e?e.compose:"auto";if(o&&void 0!==t)throw workspaceError("--compose and --no-compose together");return o?s=!1:void 0!==t&&(s=""===t?"auto":t),{networks:r,compose:s}}function wantedNetworks(e,n=[]){return[...new Set([...e,...n.map(e=>e.name)])]}function portFree(e){return new Promise(n=>{const t=net.createServer();t.once("error",()=>n(!1)),t.listen(e,"127.0.0.1",()=>t.close(()=>n(!0)))})}async function pickPort(e=[]){for(let n=22220;n<22720;n++)if(!e.includes(n)&&await portFree(n))return n;throw workspaceError("no free port in 22220-22719; pass --port")}function containerBody({name:e,image:n,mounts:t,serverDir:o,port:r,memory:s,nanoCpus:i,pidsLimit:a=4096}){return{Image:n,Hostname:`tnc-${e}`.slice(0,63),Labels:{[LABEL]:e,[`${LABEL}.path`]:t.map(e=>e.host).join("\n"),[`${LABEL}.port`]:String(r)},ExposedPorts:{"2222/tcp":{}},HostConfig:{Binds:[...t.map(e=>`${e.host}:${e.target}`),`${o}:/run/tnc-workspace:ro`,`${homeVolume(e)}:/home/${USER}`],PortBindings:{"2222/tcp":[{HostIp:"127.0.0.1",HostPort:String(r)}]},Memory:s,NanoCpus:i,PidsLimit:a,CapDrop:["ALL"],CapAdd:["CHOWN","DAC_OVERRIDE","FOWNER","SETUID","SETGID","SETPCAP"],SecurityOpt:["no-new-privileges"],RestartPolicy:{Name:"unless-stopped"}}}}module.exports={USER,SSH_PORT_IN:2222,FIRST_HOST_PORT:22220,LABEL,DEFAULT_AGENTS,IMAGE_RECIPE,SSHD_CONFIG,ENTRYPOINT,PROFILE,DROP,workspaceName,containerName,homeVolume,sshHost,agentPackages,imageTag,writeImageContext,stateDir,readMeta,listMeta,saveMeta,removeState,ensureKeys,keygen,quote,joinedDir,listJoined,joinedEntry,hostEntry,writeSshConfig,syncUserSshConfig,userSshConfig,INCLUDE_MARK,BLOCK_START,BLOCK_END,folderMounts,writeCodeWorkspace,networkChoice,wantedNetworks,pickPort,portFree,containerBody,workspaceError};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkncollab-shell",
3
- "version": "2.0.0-beta.5",
3
+ "version": "2.0.0-beta.6",
4
4
  "description": "Lightweight MicroVM Sandbox & Zero-Daemon Dev Runner: Fast MicroVM Containers, Native Microservices, Live Database Snapshots, and End-to-End Encrypted State Sync",
5
5
  "main": "dist/src/core/shell.js",
6
6
  "bin": {
@@ -1,7 +0,0 @@
1
- /**
2
- * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
- * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
- * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
- * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
- */
7
- const{execSync,spawnSync}=require("child_process"),fs=require("fs-extra"),path=require("path"),os=require("os"),chalk=require("chalk"),HOST_ARCH=(()=>{const s=process.arch;return"arm64"===s||"aarch64"===s?"arm64":"amd64"})(),HOST_PLATFORM=process.platform,QEMU_BINARIES={amd64:"qemu-x86_64-static",arm64:"qemu-aarch64-static",arm:"qemu-arm-static",386:"qemu-i386-static",ppc64le:"qemu-ppc64le-static",s390x:"qemu-s390x-static"},BINFMT_MAGIC={amd64:{magic:"\\x7fELF\\x02\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x3e\\x00",mask:"\\xff\\xff\\xff\\xff\\xff\\xfe\\xfe\\x00\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xfe\\xff\\xff\\xff",offset:0},arm64:{magic:"\\x7fELF\\x02\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\xb7\\x00",mask:"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xfe\\xff\\xff\\xff",offset:0},arm:{magic:"\\x7fELF\\x01\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x28\\x00",mask:"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xfe\\xff\\xff\\xff",offset:0}};class ArchShim{constructor(){this.hostArch=HOST_ARCH,this.platform=HOST_PLATFORM}detectImageArch(s){try{const t=path.join(s,"config.json");if(fs.existsSync(t)){const s=fs.readJsonSync(t),r=s.architecture||s.config?.architecture;if(r)return"amd64"===r||"x86_64"===r?"amd64":"arm64"===r||"aarch64"===r?"arm64":r}}catch(s){}return"amd64"}needsTranslation(s){return s!==this.hostArch}hasRosetta2(){if("darwin"!==this.platform)return!1;try{return execSync("/usr/bin/arch -x86_64 /usr/bin/true 2>/dev/null",{stdio:"pipe"}),!0}catch(s){return!1}}async ensureRosetta2(){if(this.hasRosetta2())return!0;console.log(chalk.yellow(" [Arch] Rosetta 2 not installed. Installing..."));try{return execSync("softwareupdate --install-rosetta --agree-to-license",{stdio:"inherit"}),!0}catch(s){return console.log(chalk.red(" [Arch] Rosetta 2 install failed. Run: softwareupdate --install-rosetta --agree-to-license")),!1}}hasBinfmt(s){const t=`/proc/sys/fs/binfmt_misc/qemu-${s}`;return fs.existsSync(t)}findQEMU(s){const t=QEMU_BINARIES[s];if(!t)return null;const r=[`/usr/bin/${t}`,`/usr/local/bin/${t}`,`/usr/static/${t}`];for(const s of r)if(fs.existsSync(s))return s;return null}registerBinfmt(s){if("darwin"===this.platform||"win32"===this.platform)return{success:!1,reason:"binfmt_misc only for Linux"};const t=this.findQEMU(s);if(!t)return{success:!1,reason:"QEMU binary not found. Install: sudo apt-get install qemu-user-static"};if(this.hasBinfmt(s))return{success:!0,cached:!0};const r=BINFMT_MAGIC[s];if(!r)return{success:!1,reason:`No binfmt magic for arch: ${s}`};try{const e=`:qemu-${s}:M::${r.magic}:${r.mask}:${t}:F`;return execSync(`echo '${e}' > /proc/sys/fs/binfmt_misc/register`,{stdio:"pipe"}),{success:!0}}catch(s){return{success:!1,reason:`binfmt_misc register failed (need root): ${s.message}`}}}async getExecWrapper(s){if(!this.needsTranslation(s))return{wrapper:[],archInfo:`native (${this.hostArch})`,needsTranslation:!1};if(console.log(chalk.yellow(` [Arch] Image: ${s} | Host: ${this.hostArch} → Cross-arch translation needed`)),"darwin"===this.platform&&"arm64"===this.hostArch&&"amd64"===s&&await this.ensureRosetta2())return console.log(chalk.green(" [Arch] Using Rosetta 2 translation ✓")),{wrapper:["/usr/bin/arch","-x86_64"],archInfo:"Rosetta 2 (arm64→x86_64)",needsTranslation:!0};if("darwin"===this.platform&&"amd64"===this.hostArch&&"arm64"===s){const s=this.findQEMU("arm64")||"/opt/homebrew/bin/qemu-aarch64";if(fs.existsSync(s))return console.log(chalk.green(" [Arch] Using QEMU for arm64 on Intel Mac ✓")),{wrapper:[s],archInfo:"QEMU (x86_64→arm64)",needsTranslation:!0};console.log(chalk.yellow(" [Arch] QEMU not found. Install: brew install qemu"))}if("linux"===this.platform){const t=this.registerBinfmt(s);if(t.success){const t=this.findQEMU(s);return console.log(chalk.green(` [Arch] binfmt_misc registered for ${s} ✓`)),{wrapper:t?[t]:[],archInfo:`binfmt_misc + QEMU (${this.hostArch}→${s})`,needsTranslation:!0,requiresBinfmt:!0}}console.log(chalk.yellow(` [Arch] binfmt_misc failed: ${t.reason}`))}return"win32"===this.platform?(console.log(chalk.dim(" [Arch] Windows: cross-arch via WSL2 binfmt_misc")),{wrapper:["wsl","--"],archInfo:`WSL2 binfmt_misc (${s})`,needsTranslation:!0}):(console.log(chalk.yellow(` [Arch] WARNING: No translator found for ${this.hostArch}→${s}. Binary may fail.`)),{wrapper:[],archInfo:`unknown (${s})`,needsTranslation:!0,warned:!0})}injectQEMU(s,t){const r=this.findQEMU(t);if(!r||!fs.existsSync(r))return!1;const e=path.join(s,"usr","bin"),n=path.join(e,path.basename(r));try{return fs.ensureDirSync(e),fs.copyFileSync(r,n),fs.chmodSync(n,493),console.log(chalk.dim(` [Arch] Injected ${path.basename(r)} into rootfs`)),!0}catch(s){return!1}}status(){const s={hostArch:this.hostArch,platform:this.platform,rosetta2:"darwin"===this.platform?this.hasRosetta2():"N/A",binfmt:{}};if("linux"===this.platform)for(const t of["amd64","arm64","arm"])t!==this.hostArch&&(s.binfmt[t]=this.hasBinfmt(t));const t={};for(const[s,r]of Object.entries(QEMU_BINARIES)){const r=this.findQEMU(s);r&&(t[s]=r)}return s.qemu=t,s}}module.exports=ArchShim;
@@ -1,7 +0,0 @@
1
- /**
2
- * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
- * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
- * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
- * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
- */
7
- const fs=require("fs-extra"),path=require("path"),crypto=require("crypto"),chalk=require("chalk"),{execSync}=require("child_process"),ImageManager=require("./image-manager");class CapsuleBuilder{constructor(e=process.cwd()){this.contextDir=path.resolve(e),this.imageManager=new ImageManager,this.layersDir=path.join(this.imageManager.baseDir,"layers"),fs.ensureDirSync(this.layersDir),this.dockerignore=this._loadDockerignore()}_loadDockerignore(){const e=path.join(this.contextDir,".dockerignore");if(!fs.existsSync(e))return[];try{return fs.readFileSync(e,"utf8").split("\n").map(e=>e.trim()).filter(e=>e&&!e.startsWith("#"))}catch(e){return[]}}_isIgnored(e){if(!this.dockerignore||0===this.dockerignore.length)return!1;const t=path.basename(e);for(const s of this.dockerignore){if(s===t||s===e)return!0;if(s.endsWith("/*")&&e.startsWith(s.slice(0,-2)))return!0;if(s.startsWith("*")&&e.endsWith(s.slice(1)))return!0;if(e.includes(`/${s}/`)||e.startsWith(`${s}/`))return!0}return!1}parseSpecFile(e){if(!fs.existsSync(e))throw new Error(`Dockerfile not found at: ${e}`);const t=fs.readFileSync(e,"utf8").split("\n"),s=[];let i="";for(let e of t)if(e=e.trim(),e&&!e.startsWith("#"))if(e.endsWith("\\"))i+=e.slice(0,-1).trim()+" ";else{i+=e;const t=i.indexOf(" ");if(-1!==t){const e=i.slice(0,t).toUpperCase(),n=i.slice(t+1).trim();s.push({instruction:e,args:n,raw:i})}i=""}return s}computeStepHash(e,t,s){return crypto.createHash("sha256").update(`${e}:${t}:${s}`).digest("hex").slice(0,16)}interpolateVars(e,t={}){return e.replace(/\$\{?([a-zA-Z0-9_]+)\}?/g,(e,s)=>void 0!==t[s]?t[s]:e)}async build(e="latest",t="Capsulefile",s={}){let i=path.isAbsolute(t)?t:path.join(this.contextDir,t);if(!fs.existsSync(i)){const e=path.join(this.contextDir,"Dockerfile");fs.existsSync(e)&&(i=e)}const n=this.parseSpecFile(i);console.log(chalk.cyan(`\n[+] Building TNC Capsule from ${path.basename(i)}`)),console.log(chalk.dim(` Context: ${this.contextDir}`)),console.log(chalk.dim(` Total Steps: ${n.length}\n`));const r="img-"+crypto.randomBytes(4).toString("hex"),o={};let a=0,c=null,l=null,h="/",p={};const u=s.buildArgs||{};let g=[],f="node server.js",d="node",m="genesis",y=1;for(const e of n){const t={...u,...p},s=this.interpolateVars(e.args,t),i=this.computeStepHash(m,e.instruction,s),$=`[${y}/${n.length}]`;switch(e.instruction){case"ARG":{let[e,...t]=s.split("=");e=e.trim();const i=t.length>0?t.join("=").trim():"";void 0===u[e]&&(u[e]=i),console.log(chalk.dim(` => ${$} ARG ${e}=${u[e]}`));break}case"FROM":{let e=s;e.startsWith("--platform=")&&(e=e.split(/\s+/).slice(1).join(" "));const t=e.split(/\s+/),i=t[0];let n=null;if(t.length>=3&&"AS"===t[1].toUpperCase()&&(n=t[2]),l){const e=c||String(a);o[e]={workDir:l,env:{...p},workdir:h},c&&(o[String(a)]=o[e]),a++}c=n;const u=n?`stage-${n}`:`stage-${a}`;l=path.join(this.imageManager.baseDir,r,u),fs.ensureDirSync(l),h="/",p={},g=[],d=i.includes("python")?"python":i.includes("rust")?"rust":i.includes("golang")||i.includes("go")?"go":i.includes("cpp")||i.includes("gcc")||i.includes("clang")?"cpp":i.includes("nginx")?"nginx":"node";const f=n?` AS ${n}`:"";if(console.log(chalk.blue(` => ${$} FROM ${chalk.bold(i)}${f}`)),o[i])console.log(chalk.dim(` Using previous stage "${i}" as base...`)),fs.copySync(o[i].workDir,l),p={...o[i].env},h=o[i].workdir;else if("scratch"!==i&&"base"!==i)try{const e=new(require("./oci-puller"));let t=e.inspect(i);if(t||(console.log(chalk.dim(` Pulling base OCI image: ${i}...`)),t=(await e.pull(i)).meta),t&&t.rootfsDir&&fs.existsSync(t.rootfsDir)&&(fs.copySync(t.rootfsDir,l),t.env&&Array.isArray(t.env)))for(const e of t.env){const[t,...s]=e.split("=");p[t]=s.join("=")}}catch(e){console.log(chalk.dim(` Notice: Base OCI pull skipped (${e.message}). Using clean rootfs.`))}break}case"WORKDIR":{h=s.trim();const e=path.join(l,h.replace(/^\//,""));fs.ensureDirSync(e),console.log(chalk.blue(` => ${$} WORKDIR ${h}`));break}case"ENV":{let e,t;if(s.includes("=")){const i=s.indexOf("=");e=s.slice(0,i).trim(),t=s.slice(i+1).trim()}else{const i=s.split(/\s+/);e=i[0],t=i.slice(1).join(" ")}p[e]=t,console.log(chalk.blue(` => ${$} ENV ${e}=${t}`));break}case"COPY":case"ADD":{let e=null,t=s;if(t.startsWith("--from=")){const s=t.indexOf(" ");-1!==s&&(e=t.slice(7,s).trim(),t=t.slice(s+1).trim())}const i=t.split(/\s+/),n=i[0],r=i[1]||".";let a=this.contextDir;if(e){const t=o[e];if(!t)throw new Error(`Stage "${e}" not found in multi-stage build`);a=t.workDir,console.log(chalk.cyan(` => ${$} COPY --from=${e} ${n} -> ${r}`))}else console.log(chalk.blue(` => ${$} COPY ${n} -> ${r}`));const c=path.isAbsolute(n)&&e?path.join(a,n.replace(/^\//,"")):path.join(a,n.replace(/\*/g,"")),p=r.replace(/^\.\//,"").replace(/^\//,"");let u=path.isAbsolute(r)?path.join(l,r.replace(/^\//,"")):path.join(l,h.replace(/^\//,""),p);fs.existsSync(c)&&fs.statSync(c).isFile()&&("."===r||r.endsWith("/")||fs.existsSync(u)&&fs.statSync(u).isDirectory())&&(u=path.join(u,path.basename(c))),fs.existsSync(c)?(fs.ensureDirSync(path.dirname(u)),fs.copySync(c,u,{filter:e=>{if(path.basename(e).startsWith(".git"))return!1;const t=path.relative(a,e);return!this._isIgnored(t)}}),console.log(chalk.green(` [Copied] ${c} -> ${u}`))):console.log(chalk.red(` [Notice] Source path NOT FOUND: ${c}`));break}case"RUN":{let e=s;const t=[];for(;e.startsWith("--mount=");){const s=e.indexOf(" ");if(-1===s)break;const i=e.slice(0,s);t.push(i),e=e.slice(s+1).trim()}for(const e of t)if(e.includes("type=cache")&&e.includes("target=")){const t=e.match(/target=([^,]+)/);if(t&&t[1]){const e=path.join(l,t[1].replace(/^\//,""));fs.ensureDirSync(e)}}const i=path.join(l,h.replace(/^\//,""));fs.ensureDirSync(i),console.log(chalk.yellow(` => ${$} RUN ${chalk.white(e)}`));let n=!1;const{getMicroVMManager:r,sendCommand:o}=require("./microvm-manager"),a=r();if(!a.isTncVzAvailable()||!a.isVMRunning())throw new Error(`[Security Policy] Linux MicroVM is not running. Executing Dockerfile instruction "RUN ${e}" directly on macOS host is blocked for system safety.`);const c=`build-${Date.now().toString(36)}`;try{const t=await a.start(c,{cwd:l,cmd:`cd "${h}" && ${e}`,env:p});if(!t||!t.success)throw new Error(`Failed to initiate build step in MicroVM: ${s}`);const i=12e4,r=Date.now();let u=!0;for(;u&&Date.now()-r<i;){await new Promise(e=>setTimeout(e,600));try{const e=await o({cmd:"ps"},3e3);if(e&&e.ok&&Array.isArray(e.containers)){const t=e.containers.find(e=>e.id===c);u=Boolean(t&&t.running)}else u=!1}catch(e){u=!1}}if(u)throw await a.stop(c).catch(()=>{}),new Error(`Build step timed out after 120s: ${s}`);try{await a.commit(c)}catch(e){console.warn(chalk.yellow(` [Build Commit notice] ${e.message}`))}await a.stop(c).catch(()=>{}),n=!0}catch(e){throw await a.stop(c).catch(()=>{}),console.error(chalk.red(`[FAIL] Build step failed: ${e.message}`)),e}break}case"USER":console.log(chalk.dim(` => ${$} USER ${s}`));break;case"EXPOSE":{const e=parseInt(s.trim(),10);isNaN(e)||g.push(e),console.log(chalk.blue(` => ${$} EXPOSE ${s}`));break}case"CMD":case"ENTRYPOINT":{let t=s.trim();if(t.startsWith("[")&&t.endsWith("]"))try{t=JSON.parse(t).join(" ")}catch(e){}f=t,console.log(chalk.green(` => ${$} ${e.instruction} ${f}`));break}default:console.log(chalk.dim(` => ${$} [Ignored ${e.instruction}] ${s}`))}m=i,y++}const $=path.join(this.imageManager.baseDir,r,"rootfs");l!==$&&(fs.existsSync($)&&fs.removeSync($),fs.moveSync(l,$,{overwrite:!0}));for(let e=0;e<=a;e++){const t=path.join(this.imageManager.baseDir,r,`stage-${e}`);try{fs.removeSync(t)}catch(e){}}for(const e of Object.keys(o))if(o[e]?.workDir&&o[e].workDir!==$)try{fs.removeSync(o[e].workDir)}catch(e){}const k=e.includes(":")?e:`${path.basename(this.contextDir)}:${e}`,S={id:r,tag:k,project:path.basename(this.contextDir),runtimes:[d],entrypoint:f,ports:g,environment:p,workdir:h,builtFrom:"Dockerfile (Multi-Stage)",createdAt:(new Date).toISOString(),sizeMB:this.imageManager.calculateDirSize($)},D=path.join(this.imageManager.baseDir,r);fs.ensureDirSync(D),fs.writeJsonSync(path.join(D,"meta.json"),S,{spaces:2}),fs.writeJsonSync(path.join(D,"blueprint.json"),{project:S.project,runtime:{primary:d,runtimes:[d]},scripts:{start:f},environment:p},{spaces:2});const w=this.imageManager.loadManifest();w[k]=S,w[r]=S,this.imageManager.saveManifest(w);try{const e=path.join(this.imageManager.baseDir,"manifest.json"),t=fs.existsSync(e)?fs.readJsonSync(e):{},s={...S,imageDir:D,rootfsDir:$,layers:n.length};t[k]=s,t[r]=s,fs.writeJsonSync(e,t,{spaces:2})}catch(e){}return console.log(chalk.green(`\n[OK] Successfully built Multi-Stage Capsule: ${chalk.bold(k)} (ID: ${r})`)),console.log(chalk.dim(` Entrypoint: ${f}`)),console.log(chalk.dim(` Exposed: ${g.join(", ")||"none"}\n`)),{success:!0,imageId:r,tag:k,meta:S}}}module.exports=CapsuleBuilder;
@@ -1,7 +0,0 @@
1
- /**
2
- * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
- * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
- * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
- * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
- */
7
- const fs=require("fs-extra"),path=require("path"),{execSync}=require("child_process"),TNC_CGROUP_ROOT="/sys/fs/cgroup/tnc";class CgroupsV2Manager{constructor(){this.isLinux="linux"===process.platform,this._v2Available=null}isAvailable(){if(!this.isLinux)return!1;if(null!==this._v2Available)return this._v2Available;try{const t=fs.readFileSync("/sys/fs/cgroup/cgroup.controllers","utf8");this._v2Available=t.includes("memory")&&t.includes("cpu")}catch{this._v2Available=!1}return this._v2Available}cgroupPath(t){return path.join(TNC_CGROUP_ROOT,t)}ensureRootCgroup(){if(!this.isAvailable())return!1;try{fs.ensureDirSync(TNC_CGROUP_ROOT);const t="/sys/fs/cgroup/cgroup.subtree_control",r=fs.readFileSync(t,"utf8"),e=[];r.includes("memory")||e.push("+memory"),r.includes("cpu")||e.push("+cpu"),r.includes("pids")||e.push("+pids"),e.length>0&&fs.writeFileSync(t,e.join(" "));const s=path.join(TNC_CGROUP_ROOT,"cgroup.subtree_control");if(fs.existsSync(s))try{fs.writeFileSync(s,"+memory +cpu +pids")}catch(t){}return!0}catch(t){return!1}}create(t,r={}){if(!this.isAvailable())return{success:!1,reason:"cgroups-v2-unavailable"};const{memoryLimit:e="2g",cpuPercent:s=80,pidsMax:i=500}=r,c=this.cgroupPath(t);try{this.ensureRootCgroup(),fs.ensureDirSync(c);const t=this._parseMemory(e);if(t>0){fs.writeFileSync(path.join(c,"memory.max"),String(t));try{fs.writeFileSync(path.join(c,"memory.swap.max"),"0")}catch(t){}}const r=1e5,n=Math.floor(s/100*r);return fs.writeFileSync(path.join(c,"cpu.max"),`${n} ${r}`),fs.writeFileSync(path.join(c,"pids.max"),String(i)),{success:!0,cgPath:c,memBytes:t,cpuQuota:`${n}/${r} (${s}%)`,pidsMax:i}}catch(t){return{success:!1,reason:t.message}}}assignPid(t,r){if(!this.isAvailable())return!1;try{const e=path.join(this.cgroupPath(t),"cgroup.procs");return fs.writeFileSync(e,String(r)),!0}catch{return!1}}getStats(t){if(!this.isAvailable())return null;const r=this.cgroupPath(t);if(!fs.existsSync(r))return null;const e=(t,e="0")=>{try{return fs.readFileSync(path.join(r,t),"utf8").trim()}catch{return e}},s=parseInt(e("memory.current","0")),i=e("memory.max","max"),c=parseInt(e("pids.current","0")),n=e("pids.max","max");let a=0;try{const t=e("memory.events","").match(/oom_kill\s+(\d+)/);t&&(a=parseInt(t[1]))}catch(t){}let o=0;try{const t=e("cpu.stat","").match(/usage_usec\s+(\d+)/);t&&(o=parseInt(t[1]))}catch(t){}return{memoryMB:Math.round(s/1024/1024),memoryBytes:s,memoryLimitMB:"max"===i?null:Math.round(parseInt(i)/1024/1024),pids:c,pidsLimit:"max"===n?null:parseInt(n),oomKills:a,cpuUsageMs:Math.round(o/1e3)}}cgroupFreeze(t){if(!this.isAvailable())return!1;try{const r=path.join(this.cgroupPath(t),"cgroup.freeze");if(fs.existsSync(r))return fs.writeFileSync(r,"1"),!0}catch(t){}return!1}cgroupThaw(t){if(!this.isAvailable())return!1;try{const r=path.join(this.cgroupPath(t),"cgroup.freeze");if(fs.existsSync(r))return fs.writeFileSync(r,"0"),!0}catch(t){}return!1}killAll(t){if(!this.isAvailable())return!1;const r=this.cgroupPath(t);try{const t=path.join(r,"cgroup.kill");if(fs.existsSync(t))return fs.writeFileSync(t,"1"),!0}catch(t){}try{const t=path.join(r,"cgroup.procs");if(fs.existsSync(t)){const r=fs.readFileSync(t,"utf8").split("\n").map(t=>parseInt(t.trim())).filter(t=>!isNaN(t)&&t>0);for(const t of r)try{process.kill(t,"SIGKILL")}catch(t){}return!0}}catch(t){}return!1}destroy(t){if(!this.isAvailable())return!1;const r=this.cgroupPath(t);try{return this.killAll(t),fs.existsSync(r)&&execSync(`rmdir "${r}" 2>/dev/null || true`,{stdio:"ignore"}),!0}catch{return!1}}_parseMemory(t){if(!t)return 0;const r=String(t).toLowerCase().trim().match(/^(\d+(?:\.\d+)?)([kmg]?)b?$/);if(!r)return 0;const e=parseFloat(r[1]);switch(r[2]){case"k":return Math.floor(1024*e);case"m":return Math.floor(1024*e*1024);case"g":return Math.floor(1024*e*1024*1024);default:return Math.floor(e)}}}module.exports=CgroupsV2Manager;
@@ -1,7 +0,0 @@
1
- /**
2
- * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
- * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
- * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
- * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
- */
7
- const{spawn,spawnSync}=require("child_process"),fs=require("fs-extra"),path=require("path"),os=require("os");class ContainerMonitor{constructor(t,e){this.sandboxId=t,this.sandboxDir=e||path.join(os.homedir(),".tnc","sandboxes",t),this.logFile=path.join(this.sandboxDir,"output.log"),this.pidFile=path.join(this.sandboxDir,"container.pid"),this.metaFile=path.join(this.sandboxDir,"meta.json")}launch({cmd:t,args:e=[],cwd:s,env:i={},runtime:n="crun"}){fs.ensureDirSync(this.sandboxDir);const r=fs.openSync(this.logFile,"a"),a=spawn(t,e,{cwd:s||this.sandboxDir,env:{...process.env,...i},detached:!0,stdio:["ignore",r,r]}),o=a.pid;return fs.writeFileSync(this.pidFile,String(o),"utf8"),this._updateMeta({pid:o,state:"RUNNING",startedAt:(new Date).toISOString(),exitCode:null,runtime:n}),a.unref(),a.on("exit",(t,e)=>{try{fs.closeSync(r)}catch(t){}this._updateMeta({state:"STOPPED",exitCode:null!==t?t:e?128:0,stoppedAt:(new Date).toISOString()});try{fs.existsSync(this.pidFile)&&fs.removeSync(this.pidFile)}catch(t){}}),{pid:o,logFile:this.logFile}}isAlive(){if(!fs.existsSync(this.pidFile))return!1;try{const t=fs.readFileSync(this.pidFile,"utf8").trim(),e=parseInt(t,10);return!isNaN(e)&&(process.kill(e,0),!0)}catch(t){return!1}}stop(t=5e3){if(!this.isAlive())return this._updateMeta({state:"STOPPED"}),{success:!0,message:"Container already stopped."};try{const e=parseInt(fs.readFileSync(this.pidFile,"utf8").trim(),10);process.kill(e,"SIGTERM");const s=Date.now();for(;Date.now()-s<t;)try{process.kill(e,0),spawnSync("sleep",["0.1"],{stdio:"pipe"})}catch{break}try{process.kill(e,0),process.kill(e,"SIGKILL")}catch(t){}return this._updateMeta({state:"STOPPED",stoppedAt:(new Date).toISOString()}),fs.existsSync(this.pidFile)&&fs.removeSync(this.pidFile),{success:!0}}catch(t){return{success:!1,error:t.message}}}_updateMeta(t={}){try{let e={};fs.existsSync(this.metaFile)&&(e=fs.readJsonSync(this.metaFile)),e={...e,...t,updatedAt:(new Date).toISOString()},fs.writeJsonSync(this.metaFile,e,{spaces:2})}catch(t){}}}module.exports=ContainerMonitor;
@@ -1,7 +0,0 @@
1
- /**
2
- * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
- * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
- * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
- * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
- */
7
- const path=require("path"),os=require("os"),fs=require("fs-extra"),{spawnSync,spawn}=require("child_process"),https=require("https"),crypto=require("crypto"),chalk=require("chalk"),TNC_BIN_DIR=path.join(os.homedir(),".tnc","bin"),CRUN_LOCAL_BIN=path.join(TNC_BIN_DIR,"crun"),CRUN_VERSION="1.14.4",CRUN_URLS={"linux-amd64":"https://github.com/containers/crun/releases/download/1.14.4/crun-1.14.4-linux-amd64","linux-arm64":"https://github.com/containers/crun/releases/download/1.14.4/crun-1.14.4-linux-arm64"},CRUN_SHA256={"linux-amd64":"4f170aaa10d2ef02560cfb60b67ddfa1a83b1b4f7018227e9cb23a6af3955ec1","linux-arm64":"308f8719055de178897f66cbb72d6a02567050ac645dd5eca52f48de347dda6c"};class CrunManager{constructor(){this.binDir=TNC_BIN_DIR,this.crunPath=this._locateCrun(),fs.ensureDirSync(this.binDir)}_locateCrun(){if(fs.existsSync(CRUN_LOCAL_BIN))try{if(fs.statSync(CRUN_LOCAL_BIN).size>1e5)return CRUN_LOCAL_BIN}catch(t){}try{const t=spawnSync("which",["crun"],{stdio:"pipe"});if(0===t.status)return t.stdout.toString().trim()}catch(t){}return null}isAvailable(){return Boolean(this.crunPath&&fs.existsSync(this.crunPath))}getVersion(){if(!this.isAvailable())return null;try{const t=spawnSync(this.crunPath,["--version"],{stdio:"pipe"});if(0===t.status)return t.stdout.toString().split("\n")[0].replace(/^crun version\s+/,"").trim()}catch(t){}return"unknown"}async ensureCrun(){if(this.isAvailable())return this.crunPath;if("linux"!==process.platform)return null;const t="arm64"===process.arch?"arm64":"amd64",r=`linux-${t}`,n=CRUN_URLS[r];if(!n)throw new Error(`Unsupported architecture for static crun download: ${t}`);console.log(chalk.cyan(`\n[crun] Embedding official static OCI runtime (crun v1.14.4 ${t})...`)),console.log(chalk.dim(` Download: ${n}`)),console.log(chalk.dim(` Target: ${CRUN_LOCAL_BIN}`));const e=`${CRUN_LOCAL_BIN}.download`;fs.mkdirSync(this.binDir,{recursive:!0}),await this._downloadFile(n,e);const s=crypto.createHash("sha256").update(fs.readFileSync(e)).digest("hex");if(s!==CRUN_SHA256[r])throw fs.rmSync(e,{force:!0}),new Error(`crun checksum mismatch for ${r}: expected ${CRUN_SHA256[r]}, got ${s}`);fs.renameSync(e,CRUN_LOCAL_BIN),fs.chmodSync(CRUN_LOCAL_BIN,493),this.crunPath=CRUN_LOCAL_BIN;const c=this.getVersion();return console.log(chalk.green(`✓ [crun] Successfully installed embedded OCI runtime (version ${c})!\n`)),this.crunPath}_downloadFile(t,r){return new Promise((n,e)=>{const s=t=>{https.get(t,t=>{if(t.statusCode>=300&&t.statusCode<400&&t.headers.location)return s(t.headers.location);if(200!==t.statusCode)return e(new Error(`Failed to download crun: HTTP ${t.statusCode}`));const c=fs.createWriteStream(r);t.pipe(c),c.on("finish",()=>{c.close(),n()}),c.on("error",e)}).on("error",e)};s(t)})}create(t,r,n={}){const e=this.crunPath||"crun";return spawnSync(e,["create","--bundle",r,t],{stdio:"pipe"})}start(t){const r=this.crunPath||"crun";return spawnSync(r,["start",t],{stdio:"pipe"})}run(t,r,n="inherit"){const e=this.crunPath||"crun";return spawn(e,["run","--bundle",r,t],{detached:!0,stdio:n})}kill(t,r="SIGTERM"){const n=this.crunPath||"crun";return spawnSync(n,["kill",t,r],{stdio:"pipe"})}delete(t,r=!0){const n=this.crunPath||"crun",e=["delete"];return r&&e.push("--force"),e.push(t),spawnSync(n,e,{stdio:"pipe"})}state(t){const r=this.crunPath||"crun";try{const n=spawnSync(r,["state",t],{stdio:"pipe"});if(0===n.status)return JSON.parse(n.stdout.toString())}catch(t){}return null}exec(t,r=["/bin/sh"],n={}){const e=this.crunPath||"crun",s=["exec"];return n.tty&&s.push("-t"),n.interactive&&s.push("-i"),s.push(t,...r),spawn(e,s,{stdio:n.stdio||"inherit"})}}module.exports=CrunManager;
@@ -1,7 +0,0 @@
1
- /**
2
- * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
- * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
- * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
- * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
- */
7
- const{spawn,execSync}=require("child_process"),path=require("path"),chalk=require("chalk");let fs;try{fs=require("fs-extra")}catch(e){try{fs=require("/Users/ramansingh/ThinkNCollab-Shell-V2/node_modules/fs-extra")}catch(e){const t=require("fs");fs={...t,ensureDirSync:e=>t.mkdirSync(e,{recursive:!0}),readJsonSync:e=>JSON.parse(t.readFileSync(e,"utf8")),writeJsonSync:(e,s)=>t.writeFileSync(e,JSON.stringify(s,null,2),"utf8")}}}const os=require("os"),SandboxNetwork=require("./network"),SandboxStorage=require("./storage"),MacOSSandbox=require("./macos-sandbox"),CgroupsV2=require("./cgroups"),WindowsWSL2=require("./windows-wsl2"),SandboxSupervisor=require("./supervisor"),RuntimeManager=require("./runtime-manager"),CrunManager=require("./crun-manager"),OCIRuntimeSpec=require("./oci-runtime-spec"),ContainerMonitor=require("./container-monitor");let getMicroVMManager=null;try{getMicroVMManager=require("./microvm-manager").getMicroVMManager}catch(e){}let getDepStore=null;try{getDepStore=require("../dep-store").getDepStore}catch(e){}class SandboxEngine{constructor(e={}){if(this.baseDir=e.baseDir||path.join(os.homedir(),".tnc","sandboxes"),fs.ensureDirSync(this.baseDir),this.network=new SandboxNetwork,this.cgroups=new CgroupsV2,this.wsl2=new WindowsWSL2,this.crun=new CrunManager,this.supervisor=new SandboxSupervisor(this),!1!==e.autoCleanup)try{this.selfHealingCleanup()}catch(e){}}selfHealingCleanup(){const e={staleSocketsRemoved:0,stalePidsRemoved:0,deadContainersMarked:0},t=path.join(os.homedir(),".tnc","microvm.pid"),s=path.join(os.homedir(),".tnc","vmctl.sock");let r=!1;if(fs.existsSync(t))try{const e=parseInt(fs.readFileSync(t,"utf8").trim(),10);e&&(process.kill(e,0),r=!0)}catch(r){try{fs.removeSync(t),e.stalePidsRemoved++}catch(e){}if(fs.existsSync(s))try{fs.removeSync(s),e.staleSocketsRemoved++}catch(e){}}else if(fs.existsSync(s))try{fs.removeSync(s),e.staleSocketsRemoved++}catch(e){}if(fs.existsSync(this.baseDir))try{const t=fs.readdirSync(this.baseDir);for(const s of t){const t=path.join(this.baseDir,s,"meta.json");if(fs.existsSync(t))try{const s=fs.readJsonSync(t);if(s&&"RUNNING"===s.state){let n=!1;if("macos-microvm-vz"===s.isolationLevel||"linux-microvm-vz"===s.isolationLevel)r||(n=!0);else if(s.pid)try{process.kill(s.pid,0)}catch(e){n=!0}n&&(s.state="STOPPED",s.health="DEAD",s.pid=null,s.stoppedAt=(new Date).toISOString(),fs.writeJsonSync(t,s,{spaces:2}),e.deadContainersMarked++)}}catch(e){}}}catch(e){}return e}async _findCrun(){if(this.crun.isAvailable())return this.crun.crunPath;if("linux"===process.platform)try{return await this.crun.ensureCrun()}catch(e){}return null}getMetaPath(e){return path.join(this.baseDir,e,"meta.json")}loadMeta(e){const t=this.getMetaPath(e);if(fs.existsSync(t))try{return fs.readJsonSync(t)}catch(e){}return null}saveMeta(e,t){const s=this.getMetaPath(e);fs.ensureDirSync(path.dirname(s)),fs.writeJsonSync(s,t,{spaces:2})}async create(e,t={}){let s=t.cwd||process.cwd(),r=t.cmd||"node server.js",n=t.hostPort||null,o=t.containerPort||null,i=null;if(t.image)try{const e=new(require("./oci-puller"));if(i=e.inspect(t.image),i||(i=(await e.pull(t.image)).meta),i){if(i.rootfsDir&&fs.existsSync(i.rootfsDir)&&(s=i.rootfsDir),"node server.js"===r&&(i.entrypoint?.length||i.cmd?.length)){const e=i.entrypoint||[],t=i.cmd||[];r=[...e,...t].join(" ")}if(!o&&i.exposedPorts){const e=Object.keys(i.exposedPorts)[0];if(e){const t=parseInt(e.split("/")[0],10);isNaN(t)||(o=t,n||(n=t))}}}}catch(e){console.warn(`[TNC Engine] Create image resolution notice: ${e.message}`)}new SandboxStorage(e).initStorage(null,s);const a=path.join(this.baseDir,e,"bundle");fs.ensureDirSync(a);const c=OCIRuntimeSpec.generate({rootfsPath:s,cmd:Array.isArray(r)?r:r.split(" "),workingDir:s,env:Object.entries(t.env||{}).map(([e,t])=>`${e}=${t}`)});fs.writeJsonSync(path.join(a,"config.json"),c,{spaces:2});const l={id:e,name:t.name||e,image:t.image||null,state:"CREATED",pid:null,pgid:null,cmd:r,cwd:s,hostPort:n,containerPort:o,memoryLimit:t.memoryLimit||"2g",environment:t.env||{},ephemeral:t.ephemeral||!1,health:"STARTING",createdAt:(new Date).toISOString(),startedAt:null,frozenAt:null};return this.saveMeta(e,l),{success:!0,message:`Container "${e}" created.`,meta:l}}async start(e,t={}){let s=this.loadMeta(e);s&&"CREATED"===s.state&&(t={cmd:s.cmd,cwd:s.cwd,hostPort:s.hostPort,containerPort:s.containerPort,memoryLimit:s.memoryLimit,image:s.image,env:s.environment||{},name:s.name,...t});let{cmd:r="node server.js",cwd:n=process.cwd(),hostPort:o=null,containerPort:i=null,memoryLimit:a="2g",ephemeral:c=!1,nodeVersion:l=null}=t,u=s;if(u&&u.pid)try{process.kill(u.pid,0);let s=!1;return t.roomId&&u.roomId!==t.roomId&&(u.roomId=t.roomId,s=!0),t.roomName&&u.roomName!==t.roomName&&(u.roomName=t.roomName,s=!0),t.userId&&u.userId!==t.userId&&(u.userId=t.userId,s=!0),t.userName&&u.userName!==t.userName&&(u.userName=t.userName,s=!0),s&&this.saveMeta(e,u),{success:!1,message:`Sandbox "${e}" is already running with PID ${u.pid}`,meta:u}}catch{}let d=null,m=null;if(t.image)try{const e=new(require("./oci-puller"));if(m=e.inspect(t.image),m||(m=(await e.pull(t.image)).meta),m){if(m.rootfsDir&&fs.existsSync(m.rootfsDir)&&(n=m.rootfsDir),"node server.js"===r&&(m.entrypoint?.length||m.cmd?.length)){const e=m.entrypoint||[],t=m.cmd||[];r=[...e,...t].join(" ")}if(!i&&m.exposedPorts){const e=Object.keys(m.exposedPorts)[0];if(e){const t=parseInt(e.split("/")[0],10);isNaN(t)||(i=t,o||(o=t))}}try{const e=new(require("./arch-shim")),t=m.platform?m.platform.split("/")[1]:"amd64",s=await e.getExecWrapper(t);s.wrapper&&s.wrapper.length>0&&(r=`${s.wrapper.join(" ")} ${r}`)}catch(e){}}}catch(e){console.warn(`[TNC Engine] Image resolution notice: ${e.message}`)}try{const e=l||t.runtime&&t.runtime.node;d=await RuntimeManager.ensureNodeRuntime(n,e)}catch(e){console.warn(`[TNC Runtime] Runtime resolution notice: ${e.message}`)}const p=d?d.binDir:null,h=new SandboxStorage(e);h.initStorage(null,n,d?.runtimeDir);const f="linux"===process.platform;let y,g="host-process",S=2048;const w="10.89.0."+(Math.floor(200*Math.random())+10),v=path.join(this.baseDir,e,"output.log");fs.ensureDirSync(path.dirname(v));const N=f?fs.openSync(v,"a"):null;if(f){this.network.setupBridge();const t=fs.existsSync(path.join(h.mergedDir,"bin","sh"))||fs.existsSync(path.join(h.mergedDir,"bin","bash")),s=p?`export PATH="/opt/node/bin:${p}:$PATH"; `:"",c=t?["-m","-p","-n","-u","-i","-f","chroot",h.mergedDir,"/bin/sh","-c",`mount -t proc proc /proc 2>/dev/null || true; ${s}cd /workspace && exec ${r}`]:["-m","-p","-n","-u","-i","-f","/bin/sh","-c",`${s}cd "${n}" && exec ${r}`],l=await this._findCrun();if(l)try{const s=path.join(this.baseDir,e,"bundle");fs.ensureDirSync(s);const o=t?h.mergedDir:n,i=OCIRuntimeSpec.generate({rootfsPath:o,cmd:Array.isArray(r)?r:r.split(" "),workingDir:t?"/workspace":n,mounts:[{destination:"/workspace",type:"bind",source:n,options:["rbind","rw"]}]});fs.writeJsonSync(path.join(s,"config.json"),i,{spaces:2});y={pid:new ContainerMonitor(e,path.join(this.baseDir,e)).launch({cmd:l,args:["run","--bundle",s,e],cwd:s,runtime:"crun"}).pid},g="linux-crun-oci"}catch(e){console.warn(`[TNC Engine] crun launch notice, falling back to unshare: ${e.message}`)}y||(y=spawn("unshare",c,{detached:!0,stdio:["ignore",N,N],env:{...process.env,PATH:p?`${p}:${process.env.PATH}`:process.env.PATH}}),g="linux-unshare"),this.network.attachNetwork(e,y.pid,w),o&&i&&this.network.mapPort(o,w,i);const u=this.cgroups.create(e,{memoryLimit:a,cpuPercent:80,pidsMax:500});u.success&&(this.cgroups.assignPid(e,y.pid),g="linux-unshare+cgroups2",S=Math.round(u.memBytes/1024/1024))}else if("darwin"===process.platform){if(getMicroVMManager){let s=null;try{const a=getMicroVMManager(),c=await a.getStatus();(c.available||c.kernelReady)&&(s=await a.start(e,{image:t.image,cmd:r,cwd:n,hostPort:o,containerPort:i,env:t.env||{},volumes:t.volumes||[],name:t.name,roomId:t.roomId,roomName:t.roomName,userId:t.userId,userName:t.userName}))}catch(e){if(t.image)throw e;console.warn(chalk.yellow(`[TNC Engine] MicroVM (${e.message}) — falling back to sandbox-exec`))}if(s&&s.success){const t=s.meta;return this.saveMeta(e,{...t,isolationLevel:"macos-microvm-vz"}),{success:!0,meta:t}}}const s=(new MacOSSandbox).spawnSandboxed(e,r,{cwd:n,hostPort:o,containerPort:i,memoryLimit:a,allowNetwork:!0,outFd:N,runtimeBinDir:p});y=s.child,g=s.isolationLevel,S=s.memoryLimitMB}else if("win32"===process.platform){const t=this.wsl2.spawnInWSL2(e,r,{cwd:n,hostPort:o,containerPort:i,memoryLimit:a,outFd:N,runtimeBinDir:p});if(t.success)y=t.child,g=t.isolationLevel,S=t.memoryLimitMB;else{const t=process.env.PATH||process.env.Path||"",s=p?`${p};${t}`:t;y=spawn("cmd.exe",["/s","/c",r],{cwd:n,detached:!0,stdio:["ignore",N,N],env:{...process.env,TNC_SANDBOX_ID:e,TNC_PLATFORM:"host-process-win32",PORT:String(o||i||"3000"),PATH:s,Path:s}}),g="host-process"}}else y=spawn("/bin/sh",["-c",r],{cwd:n,detached:!0,stdio:["ignore",N,N],env:{...process.env,TNC_SANDBOX_ID:e,PORT:String(o||i||3e3),PATH:p?`${p}:${process.env.PATH}`:process.env.PATH}}),g="host-process";y.unref();const D=y.pid;if(s=this.loadMeta(e),u={id:e,name:t.name||s&&s.name||e,image:t.image||s&&s.image||null,roomId:t.roomId||s&&s.roomId||process.env.THINKNCOLLAB_ROOM_ID||null,roomName:t.roomName||s&&s.roomName||process.env.THINKNCOLLAB_ROOM_NAME||null,userId:t.userId||s&&s.userId||process.env.THINKNCOLLAB_USER_ID||null,userName:t.userName||s&&s.userName||process.env.THINKNCOLLAB_USER_NAME||null,state:"RUNNING",pid:y.pid,pgid:D,cmd:r,cwd:n,hostPort:o,containerPort:i,containerIp:w,memoryLimit:a,isolationLevel:g,memoryLimitMB:S,ephemeral:c,health:"HEALTHY",healthPath:t.healthPath||"/health",cpuPercent:0,memoryMB:0,restartCount:s&&s.restartCount||0,runtime:d?{node:d.version,binDir:d.binDir,nodePath:d.nodePath,isHermetic:!0}:{node:process.versions.node,isHost:!0},startedAt:(new Date).toISOString(),frozenAt:null},this.saveMeta(e,u),this.supervisor.watch(e,u),getDepStore)try{const t=new(require("../dependency-tracker"))(n).loadConfig();getDepStore().recordFromConfig(t,{sandboxId:e})}catch(e){}const x=t.standalone||t.noTunnel||"true"===process.env.TNC_STANDALONE||"true"===process.env.THINKNCOLLAB_STANDALONE;return o&&!x&&this.startTunnelDaemon(e,o,t.serverUrl,t.userId),{success:!0,message:`Sandbox "${e}" started successfully.`,meta:u}}async freeze(e){const t=this.loadMeta(e);if(!t)return{success:!1,message:`Sandbox "${e}" does not exist.`};if("FROZEN"===t.state)return{success:!0,message:`Sandbox "${e}" is already frozen.`,meta:t};if(this.supervisor.unwatch(e),this.stopTunnelDaemon(e),"macos-microvm-vz"===t.isolationLevel||"linux-microvm-vz"===t.isolationLevel){if(getMicroVMManager)try{await getMicroVMManager().stop(e)}catch(e){}return t.state="STOPPED",t.frozenPid=t.pid,t.pid=null,t.frozenAt=(new Date).toISOString(),this.saveMeta(e,t),{success:!0,message:`Container "${e}" stopped. RAM released to 0 MB.`,meta:t}}if(t.pid){const s=t.pgid||t.pid;if("linux-crun-oci"===t.isolationLevel)try{new(require("./container-monitor"))(e,path.join(this.baseDir,e)).stop()}catch(e){}"linux"===process.platform&&(this.cgroups.killAll(e),await new Promise(e=>setTimeout(e,500))),"win32"===process.platform&&(this.wsl2.killSandbox(t.pid,e),this.wsl2.freezeCleanup(t),await new Promise(e=>setTimeout(e,500)));try{process.kill(-s,"SIGTERM")}catch(e){try{process.kill(t.pid,"SIGTERM")}catch(e){}}await new Promise(e=>setTimeout(e,1500));try{process.kill(-s,0);try{process.kill(-s,"SIGKILL")}catch(e){}}catch(e){}if("win32"!==process.platform)try{execSync(`kill -9 -${s} 2>/dev/null || true`,{stdio:"ignore"})}catch(e){}if(t.hostPort&&t.containerIp&&t.containerPort&&this.network.unmapPort(t.hostPort,t.containerIp,t.containerPort),"darwin"===process.platform&&t.hostPort&&t.containerPort)try{(new(require("./macos-sandbox"))).removePortForward(t.hostPort,t.containerPort)}catch(e){}}return t.state="FROZEN",t.frozenPid=t.pid,t.pid=null,t.frozenAt=(new Date).toISOString(),this.saveMeta(e,t),this.supervisor.unwatch(e),{success:!0,message:`Sandbox "${e}" frozen. RAM dropped to 0 MB. Filesystem preserved.`,meta:t}}async stop(e){return this.freeze(e)}async resume(e){const t=this.loadMeta(e);if(!t)return{success:!1,message:`Sandbox "${e}" does not exist.`};if("RUNNING"===t.state&&t.pid)try{return process.kill(t.pid,0),{success:!0,message:`Sandbox "${e}" is already running.`,meta:t}}catch(e){}return await this.start(e,{cmd:t.cmd,cwd:t.cwd,hostPort:t.hostPort,containerPort:t.containerPort,memoryLimit:t.memoryLimit,ephemeral:t.ephemeral,name:t.name,roomId:t.roomId,roomName:t.roomName,userId:t.userId,userName:t.userName,nodeVersion:t.runtime&&t.runtime.node||null})}async destroy(e){return this.supervisor.unwatch(e),await this.freeze(e),"linux"===process.platform&&this.cgroups.destroy(e),new SandboxStorage(e).purge(),{success:!0,message:`Sandbox "${e}" destroyed and cleaned up.`}}list(){const e=[],t=new Set;let s=null;const r=()=>{if(null!==s)return s;s=new Set;try{const e=path.join(os.homedir(),".tnc","vmctl.sock");if(!fs.existsSync(e))return s;const t=['const net=require("net"),sock=new net.Socket();','let d="";',"sock.setTimeout(3000);",'sock.on("data",b=>{d+=b;if(d.includes("\\n")){process.stdout.write(d);sock.destroy();}});','sock.on("timeout",()=>{process.stdout.write("{}\\n");sock.destroy();});','sock.on("error",()=>process.stdout.write("{}\\n"));',`sock.connect(${JSON.stringify(e)});`,'sock.write(\'{"cmd":"ps"}\\n\');'].join(""),{execFileSync:r}=require("child_process"),n=r(process.execPath,["-e",t],{timeout:4e3,encoding:"utf8"}).trim().split("\n").pop(),o=JSON.parse(n);if(o&&o.ok&&Array.isArray(o.containers))for(const e of o.containers)e.id&&!1!==e.running&&s.add(e.id)}catch(e){}return s};if(fs.existsSync(this.baseDir)){const s=fs.readdirSync(this.baseDir);for(const n of s){const s=this.loadMeta(n);if(s){if(t.add(n),"RUNNING"===s.state&&s.pid)if("macos-microvm-vz"===s.isolationLevel||"linux-microvm-vz"===s.isolationLevel){const e=path.join(os.homedir(),".tnc","vmctl.sock");if(fs.existsSync(e))if(r().has(n))s.health="HEALTHY";else{s.state="DEAD",s.health="DEAD";try{const e=path.join(this.baseDir,n,"meta.json");if(fs.existsSync(e)){const t=fs.readJsonSync(e);t.state="DEAD",t.health="DEAD",fs.writeJsonSync(e,t,{spaces:2})}}catch(e){}}else s.state="DEAD",s.health="DEAD"}else try{process.kill(s.pid,0);const e=this.supervisor.gatherTelemetry(s.pid);s.cpuPercent=e.cpu,s.memoryMB=e.memory,s.health=s.health||"HEALTHY"}catch{s.state="DEAD",s.health="DEAD"}e.push(s)}}}const n=path.join(os.homedir(),".tnc","state");if(fs.existsSync(n))try{const s=r(),o=fs.readdirSync(n);for(const r of o){if(!r.endsWith(".json"))continue;const o=path.basename(r,".json");if(!t.has(o))try{const i=fs.readJsonSync(path.join(n,r));if(i){const r=s.size>0?s.has(o):Boolean(i.running);e.push({id:i.id||o,name:i.id||o,image:"oci-container",state:r?"RUNNING":"DEAD",pid:i.pid,vmIp:i.ip,isolationLevel:"macos-microvm-vz",health:r?"HEALTHY":"DEAD",startedAt:(new Date).toISOString()}),t.add(o)}}catch(e){}}}catch(e){}return e}startTunnelDaemon(e,t,s=null,r=null){try{this.stopTunnelDaemon(e);const n=path.join(__dirname,"tunnel-daemon.js"),o=s||process.env.THINKNCOLLAB_SERVER||"https://thinkncollab.com",i=r||process.env.THINKNCOLLAB_USER_ID||"",a=spawn(process.execPath,[n,e,String(t),o,i],{detached:!0,stdio:"ignore"});a.unref();const c=path.join(this.baseDir,e,"tunnel.pid");fs.writeFileSync(c,String(a.pid))}catch(e){}}stopTunnelDaemon(e){try{const t=path.join(this.baseDir,e,"tunnel.pid");if(fs.existsSync(t)){const e=parseInt(fs.readFileSync(t,"utf8").trim(),10);if(e&&!isNaN(e))try{process.kill(e,"SIGTERM")}catch(e){}try{fs.removeSync(t)}catch(e){}}}catch(e){}}async kill(e,t="SIGKILL"){const s=this.loadMeta(e);if(!s)return{success:!1,message:`Container "${e}" does not exist.`};if(this.supervisor.unwatch(e),this.stopTunnelDaemon(e),"linux-crun-oci"===s.isolationLevel&&this.crun.isAvailable())try{this.crun.kill(e,t)}catch(e){}if(s.pid){const e=s.pgid||s.pid;try{process.kill(-e,t)}catch(e){try{process.kill(s.pid,t)}catch(e){}}if("win32"!==process.platform)try{execSync(`kill -9 -${e} 2>/dev/null || true`,{stdio:"ignore"})}catch(e){}}return s.state="STOPPED",s.exitCode=137,s.stoppedAt=(new Date).toISOString(),this.saveMeta(e,s),{success:!0,message:`Container "${e}" killed (${t}).`,meta:s}}logs(e,t={}){const s=path.join(this.baseDir,e,"output.log");if(!fs.existsSync(s))return"";try{const e=fs.readFileSync(s,"utf8");return t.tail&&"number"==typeof t.tail?e.trim().split("\n").slice(-t.tail).join("\n"):e}catch(e){return""}}async exec(e,t=["/bin/sh"],s={}){const r=this.loadMeta(e);if(!r||"RUNNING"!==r.state)throw new Error(`Container "${e}" is not running.`);const{spawnSync:n}=require("child_process"),o=s.cwd||r.cwd||process.cwd(),i={...process.env,...r.environment,...s.env,TNC_CONTAINER_ID:e};if("macos-microvm-vz"===r.isolationLevel||"linux-microvm-vz"===r.isolationLevel){const{getMicroVMManager:s}=require("./microvm-manager"),r=s(),n=(Array.isArray(t)?t:t.split(" ")).map(e=>e.includes(" ")||e.includes("\t")||e.includes(";")||e.includes('"')||e.includes("$")?`"${e.replace(/(["\\$`])/g,"\\$1")}"`:e).join(" "),o=await r.exec(e,n);return o&&o.ok?{status:0,stdout:o.output||"",stderr:""}:{status:1,stdout:"",stderr:o?.error||"Exec error"}}if("linux-crun-oci"===r.isolationLevel&&this.crun.isAvailable())return this.crun.exec(e,Array.isArray(t)?t:t.split(" "),s);if("linux"===process.platform&&r.pid)try{const e=Array.isArray(t)?t:t.split(" ");return n("nsenter",["-t",String(r.pid),"-m","-u","-i","-n","-p","--",...e],{stdio:s.stdio||"pipe",env:i,cwd:o,encoding:"utf8"})}catch(e){}return n("/bin/sh",["-c",Array.isArray(t)?t.join(" "):t],{stdio:s.stdio||"pipe",cwd:o,env:i,encoding:"utf8"})}inspect(e){const t=this.loadMeta(e);if(!t)return null;if("RUNNING"===t.state&&t.pid)try{process.kill(t.pid,0);const e=this.supervisor.gatherTelemetry(t.pid);t.cpuPercent=e.cpu,t.memoryMB=e.memory,t.isAlive=!0}catch{t.isAlive=!1,t.state="DEAD"}return t}async prune(){const e=this.list(),t=[];for(const s of e)"RUNNING"!==s.state&&(await this.destroy(s.id),t.push(s.id));return{prunedCount:t.length,pruned:t}}}module.exports=SandboxEngine;
@@ -1,7 +0,0 @@
1
- /**
2
- * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
- * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
- * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
- * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
- */
7
- const path=require("path"),fs=require("fs-extra"),os=require("os"),{execSync}=require("child_process"),DependencyTracker=require("../dependency-tracker");class ImageManager{constructor(e=null){this.baseDir=e||path.join(os.homedir(),".tnc","images"),fs.ensureDirSync(this.baseDir),this.manifestFile=path.join(this.baseDir,"images.json"),this.initStore()}initStore(){fs.existsSync(this.manifestFile)||fs.writeJsonSync(this.manifestFile,{})}loadManifest(){try{return fs.readJsonSync(this.manifestFile)}catch{return{}}}saveManifest(e){fs.writeJsonSync(this.manifestFile,e,{spaces:2})}async build(e="latest",t=process.cwd(),s={}){const n=new DependencyTracker(t),i=await n.scanAndRecord()||n.loadConfig(),r="img-"+Math.random().toString(36).substring(2,10),a=e.includes(":")?e:`${i.project||"app"}:${e}`,o=path.join(this.baseDir,r);fs.ensureDirSync(o),fs.writeJsonSync(path.join(o,"blueprint.json"),i);const c={id:r,tag:a,project:i.project,runtimes:i.runtime?.runtimes||[i.runtime?.primary||"node"],dependenciesCount:Object.values(i.dependencies||{}).reduce((e,t)=>e+(t?.length||0),0),sizeMB:this.calculateDirSize(t),createdAt:(new Date).toISOString(),author:s.author||os.userInfo().username,entrypoint:i.scripts?.start||i.scripts?.dev||"node server.js",environment:i.environment||{}};fs.writeJsonSync(path.join(o,"meta.json"),c);const d=this.loadManifest();return d[a]=c,d[r]=c,this.saveManifest(d),{success:!0,imageId:r,tag:a,meta:c}}list(){const e=this.loadManifest(),t=new Map;for(const[s,n]of Object.entries(e))t.has(n.id)||t.set(n.id,n);return Array.from(t.values())}inspect(e){return this.loadManifest()[e]||null}tag(e,t){const s=this.loadManifest(),n=s[e];if(!n)throw new Error(`Image "${e}" not found.`);const i={...n,tag:t,updatedAt:(new Date).toISOString()};return s[t]=i,this.saveManifest(s),i}remove(e,t=!1){const s=this.loadManifest(),n=s[e];if(!n)throw new Error(`Image "${e}" not found.`);const i=n.id;for(const[t,n]of Object.entries(s))n.id!==i&&t!==e||delete s[t];this.saveManifest(s);const r=path.join(this.baseDir,i);return fs.existsSync(r)&&fs.removeSync(r),{success:!0,message:`Image "${e}" removed.`}}save(e,t){const s=this.inspect(e);if(!s)throw new Error(`Image "${e}" not found.`);path.join(this.baseDir,s.id);const n=path.resolve(t||`${s.tag.replace(/[:/]/g,"-")}.tar.gz`);return execSync(`tar -czf "${n}" -C "${this.baseDir}" "${s.id}"`),{success:!0,file:n,size:(fs.statSync(n).size/1048576).toFixed(2)+" MB"}}load(e){const t=path.resolve(e);if(!fs.existsSync(t))throw new Error(`File "${e}" not found.`);execSync(`tar -xzf "${t}" -C "${this.baseDir}"`);const s=fs.readdirSync(this.baseDir),n=this.loadManifest();let i=null;for(const e of s){const t=path.join(this.baseDir,e,"meta.json");if(fs.existsSync(t))try{const e=fs.readJsonSync(t);n[e.tag]=e,n[e.id]=e,i=e}catch(e){}}return this.saveManifest(n),{success:!0,meta:i}}prune(){this.loadManifest();let e=0;const t=new Set(this.list().map(e=>e.id)),s=fs.readdirSync(this.baseDir).filter(e=>e.startsWith("img-"));for(const n of s)t.has(n)||(fs.removeSync(path.join(this.baseDir,n)),e++);return{success:!0,deletedCount:e}}calculateDirSize(e){try{const t=execSync(`du -sm "${e}" 2>/dev/null || echo "10"`,{encoding:"utf8"}).trim();return parseInt(t.split(/\s+/)[0],10)||10}catch{return 10}}}module.exports=ImageManager;
@@ -1,7 +0,0 @@
1
- /**
2
- * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
- * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
- * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
- * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
- */
7
- const path=require("path"),os=require("os"),fs=require("fs-extra"),crypto=require("crypto"),DEFAULT_OCI_DIR=path.join(os.homedir(),".tnc","oci");class ImageStore{constructor(e=DEFAULT_OCI_DIR){this.baseDir=e,this.blobsDir=path.join(this.baseDir,"blobs","sha256"),this.rootfsBaseDir=path.join(this.baseDir,"rootfs"),this.dbPath=path.join(this.baseDir,"index.db"),fs.ensureDirSync(this.blobsDir),fs.ensureDirSync(this.rootfsBaseDir),this.db=this._initDatabase()}_initDatabase(){let e=null;try{const{DatabaseSync:t}=require("node:sqlite");return e=new t(this.dbPath),e.exec("\n CREATE TABLE IF NOT EXISTS images (\n id TEXT PRIMARY KEY,\n repo TEXT NOT NULL,\n tag TEXT NOT NULL,\n digest TEXT,\n platform TEXT,\n size_bytes INTEGER DEFAULT 0,\n layers_count INTEGER DEFAULT 0,\n pulled_at TEXT,\n last_used_at TEXT,\n rootfs_dir TEXT,\n config_json TEXT\n );\n\n CREATE UNIQUE INDEX IF NOT EXISTS idx_repo_tag ON images(repo, tag);\n\n CREATE TABLE IF NOT EXISTS blobs (\n digest TEXT PRIMARY KEY,\n size_bytes INTEGER DEFAULT 0,\n media_type TEXT,\n created_at TEXT\n );\n\n CREATE TABLE IF NOT EXISTS image_blobs (\n image_id TEXT NOT NULL,\n blob_digest TEXT NOT NULL,\n PRIMARY KEY (image_id, blob_digest)\n );\n "),e}catch(e){return this._initJsonFallback()}}_initJsonFallback(){const e=path.join(this.baseDir,"index.json");let t={images:{},blobs:{},image_blobs:[]};if(fs.existsSync(e))try{t=fs.readJsonSync(e)}catch(e){}const s=()=>{try{fs.writeJsonSync(e,t,{spaces:2})}catch(e){}};return{isFallback:!0,exec:()=>{},prepare:e=>({run:(...i)=>{if(e.includes("INSERT INTO blobs")||e.includes("INSERT OR REPLACE INTO blobs")){const[e,r,a,o]=i;t.blobs[e]={digest:e,size_bytes:r,media_type:a,created_at:o},s()}else if(e.includes("INSERT INTO images")||e.includes("INSERT OR REPLACE INTO images")){const[e,r,a,o,n,l,c,d,b,g,E]=i;t.images[`${r}:${a}`]={id:e,repo:r,tag:a,digest:o,platform:n,size_bytes:l,layers_count:c,pulled_at:d,last_used_at:b,rootfs_dir:g,config_json:E},s()}else if(e.includes("INSERT OR IGNORE INTO image_blobs")){const[e,r]=i;t.image_blobs.some(t=>t.image_id===e&&t.blob_digest===r)||(t.image_blobs.push({image_id:e,blob_digest:r}),s())}else if(e.includes("DELETE FROM images WHERE id = ?")){const[e]=i;for(const[s,i]of Object.entries(t.images))i.id===e&&delete t.images[s];t.image_blobs=t.image_blobs.filter(t=>t.image_id!==e),s()}else if(e.includes("DELETE FROM blobs WHERE digest = ?")){const[e]=i;delete t.blobs[e],s()}},all:(...s)=>{if(e.includes("SELECT * FROM images WHERE repo = ? AND tag = ?")){const[e,i]=s,r=t.images[`${e}:${i}`];return r?[r]:[]}if(e.includes("SELECT * FROM images WHERE id = ?")){const[e]=s,i=Object.values(t.images).find(t=>t.id===e);return i?[i]:[]}if(e.includes("SELECT * FROM images ORDER BY"))return Object.values(t.images);if(e.includes("SELECT * FROM blobs"))return Object.values(t.blobs);if(e.includes("SELECT DISTINCT blob_digest FROM image_blobs")){const e=new Set(t.image_blobs.map(e=>e.blob_digest));return Array.from(e).map(e=>({blob_digest:e}))}return[]}})}}getBlobPath(e){const t=e.replace(/^sha256:/,"");return path.join(this.blobsDir,t)}hasBlob(e){const t=this.getBlobPath(e);if(!fs.existsSync(t))return!1;try{return fs.statSync(t).size>0}catch(e){return!1}}putBlob(e,t,s="application/vnd.oci.image.layer.v1.tar+gzip"){const i=e.replace(/^sha256:/,""),r=path.join(this.blobsDir,i);let a=0;Buffer.isBuffer(t)?(fs.writeFileSync(r,t),a=t.length):"string"==typeof t&&fs.existsSync(t)&&(t!==r&&fs.copyFileSync(t,r),a=fs.statSync(r).size);try{this.db.prepare("\n INSERT OR REPLACE INTO blobs (digest, size_bytes, media_type, created_at)\n VALUES (?, ?, ?, ?)\n ").run(i,a,s,(new Date).toISOString())}catch(e){}return{digest:i,path:r,sizeBytes:a}}registerImage({repo:e,tag:t="latest",digest:s,platform:i,sizeBytes:r=0,layersCount:a=0,rootfsDir:o,config:n={},blobDigests:l=[]}){const c="img-"+crypto.createHash("sha256").update(`${e}:${t}:${s||Date.now()}`).digest("hex").slice(0,12),d=(new Date).toISOString(),b="string"==typeof n?n:JSON.stringify(n);try{this.db.prepare("\n INSERT OR REPLACE INTO images (\n id, repo, tag, digest, platform, size_bytes, layers_count, pulled_at, last_used_at, rootfs_dir, config_json\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ").run(c,e,t,s,i,r,a,d,d,o,b);const n=this.db.prepare("\n INSERT OR IGNORE INTO image_blobs (image_id, blob_digest)\n VALUES (?, ?)\n ");for(const e of l){const t=e.replace(/^sha256:/,"");n.run(c,t)}}catch(e){}return{id:c,repo:e,tag:t,digest:s,rootfsDir:o}}listImages(){try{return this.db.prepare("\n SELECT id, repo, tag, digest, platform, size_bytes, layers_count, pulled_at, last_used_at, rootfs_dir\n FROM images\n ORDER BY pulled_at DESC\n ").all().map(e=>({id:e.id,repo:e.repo,tag:e.tag,fullRef:`${e.repo}:${e.tag}`,digest:e.digest,platform:e.platform,sizeBytes:e.size_bytes,sizeMB:(e.size_bytes/1048576).toFixed(1),layersCount:e.layers_count,pulledAt:e.pulled_at,lastUsedAt:e.last_used_at,rootfsDir:e.rootfs_dir}))}catch(e){return[]}}getImage(e){if(!e)return null;let t,s;if(e.includes(":")){const i=e.split(":");t=i[0],s=i[1]}else t=e,s="latest";try{let i=this.db.prepare("\n SELECT * FROM images WHERE repo = ? AND tag = ?\n ").all(t,s);if(i.length||e.includes(":")||(i=this.db.prepare("\n SELECT * FROM images WHERE id LIKE ?\n ").all(`${e}%`)),i&&i.length>0){const e=i[0];let t={};try{t=JSON.parse(e.config_json)}catch(e){}return{id:e.id,repo:e.repo,tag:e.tag,fullRef:`${e.repo}:${e.tag}`,digest:e.digest,platform:e.platform,sizeBytes:e.size_bytes,sizeMB:(e.size_bytes/1048576).toFixed(1),layersCount:e.layers_count,pulledAt:e.pulled_at,lastUsedAt:e.last_used_at,rootfsDir:e.rootfs_dir,config:t}}}catch(e){}return null}tagImage(e,t){const s=this.getImage(e);if(!s)throw new Error(`Source image "${e}" not found.`);let i,r;if(t.includes(":")){const e=t.split(":");i=e[0],r=e[1]}else i=t,r="latest";return this.registerImage({repo:i,tag:r,digest:s.digest,platform:s.platform,sizeBytes:s.sizeBytes,layersCount:s.layersCount,rootfsDir:s.rootfsDir,config:s.config})}removeImage(e){const t=this.getImage(e);if(!t)throw new Error(`Image "${e}" not found locally.`);try{this.db.prepare("DELETE FROM images WHERE id = ?").run(t.id)}catch(e){}if(0===this.listImages().filter(e=>e.rootfsDir===t.rootfsDir).length&&t.rootfsDir&&fs.existsSync(t.rootfsDir))try{fs.removeSync(t.rootfsDir)}catch(e){}return{success:!0,removed:t.fullRef}}pruneBlobs(){let e=0,t=0;try{const s=this.db.prepare("SELECT DISTINCT blob_digest FROM image_blobs").all(),i=new Set(s.map(e=>e.blob_digest));if(fs.existsSync(this.blobsDir)){const s=fs.readdirSync(this.blobsDir);for(const r of s)if(!i.has(r)){const s=path.join(this.blobsDir,r);try{e+=fs.statSync(s).size,fs.unlinkSync(s),t++}catch(e){}}}const r=this.db.prepare("SELECT digest FROM blobs").all(),a=this.db.prepare("DELETE FROM blobs WHERE digest = ?");for(const e of r)i.has(e.digest)||a.run(e.digest)}catch(e){}return{reclaimedBytes:e,reclaimedMB:(e/1048576).toFixed(1),prunedCount:t}}}module.exports=ImageStore;
@@ -1,7 +0,0 @@
1
- /**
2
- * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
- * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
- * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
- * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
- */
7
- const SandboxEngine=require("./engine"),SandboxNetwork=require("./network"),SandboxStorage=require("./storage"),MacOSSandbox=require("./macos-sandbox"),CgroupsV2=require("./cgroups"),WindowsWSL2=require("./windows-wsl2"),CapsuleBuilder=require("./capsule-builder"),RuntimeManager=require("./runtime-manager");module.exports={SandboxEngine,SandboxNetwork,SandboxStorage,MacOSSandbox,CgroupsV2,WindowsWSL2,CapsuleBuilder,RuntimeManager};
@@ -1,7 +0,0 @@
1
- /**
2
- * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
- * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
- * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
- * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
- */
7
- const{execSync,spawn}=require("child_process");let fs;try{fs=require("fs-extra")}catch(e){try{fs=require("/Users/ramansingh/ThinkNCollab-Shell-V2/node_modules/fs-extra")}catch(e){const n=require("fs");fs={...n,ensureDirSync:e=>n.mkdirSync(e,{recursive:!0}),readJsonSync:e=>JSON.parse(n.readFileSync(e,"utf8")),writeJsonSync:(e,r)=>n.writeFileSync(e,JSON.stringify(r,null,2),"utf8"),removeSync:e=>n.rmSync(e,{recursive:!0,force:!0})}}}const path=require("path"),os=require("os"),PROFILES_DIR=path.join(os.homedir(),".tnc","profiles");class MacOSSandbox{constructor(){this.isMac="darwin"===process.platform,fs.ensureDirSync(PROFILES_DIR)}generateSeatbeltProfile(e,n={}){const{allowNetwork:r=!0,allowRead:t=!0,workspacePath:o=process.cwd(),allowedPorts:s=[]}=n,l=`\n(version 1)\n\n;; --- Default: deny everything ---\n(deny default)\n\n;; --- Process basics ---\n(allow process-exec)\n(allow process-fork)\n(allow signal)\n(allow mach-lookup)\n(allow sysctl-read)\n(allow system-socket)\n\n;; --- Allow reading system libraries & paths ---\n(allow file-read-metadata)\n(allow file-read*\n (subpath "/usr")\n (subpath "/lib")\n (subpath "/System")\n (subpath "/private")\n (subpath "/dev")\n (literal "/")\n (literal "/etc")\n (literal "/tmp")\n (literal "/var")\n (literal "/Users")\n (subpath "${os.homedir()}")\n)\n\n;; --- Allow Node.js / nvm / homebrew ---\n(allow file-read*\n (subpath "${os.homedir()}/.nvm")\n (subpath "${os.homedir()}/.node")\n (subpath "${os.homedir()}/.npm")\n (subpath "/opt/homebrew")\n (subpath "/usr/local")\n)\n\n;; --- Allow workspace directory (read+write) ---\n${t?`\n(allow file-read* (subpath "${o}"))\n(allow file-write* (subpath "${o}"))\n`:""}\n\n;; --- Allow TNC state directory ---\n(allow file-read* (subpath "${os.homedir()}/.tnc"))\n(allow file-write* (subpath "${os.homedir()}/.tnc"))\n\n;; --- Temp files ---\n(allow file-read* (subpath "/tmp"))\n(allow file-write* (subpath "/tmp"))\n(allow file-read* (subpath "/var/folders"))\n(allow file-write* (subpath "/var/folders"))\n\n;; --- Allow stdout/stderr ---\n(allow file-write-data\n (literal "/dev/stdout")\n (literal "/dev/stderr")\n (literal "/dev/null")\n)\n\n;; --- Networking ---\n${r?"\n(allow network-outbound)\n(allow network-inbound)\n(allow network-bind)\n":"\n;; Network blocked — no external access\n(deny network-outbound)\n(deny network-inbound)\n"}\n\n;; --- IPC basics ---\n(allow ipc-posix-shm)\n(allow ipc-posix-sem)\n`,a=path.join(PROFILES_DIR,`${e}.sb`);return fs.writeFileSync(a,l.trim()),a}buildResourceLimitScript(e,n=2048){return{prelude:`ulimit -v ${1024*n} 2>/dev/null || true; ulimit -n 4096 2>/dev/null || true`,cmd:e}}setupPortForward(e,n){if(!this.isMac)return!1;try{const r=`tnc_${e}_${n}`,t=`rdr pass on lo0 proto tcp from any to any port ${e} -> 127.0.0.1 port ${n}\n`,o=`/tmp/tnc_pf_${r}.conf`;return fs.writeFileSync(o,t),execSync(`sudo -n pfctl -a "tnc/${r}" -f ${o} 2>/dev/null || true`,{stdio:"ignore"}),execSync("sudo -n pfctl -e 2>/dev/null || true",{stdio:"ignore"}),{success:!0,rule:t.trim()}}catch(e){return{success:!1,reason:e.message}}}removePortForward(e,n){if(!this.isMac)return!1;try{return execSync(`sudo -n pfctl -a "tnc/tnc_${e}_${n}" -F all 2>/dev/null || true`,{stdio:"ignore"}),!0}catch{return!1}}spawnSandboxed(e,n,r={}){const{cwd:t=process.cwd(),hostPort:o=null,containerPort:s=null,memoryLimit:l="2g",allowNetwork:a=!0,outFd:i=null,runtimeBinDir:c=null}=r;if(!this.isMac)throw new Error("MacOSSandbox only runs on macOS/Darwin");let u=2048;if(l){const e=String(l).match(/^(\d+)([gGmM]?)$/);e&&(u=parseInt(e[1])*("g"===e[2].toLowerCase()?1024:1))}const d=this.generateSeatbeltProfile(e,{allowNetwork:a,workspacePath:t}),{prelude:p}=this.buildResourceLimitScript(n,u);o&&s&&o!==s&&this.setupPortForward(o,s);const w=path.join(os.homedir(),".tnc","sandboxes",e);fs.ensureDirSync(w);const h=path.join(w,"entrypoint.sh"),f=path.join(w,"output.log"),m=`#!/bin/bash\n${c?`export PATH=${JSON.stringify(c)}:"$PATH"\n`:""}${p}\ncd ${JSON.stringify(t)} 2>/dev/null || true\nexec /bin/bash -c ${JSON.stringify(n)} >> ${JSON.stringify(f)} 2>&1\n`;fs.writeFileSync(h,m,{mode:493});const b=spawn("sandbox-exec",["-f",d,"/bin/bash",h],{cwd:t,detached:!0,stdio:["ignore","ignore","ignore"],env:{...process.env,TNC_SANDBOX_ID:e,TNC_PLATFORM:"macos-seatbelt",PORT:String(o||s||3e3),HOME:os.homedir(),PATH:c?`${c}:${process.env.PATH}`:process.env.PATH}});return b.unref(),{child:b,profilePath:d,isolationLevel:"macos-seatbelt",memoryLimitMB:u}}getMemoryUsage(e){if(!e)return 0;try{const n=execSync(`ps -o rss= -p ${e} 2>/dev/null || echo 0`,{encoding:"utf8"}).trim();return Math.round(parseInt(n||"0")/1024)}catch{return 0}}cleanupProfile(e){const n=path.join(PROFILES_DIR,`${e}.sb`);try{fs.removeSync(n)}catch(e){}}}module.exports=MacOSSandbox;