# NEOME Hello Connect an LLM's local workspace to the user's existing NEOME browser page. This page describes pairing and the client lifecycle. Read https://gpt.neome.com/hello/world before creating, importing, exporting or submitting a scene. ## Follow the user's requested outcome - **Create and display:** build the scene, wait for its successful render, and verify the result. Publication is a separate action. - **Use an existing pack:** inspect that pack's manifest, README and catalog; select the exact asset entry instead of loading a whole showroom. - **Submit or publish:** use the existing client's submission command and wait for a successful saved result. - The user-supplied UID authorizes the connection. The encrypted handshake connects automatically; do not ask for a fingerprint comparison or send a confirmation command. ## 1. Start the workspace client Save the complete JavaScript code block below as `client.cjs`. Use the user's 32-character routing UID and the local directory containing this task's files. Choose a task-specific workspace rather than a filesystem root or unrelated directory. ```bash node --experimental-websocket client.cjs --uid USER_UID --root /path/to/workspace ``` The client connects to `wss://ws.neome.com/workspace` by default. An older saved client can use the same relay by adding `--url wss://ws.neome.com/workspace`. Use the installed Node binary. Node 20.10+ needs the experimental WebSocket flag; Node 22+ provides the native WebSocket API. No npm dependencies are required. If that installed runtime lacks native WebSocket support, report the limitation; do not install another runtime or substitute a new client. Keep stdin interactive (`tty:true` for exec tools). Retain the process/session handle so later commands reach this same process. Do not close stdin, terminate the client after one response, or start a second connection for scene feedback. ## 2. Connect automatically The user-supplied UID routes the client to the browser. The browser and current client complete their encrypted handshake and activate the workspace automatically. No manual fingerprint comparison or `confirm` command is needed. Wait for: ```text Workspace connected. Explorer updates are active. ``` Only then perform connected scene, speech, capture or submission operations. Treat the UID as access to the selected workspace; share it only with the intended client. If the browser says a client update is required, use the current client below and reload the NEOME page once. ## 3. Continue through the same process The browser can read files beneath the selected workspace root; browser file access is read-only. Author and edit files with your normal local file tools. CPU, memory and bandwidth monitoring and tests are included in the client. Give short spoken updates when starting a meaningful step, using the browser's built-in TTS: ```text tts {"text":"I'm building your world now."} ``` Describe the actual action. Do not announce success before its corresponding acknowledgement. If speech is blocked or unavailable, continue the task with text updates; do not make repeated speech attempts the main task. Optional local voice generation: https://gpt.neome.com/hello/voice Before asset creation, read https://gpt.neome.com/hello/world. It defines supported construction calls, scene formats, hosted-pack placement, texture dependencies, capture, export and submission. Connection success does not prove scene validity; preview success does not prove publishability. ## Reconnection and page lifecycle Keep the original client running across turns. On `WORKSPACE_RECONNECTING`, pause connected operations and allow its automatic retry. A temporary transport interruption reconnects to the same approved browser identity automatically, without a confirmation command. Resume only after: ```text Workspace reconnected securely. Explorer updates are active. ``` For scene operations, also wait for a successful render of the current scene after reconnecting. Do not restart the client to address temporary WebSocket loss. Routing credentials and trust stay in memory in this browser page and client process. A full page load/reload creates a new routing UID and requires connecting with that UID. Temporary transport interruption and tab switching retain the page UID and approved identity. The UID belongs to the page instance and has no time-based expiry. `invalid_hello` means the relay rejected the hello; it is not evidence that the UID expired. The unchanged client retains its credentials and retries. An explicit Disconnect ends the session. Page lifecycle events must not send `session_disconnect` or deliberately close the socket. Destroying the page may cause the browser to close transport; reliable client termination on tab closure requires relay-side handling. A final `WORKSPACE_DISCONNECTED` or `workspace_disconnected` feedback event ends connected work. Do not claim continued access after that event. ## Client source Save the complete code block as `client.cjs`. ```javascript #!/usr/bin/env node // Node.js 20 with --experimental-websocket, or Node.js 22+. Built-in WebSocket and filesystem APIs; no npm dependencies. 'use strict'; const fs=require('node:fs'),fsp=fs.promises,path=require('node:path'),os=require('node:os'); // Local event bridge: the WSS handler publishes; --wait blocks on fs.watch. async function privateInbox(workspace,session,create){ if(!/^[A-Za-z0-9_-]{32}$/.test(session))throw Error('Use the 32-character routing ID for --uid in wait mode.'); let directory=process.env.NEOME_CONNECT_INBOX_ROOT||workspace; for(const part of ['.neome-connect',session,'inbox']){ directory=path.join(directory,part); if(create){try{await fsp.mkdir(directory,{mode:0o700});}catch(error){if(error.code!=='EEXIST')throw error;}} const stat=await fsp.lstat(directory);if(!stat.isDirectory()||stat.isSymbolicLink())throw Error('Unsafe event directory'); } return directory; } async function waitForFeedback(){ const args={root:process.cwd(),uid:'',timeout:'60'}; for(let i=2;i600)throw Error('Wait timeout must be 0 (indefinite) or 1–600 seconds.'); const workspace=await fsp.realpath(args.root),directory=await privateInbox(workspace,args.uid.split('.')[0],false); await new Promise((resolve,reject)=>{ let watcher,timer,busy=false,again=false,done=false; function finish(error,event){if(done)return;done=true;clearTimeout(timer);watcher?.close();if(error){reject(error);return;}process.stdout.write(JSON.stringify(event)+'\n',resolve);} async function check(){if(done)return;if(busy){again=true;return;}busy=true; try{do{again=false;const stoppedFile=path.join(directory,'.disconnected.json');try{const stat=await fsp.lstat(stoppedFile);if(!stat.isFile()||stat.isSymbolicLink()||stat.size>4096)throw Error('Invalid disconnect event');const event=JSON.parse(await fsp.readFile(stoppedFile,'utf8'));if(event.type!=='workspace_disconnected')throw Error('Invalid disconnect event');finish(null,event);return;}catch(error){if(error.code!=='ENOENT')throw error;}const names=(await fsp.readdir(directory)).filter(n=>/^\d{16}-[a-f0-9]{16}\.json$/.test(n)).sort();for(const name of names){ const file=path.join(directory,name);let bytes; try{const stat=await fsp.lstat(file);if(!stat.isFile()||stat.isSymbolicLink()||stat.size>67112960)throw Error('Invalid inbox event');bytes=await fsp.readFile(file);} catch(error){if(error.code==='ENOENT')continue;throw error;} const event=JSON.parse(bytes);if(event.type==='world_render'||event.type==='world_step'){if(!Number.isSafeInteger(event.revision)||typeof event.ok!=='boolean')throw Error('Invalid world feedback');}else if(event.type==='world_submit'){if(typeof event.requestId!=='string'||!Number.isSafeInteger(event.revision)||typeof event.ok!=='boolean')throw Error('Invalid submission feedback');}else throw Error('Invalid inbox event'); if(done)return;clearTimeout(timer);await fsp.unlink(file);finish(null,event);return; }}while(again&&!done);}catch(error){finish(error);}finally{busy=false;} } watcher=fs.watch(directory,()=>{check();});watcher.on('error',error=>finish(error)); if(seconds>0)timer=setTimeout(()=>finish(null,{type:'timeout',waited_seconds:seconds}),seconds*1000); check(); }); } if(process.argv.includes('--wait')){ waitForFeedback().catch(error=>{console.error('Wait failed: '+error.message);process.exitCode=1;}); return; } const options={uid:'',root:process.cwd(),url:'wss://ws.neome.com/workspace',name:'Workspace'}; for(let i=2;iflag!=='--no-experimental-websocket'); const child=spawn(process.execPath,[...flags,'--experimental-websocket',...process.argv.slice(1)],{stdio:'inherit'}); for(const signal of ['SIGINT','SIGTERM','SIGHUP'])process.on(signal,()=>{child.kill(signal);}); child.on('error',error=>{console.error('Unable to start the installed Node with native WebSocket: '+error.message);process.exitCode=1;}); child.on('exit',(code,signal)=>{process.exitCode=code??({SIGINT:130,SIGTERM:143,SIGHUP:129}[signal]||1);}); return; } if(typeof WebSocket!=='function')throw Error('This installed Node build lacks native WebSocket even with --experimental-websocket. Node 20.10+ supports the flag; report this runtime limitation without installing another Node build or npm package.'); const url=new URL(options.url);if(url.protocol!=='wss:'&&!(url.protocol==='ws:'&&['localhost','127.0.0.1','[::1]'].includes(url.hostname)))throw Error('Use WSS, or WS on localhost for tests.'); // Noise_XX_25519_AESGCM_SHA256, Noise revision 34. // Shared verbatim by the browser and Node client. Uses native Web Crypto only. // Reviewed against published Cacophony vectors; not an independent security audit. const subtle=globalThis.crypto.subtle,enc=new TextEncoder(),dec=new TextDecoder('utf-8',{fatal:true}); const EMPTY=new Uint8Array(0),NOISE_NAME='Noise_XX_25519_AESGCM_SHA256'; const concat=(...parts)=>{const out=new Uint8Array(parts.reduce((n,p)=>n+p.length,0));let at=0;for(const p of parts){out.set(p,at);at+=p.length;}return out;}; const b64=bytes=>{let text='';for(let i=0;i{if(typeof text!=='string'||text.length>1500000||!/^[A-Za-z0-9_-]*$/.test(text)||text.length%4===1)throw Error('Invalid encoding');return Uint8Array.from(atob(text.replace(/-/g,'+').replace(/_/g,'/')),c=>c.charCodeAt(0));}; const hex=bytes=>Array.from(bytes,b=>b.toString(16).padStart(2,'0')).join(''); const hash=async bytes=>new Uint8Array(await subtle.digest('SHA-256',bytes)); async function hmac(key,bytes){const k=await subtle.importKey('raw',key,{name:'HMAC',hash:'SHA-256'},false,['sign']);return new Uint8Array(await subtle.sign('HMAC',k,bytes));} async function noiseHKDF(ck,ikm){const tmp=await hmac(ck,ikm),a=await hmac(tmp,new Uint8Array([1])),b=await hmac(tmp,concat(a,new Uint8Array([2])));tmp.fill(0);return [a,b];} async function keypair(){const k=await subtle.generateKey({name:'X25519'},false,['deriveBits']);return {privateKey:k.privateKey,publicBytes:new Uint8Array(await subtle.exportKey('raw',k.publicKey))};} async function dh(local,remote){if(remote.length!==32)throw Error('Invalid public key');const key=await subtle.importKey('raw',remote,{name:'X25519'},false,[]);const out=new Uint8Array(await subtle.deriveBits({name:'X25519',public:key},local.privateKey,256));if(out.every(b=>b===0))throw Error('Invalid DH result');return out;} class NoiseCipher { constructor(key=null){this.key=key;this.n=0n;} static async fromBytes(bytes){return new NoiseCipher(await subtle.importKey('raw',bytes,{name:'AES-GCM'},false,['encrypt','decrypt']));} async crypt(bytes,ad,decrypt=false){if(!this.key)return bytes;if(this.n>=(1n<<64n)-1n)throw Error('Nonce limit reached');const iv=new Uint8Array(12);new DataView(iv.buffer).setBigUint64(4,this.n,false);const out=new Uint8Array(await subtle[decrypt?'decrypt':'encrypt']({name:'AES-GCM',iv,additionalData:ad,tagLength:128},this.key,bytes));this.n++;return out;} } class NoiseXX { constructor(initiator,s,prologue=EMPTY,e=null){this.initiator=initiator;this.s=s;this.e=e;this.re=null;this.rs=null;this.prologue=prologue;this.stage=0;this.cipher=new NoiseCipher();} async initialize(){const name=enc.encode(NOISE_NAME);this.h=name.length<=32?concat(name,new Uint8Array(32-name.length)):await hash(name);this.ck=this.h.slice();await this.mixHash(this.prologue);return this;} async mixHash(bytes){this.h=await hash(concat(this.h,bytes));} async mixKey(bytes){const [ck,k]=await noiseHKDF(this.ck,bytes);this.ck.fill(0);this.ck=ck;this.cipher=await NoiseCipher.fromBytes(k);k.fill(0);bytes.fill(0);} async field(bytes,decrypt=false){const out=await this.cipher.crypt(bytes,this.h,decrypt);await this.mixHash(decrypt?bytes:out);return out;} async write(payload=EMPTY){if(payload.length>60000)throw Error('Handshake payload too large');let out; if(this.initiator&&this.stage===0){this.e=this.e||await keypair();await this.mixHash(this.e.publicBytes);out=concat(this.e.publicBytes,await this.field(payload));} else if(!this.initiator&&this.stage===1){this.e=this.e||await keypair();await this.mixHash(this.e.publicBytes);await this.mixKey(await dh(this.e,this.re));const encryptedStatic=await this.field(this.s.publicBytes);await this.mixKey(await dh(this.s,this.re));out=concat(this.e.publicBytes,encryptedStatic,await this.field(payload));} else if(this.initiator&&this.stage===2){const encryptedStatic=await this.field(this.s.publicBytes);await this.mixKey(await dh(this.s,this.re));out=concat(encryptedStatic,await this.field(payload));} else throw Error('Unexpected handshake write');this.stage++;return out; } async read(bytes){if(bytes.length>65535)throw Error('Handshake too large');let payload; if(!this.initiator&&this.stage===0){if(bytes.length<32)throw Error('Truncated handshake');this.re=bytes.slice(0,32);await this.mixHash(this.re);payload=await this.field(bytes.slice(32),true);} else if(this.initiator&&this.stage===1){if(bytes.length<96)throw Error('Truncated handshake');this.re=bytes.slice(0,32);await this.mixHash(this.re);await this.mixKey(await dh(this.e,this.re));this.rs=await this.field(bytes.slice(32,80),true);await this.mixKey(await dh(this.e,this.rs));payload=await this.field(bytes.slice(80),true);} else if(!this.initiator&&this.stage===2){if(bytes.length<64)throw Error('Truncated handshake');this.rs=await this.field(bytes.slice(0,48),true);await this.mixKey(await dh(this.e,this.rs));payload=await this.field(bytes.slice(48),true);} else throw Error('Unexpected handshake read');this.stage++;return payload; } async finish(){if(this.stage!==3)throw Error('Incomplete handshake');const [a,b]=await noiseHKDF(this.ck,EMPTY);const first=await NoiseCipher.fromBytes(a),second=await NoiseCipher.fromBytes(b);a.fill(0);b.fill(0);this.ck.fill(0);this.ck=null;this.e=null;this.s=null;this.cipher=null;const fingerprint=hex(this.h).toUpperCase().match(/.{8}/g).join('-');return {tx:this.initiator?first:second,rx:this.initiator?second:first,fingerprint,peerKey:b64(this.rs)};} } // Large application messages are framed into individual Noise messages <=65535 bytes. // The authenticated fragment header binds order and completeness within each box. async function seal(cipher,value){const bytes=enc.encode(JSON.stringify(value));if(bytes.length>1048576)throw Error('Message too large');const count=Math.max(1,Math.ceil(bytes.length/48000)),frames=[];for(let i=0;i22)throw Error('Invalid encrypted message');const chunks=[];let total=0;for(let i=0;i65535)throw Error('Invalid frame');const plain=await cipher.crypt(bytes,EMPTY,true),view=new DataView(plain.buffer,plain.byteOffset,plain.byteLength);if(view.getUint32(0)!==i||view.getUint32(4)!==box.frames.length)throw Error('Invalid fragment order');total+=plain.length-8;if(total>1048576)throw Error('Message too large');chunks.push(plain.subarray(8));}return JSON.parse(dec.decode(concat(...chunks)));} function validateSpeech(data){ if(!data||typeof data.text!=='string'||!data.text.trim()||data.text.length>8000)throw Error('TTS text must contain 1–8000 characters'); if(Object.keys(data).some(key=>!['text','lang','voice','rate','pitch','volume','id'].includes(key)))throw Error('Unknown TTS option'); for(const [key,min,max] of [['rate',.1,10],['pitch',0,2],['volume',0,1]])if(data[key]!==undefined&&(!Number.isFinite(data[key])||data[key]max))throw Error('Invalid TTS '+key); for(const [key,max] of [['lang',80],['voice',200]])if(data[key]!==undefined&&(typeof data[key]!=='string'||data[key].length>max))throw Error('Invalid TTS '+key); } const identity=keypair(); let handshake=null,session=null,rxKey=null,txKey=null,rxSeq=0,txSeq=0,sendQueue=Promise.resolve(),receiveQueue=Promise.resolve(); let pairingCode=null,verified=false,pairDeadline=null,trustedPeerKey=null; async function approveWorkspace(resumed){ const current=session,ws=socket;if(!ready||!current||stopping)throw Error('Session changed'); if(resumed&&trustedPeerKey!==current.peerKey)throw Error('Browser identity is not approved'); const inbox=await privateInbox(root,options.uid,true);await fsp.unlink(path.join(inbox,'.disconnected.json')).catch(error=>{if(error.code!=='ENOENT')throw error;}); if(ws!==socket||current!==session||!ready||stopping)throw Error('Session changed'); trustedPeerKey=current.peerKey;verified=true;clearTimeout(pairDeadline);pairingCode=null; await send({type:'event',event:'verified',data:{workspace:root,name:options.name,fingerprint:current.fingerprint,resumed,features:['world_image','world_capture','world_submit','world','lifecycle','cpu_benchmark','audio','tts','file_bytes','resources','bandwidth']}}); watch();reportResources();console.log(resumed?'Workspace reconnected securely. Explorer updates are active.':'Workspace connected. Explorer updates are active.'); } let inputQueue=Promise.resolve(); require('node:readline').createInterface({input:process.stdin}).on('line',line=>{inputQueue=inputQueue.then(async()=>{ if(!ready||!verified)throw Error('Wait for the workspace connection before sending commands.'); if(line.startsWith('world ')){await worldCommand(JSON.parse(line.slice(6)));return;} if(line.startsWith('tts ')){const data=JSON.parse(line.slice(4));validateSpeech(data);const id=require('node:crypto').randomBytes(16).toString('hex');await send({type:'event',event:'tts',data:{...data,id}});console.log('TTS_SENT '+JSON.stringify({id}));return;} if(line.startsWith('audio ')){const data=JSON.parse(line.slice(6));const file=await resolve(data.path);const stat=await fsp.stat(file.absolute);if(!stat.isFile()||stat.size<1||stat.size>2097152)throw Error('Audio must be a regular file under 2 MiB.');const bytes=await fsp.readFile(file.absolute);const ext=path.extname(file.absolute).toLowerCase();let mime; if(ext==='.wav'&&bytes.toString('ascii',0,4)==='RIFF'&&bytes.toString('ascii',8,12)==='WAVE')mime='audio/wav'; else if(ext==='.mp3'&&(bytes.toString('ascii',0,3)==='ID3'||(bytes[0]===255&&(bytes[1]&224)===224)))mime='audio/mpeg'; else if(ext==='.ogg'&&bytes.toString('ascii',0,4)==='OggS')mime='audio/ogg';else throw Error('Supported audio: WAV, MP3 or Ogg.'); const id=b64(crypto.getRandomValues(new Uint8Array(32))),count=Math.ceil(bytes.length/49152);await send({type:'event',event:'audio_start',data:{id,mime,size:bytes.length,count}}); for(let index=0;indexconsole.error('Input rejected: '+error.message));}); const root=fs.realpathSync(options.root);if(!fs.statSync(root).isDirectory())throw Error('Workspace root must be a directory'); const rootPrefix=root.endsWith(path.sep)?root:root+path.sep; const hidden=p=>p.split(/[\\/]/).some(part=>part.startsWith('.')); const normalize=p=>p.split(path.sep).join('/')||'.'; async function resolve(relative='.'){ if(typeof relative!=='string'||relative.length>4096||relative.includes('\0')||path.isAbsolute(relative))throw Error('Invalid relative path'); if(relative!=='.'&&hidden(relative))throw Error('Hidden files are not exposed'); const lexical=path.resolve(root,relative),resolved=await fsp.realpath(lexical); if(resolved!==root&&!resolved.startsWith(rootPrefix))throw Error('Path is outside workspace'); if(resolved!==root&&hidden(normalize(path.relative(root,resolved))))throw Error('Hidden files are not exposed'); if(lexical!==root&&!lexical.startsWith(rootPrefix))throw Error('Path is outside workspace'); return {absolute:resolved,relative:normalize(path.relative(root,lexical))}; } let previousCores=os.cpus(); const ticks=core=>Object.values(core.times).reduce((n,v)=>n+v,0); const clamp=n=>Math.max(0,Math.min(100,n)); function cpuReport(){ const cores=os.cpus(),models=new Map();let total=0,idle=0; const details=cores.map((core,index)=>{const previous=previousCores[index],delta=previous?ticks(core)-ticks(previous):0,idleDelta=previous?core.times.idle-previous.times.idle:0;if(delta>0){total+=delta;idle+=idleDelta;} models.set(core.model,(models.get(core.model)||0)+1);return {index,name:core.model,mhz:Number.isFinite(core.speed)&&core.speed>0?core.speed:null,percent:delta>0?clamp(100*(1-idleDelta/delta)):null};}); previousCores=cores;const percent=total>0?clamp(100*(1-idle/total)):null,totalGHz=details.length&&details.every(c=>c.mhz!==null)?details.reduce((n,c)=>n+c.mhz,0)/1000:null; const availableGHz=totalGHz!==null&&details.every(c=>c.percent!==null)?details.reduce((n,c)=>n+c.mhz/1000*(1-c.percent/100),0):null; return {percent,cores:cores.length||os.availableParallelism?.()||null,models:[...models].map(([name,count])=>({name,count})),details,totalGHz,availableGHz}; } async function resources(){const cpu=cpuReport();let disk=null;try{const d=await fsp.statfs(root);disk={total:d.blocks*d.bsize,available:d.bavail*d.bsize,used:(d.blocks-d.bfree)*d.bsize};}catch{}return {cpu,ram:{total:os.totalmem(),used:os.totalmem()-os.freemem()},disk};} let bandwidthTest=null; const benchmarkBytes=require('node:crypto').randomBytes(196608); function bandwidthOperation(method,p){ if(method==='bandwidth_ping')return {pong:true}; if(method==='bandwidth_begin'){if(bandwidthTest&&bandwidthTest.expires>Date.now())throw Error('A link test is already running');if(!Number.isSafeInteger(p.bytes)||p.bytes<196608||p.bytes>8388608)throw Error('Invalid test size');const id=require('node:crypto').randomBytes(24).toString('base64url');bandwidthTest={id,size:p.bytes,down:0,up:0,expires:Date.now()+120000};return {id,bytes:p.bytes};} const test=bandwidthTest;if(!test||test.id!==p.id||test.expires196608||test.down+p.length>test.size)throw Error('Invalid test range');test.down+=p.length;return {id:test.id,received:test.down,bytes:benchmarkBytes.subarray(0,p.length).toString('base64url')};} if(method==='bandwidth_upload'){const bytes=un64(p.bytes);if(!bytes.length||bytes.length>196608||test.up+bytes.length>test.size)throw Error('Invalid test chunk');test.up+=bytes.length;return {id:test.id,received:test.up};} if(method==='bandwidth_end'){if(test.up!==test.size||test.down!==test.size)throw Error('Link test is incomplete');bandwidthTest=null;return {downloadBytes:test.down,uploadBytes:test.up};} throw Error('Unsupported link test operation'); } async function reportResources(){if(verified)send({type:'event',event:'resources',data:await resources()});} const resourceTimer=setInterval(()=>reportResources().catch(()=>{}),3000);resourceTimer.unref(); let cpuBenchmarkBusy=false,benchmarkWorkers=new Set(); const cpuWorkSource=`const {parentPort}=require('node:worker_threads');const {performance}=require('node:perf_hooks');let x=123456789,count=0;const start=performance.now();do{for(let i=0;i<4096;i++){x^=x<<13;x^=x>>>17;x^=x<<5;x=Math.imul(x,1664525)+1013904223|0;}count+=4096;}while(performance.now()-start<1000);parentPort.postMessage({iterations:count,seconds:(performance.now()-start)/1000,checksum:x});`; function cpuWorker(){return new Promise((resolve,reject)=>{const {Worker}=require('node:worker_threads');const worker=new Worker(cpuWorkSource,{eval:true,resourceLimits:{maxOldGenerationSizeMb:16,maxYoungGenerationSizeMb:4}});benchmarkWorkers.add(worker);let settled=false;const timer=setTimeout(()=>{worker.terminate();finish(Error('CPU benchmark timed out'));},5000);function finish(error,result){if(settled)return;settled=true;clearTimeout(timer);benchmarkWorkers.delete(worker);error?reject(error):resolve(result);}worker.once('message',result=>finish(null,result));worker.once('error',error=>finish(error));worker.once('exit',code=>{if(!settled)finish(Error('CPU benchmark stopped ('+code+')'));});});} async function cpuBenchmark(){if(cpuBenchmarkBusy)throw Error('A CPU benchmark is already running');cpuBenchmarkBusy=true;try{const single=await cpuWorker();if(!verified||stopping)throw Error('Workspace disconnected');const threads=Math.min(8,os.availableParallelism?.()||os.cpus().length||1);const started=performance.now();const parallel=await Promise.all(Array.from({length:threads},cpuWorker));const elapsed=(performance.now()-started)/1000;return {workload:'Integer mix v1',singleMIterations:single.iterations/single.seconds/1e6,parallelMIterations:parallel.reduce((n,r)=>n+r.iterations,0)/elapsed/1e6,threads,seconds:single.seconds+elapsed,measuredAt:new Date().toISOString()};}finally{cpuBenchmarkBusy=false;}} const worldSubmissions=new Map();let completedWorldRevision=-1; async function submitWorld(data){ if(!worldDraft||worldDraft.draft||completedWorldRevision!==worldDraft.revision)throw Error('Wait for successful WORLD_RENDER of the finished scene before submitting'); if(data.path&&worldPath(data.path)!==worldDraft.path)throw Error('Load the requested scene before submitting'); if(worldSubmissions.size)throw Error('A submission is already in progress'); if(typeof data.title!=='string'||typeof (data.description??'')!=='string'||typeof (data.twitterHandle??'')!=='string')throw Error('Submission metadata must be text'); const metadata={title:data.title.trim(),description:(data.description||'').trim(),twitterHandle:(data.twitterHandle||'').trim(),category:data.category??'others'}; if(!['person','animal','vehicle','building','interior','object','nature','infrastructure','world','game','others'].includes(metadata.category))throw Error('Invalid submission category'); if(!metadata.title||[...metadata.title].length>120||[...metadata.description].length>2000||(metadata.twitterHandle&&!/^@?[A-Za-z0-9_]{1,15}$/.test(metadata.twitterHandle)))throw Error('Invalid title, description or Twitter handle'); const requestId=require('node:crypto').randomBytes(16).toString('hex'),revision=worldDraft.revision; const timer=setTimeout(()=>{worldSubmissions.delete(requestId);console.log('WORLD_SUBMIT '+JSON.stringify({type:'world_submit',requestId,revision,ok:false,error:'Submission timed out; verify the gallery before retrying.'}));},360000);timer.unref(); worldSubmissions.set(requestId,{revision,timer}); try{await send({type:'event',event:'world_submit',data:{requestId,revision,path:worldDraft.path,...metadata}});}catch(error){clearTimeout(timer);worldSubmissions.delete(requestId);throw error;} console.log('WORLD_SUBMIT_SENT '+JSON.stringify({requestId,revision,path:worldDraft.path})); } let worldDraft=null,worldRevision=0; function worldPath(value){if(typeof value!=='string'||!value||value.length>1024||path.isAbsolute(value)||value.includes('\\')||value.split('/').some(part=>!part||part==='..'||part.startsWith('.')))throw Error('Use a visible relative workspace path');return value;} function worldAssets(entries=[]){if(!Array.isArray(entries)||entries.length>32)throw Error('Use at most 32 world assets');return entries.map(asset=>{if(!asset||typeof asset!=='object')throw Error('Invalid world asset');const name=asset.name||'';if(name&&!/^[A-Za-z0-9_-]{1,80}$/.test(name))throw Error('Invalid texture name');if(asset.source!==undefined)throw Error('Invalid asset source');return {path:worldPath(asset.path),name};});} function assetsFromScene(code){const match=code.match(/^\/\/ CONNECT_WORLD_ASSETS: (.+)$/m);return match?worldAssets(JSON.parse(match[1])):null;} async function publishWorldFeedback(event){const directory=await privateInbox(root,options.uid,true);if((await fsp.readdir(directory)).filter(n=>n.endsWith('.json')).length>=1000)throw Error('Workspace inbox is full');const name=Date.now().toString().padStart(16,'0')+'-'+require('node:crypto').randomBytes(8).toString('hex');const temp=path.join(directory,'.'+name+'.tmp'),file=path.join(directory,name+'.json');try{await fsp.writeFile(temp,JSON.stringify(event),{flag:'wx',mode:0o600});await fsp.rename(temp,file);}finally{await fsp.unlink(temp).catch(()=>{});}} async function readWorldFile(relative){const file=await resolve(worldPath(relative));const stat=await fsp.stat(file.absolute);if(!stat.isFile()||stat.size>262144)throw Error('scene.txt must be a text file under 256 KiB');const code=new TextDecoder('utf-8',{fatal:true}).decode(await fsp.readFile(file.absolute));if(code.includes('\0'))throw Error('Scene contains binary data');return {code,path:file.relative,stat};} async function worldCommand(data){ if(!verified)throw Error('Wait for the workspace connection before sending commands'); if(data?.code!==undefined||data?.assets!==undefined)throw Error('Edit the scene file with your normal file tools; scene source is read from disk'); if(data?.capture!==undefined)throw Error('Use world {"op":"capture"} after a successful render'); if(data?.op==='submit'){await submitWorld(data);return;} if(data?.op==='capture'){ if(!worldDraft||completedWorldRevision!==worldDraft.revision)throw Error('Wait for successful WORLD_RENDER before requesting capture'); if(data.path&&worldPath(data.path)!==worldDraft.path)throw Error('Load the requested scene before capturing it'); await send({type:'event',event:'world_capture',data:{revision:worldDraft.revision,path:worldDraft.path}}); console.log('WORLD_CAPTURE_SENT '+JSON.stringify({revision:worldDraft.revision,path:worldDraft.path}));return; } if(data?.op==='load'){ const snapshot=await worldSnapshot({path:data.path||worldDraft?.path||'scene.txt',force:true}); await send({type:'event',event:'world',data:snapshot}); console.log('WORLD_SENT '+JSON.stringify({op:'load',revision:snapshot.revision,path:snapshot.path,saved:false}));return; } throw Error('Edit scene.txt directly. World commands: load, capture, submit'); } async function worldSnapshot(params={}){const file=await readWorldFile(params.path||worldDraft?.path||'scene.txt');if(params.force||worldDraft?.path!==file.path||worldDraft.code!==file.code)worldDraft={code:file.code,path:file.path,assets:assetsFromScene(file.code)||[],revision:++worldRevision,draft:false};return {...worldDraft,op:'snapshot',capture:false};} // Completed screenshots remain base64 text in render feedback; no image file is created. const worldImages=new Map(),WORLD_IMAGE_LIMIT=16769024,WORLD_IMAGE_CHUNK=196608; function clearWorldImages(){for(const image of worldImages.values())clearTimeout(image.timer);worldImages.clear();} function receiveWorldImage(params){ const {revision,index,count,data,width,height,mimeType}=params; if(!Number.isSafeInteger(revision)||revision<0||!Number.isSafeInteger(index)||!Number.isSafeInteger(count)||count<1||count>Math.ceil(WORLD_IMAGE_LIMIT/WORLD_IMAGE_CHUNK)||index<0||index>=count||typeof data!=='string'||!data.length||data.length>WORLD_IMAGE_CHUNK||data.length%4!==0||!/^[A-Za-z0-9+/]*={0,2}$/.test(data)||mimeType!=='image/png'||!Number.isSafeInteger(width)||!Number.isSafeInteger(height)||width<1||height<1||width>32768||height>32768)throw Error('Invalid world image chunk'); if(index===0){clearWorldImages();const image={revision,count,width,height,mimeType,parts:[],size:0,timer:setTimeout(clearWorldImages,120000)};image.timer.unref?.();worldImages.set(revision,image);} const image=worldImages.get(revision); if(!image||image.parts.length!==index||image.count!==count||image.width!==width||image.height!==height||image.mimeType!==mimeType)throw Error('World image chunk sequence mismatch'); if(indexWORLD_IMAGE_LIMIT){clearWorldImages();throw Error('World image exceeds 16 MiB feedback limit');} image.parts.push(data);image.size+=data.length;return {received:true,index}; } function takeWorldImage(revision){const image=worldImages.get(revision);if(!image)return null;clearTimeout(image.timer);worldImages.delete(revision);if(image.parts.length!==image.count)throw Error('Incomplete world image');const data=image.parts.join('');if(!data.startsWith('iVBORw0KGgo'))throw Error('Invalid PNG world image');return {mimeType:image.mimeType,encoding:'base64',width:image.width,height:image.height,data};} async function handle(method,params){ if(method==='world_submit_result'){ const job=worldSubmissions.get(params.requestId);if(!job||params.revision!==job.revision||typeof params.ok!=='boolean')throw Error('Unknown or stale submission result'); if(params.ok&&(!/^[a-f0-9]{64}$/.test(params.saved?.uid||'')||params.saved.url!=='/validate/uploads/?file='+params.saved.uid+'.neo'||!['/validate/uploads/'+params.saved.uid+'.webp','/validate/uploads/?preview='+params.saved.uid].includes(params.saved.preview)))throw Error('Invalid submission result'); clearTimeout(job.timer);worldSubmissions.delete(params.requestId); const event={type:'world_submit',requestId:params.requestId,revision:job.revision,ok:params.ok,...(params.ok?{saved:params.saved}:{error:String(params.error||'Submission failed').slice(0,2000)})}; console.log('WORLD_SUBMIT '+JSON.stringify(event));await publishWorldFeedback(event);return {received:true}; } if(method==='world_image')return receiveWorldImage(params); if(method==='world_state')return worldSnapshot(params); if(method==='world_render'){if(!Number.isSafeInteger(params.revision)||params.revision<0||typeof params.ok!=='boolean')throw Error('Invalid render acknowledgement');const data={type:params.phase==='step'?'world_step':'world_render',revision:params.revision,ok:params.ok,objects:Number(params.objects)||0,triangles:Number(params.triangles)||0,error:String(params.error||'').slice(0,2000),submissionAllowed:params.submissionAllowed!==false,warnings:Array.isArray(params.warnings)?params.warnings.slice(0,30).map(w=>String(w).slice(0,1000)):[]};if(data.type==='world_render'){try{const image=takeWorldImage(params.revision);if(image&&data.ok)data.image=image;}catch(error){data.imageError=String(error.message).slice(0,2000);}if(params.imageError)data.imageError=String(params.imageError).slice(0,2000);}if(data.type==='world_render'&¶ms.revision===worldDraft?.revision&&!params.imageError)completedWorldRevision=data.ok?params.revision:-1;console.log((data.type==='world_step'?'WORLD_STEP ':'WORLD_RENDER ')+JSON.stringify(data.image?{...data,image:{mimeType:data.image.mimeType,width:data.image.width,height:data.image.height,bytes:data.image.data?.length}}:data));await publishWorldFeedback(data);return {received:true};} if(method==='session_disconnect'){setTimeout(()=>stop('Browser disconnected'),150);return {stopping:true};} if(method==='cpu_benchmark')return cpuBenchmark(); if(method==='resources')return resources(); if(method.startsWith('bandwidth_'))return bandwidthOperation(method,params); if(method==='mkdir'||method.startsWith('write_'))throw Error('Browser file access is read-only'); if(method==='tts_status'){if(!/^[a-f0-9]{32}$/.test(params.id||'')||!['playing','blocked','ended','failed','stopped'].includes(params.status))throw Error('Invalid speech status');console.log('BROWSER_TTS '+JSON.stringify({id:params.id,status:params.status}));return {accepted:true};} if(method==='audio_status'){if(!['playing','blocked','ended','failed'].includes(params.status))throw Error('Invalid audio status');console.log('BROWSER_AUDIO '+JSON.stringify({status:params.status}));return {accepted:true};} const target=await resolve(params.path??'.'),stat=await fsp.stat(target.absolute); if(method==='stat')return {path:target.relative,kind:stat.isDirectory()?'directory':stat.isFile()?'file':'other',size:stat.size,mtime:stat.mtime.toISOString()}; if(method==='list'){ if(!stat.isDirectory())throw Error('Not a directory'); const names=(await fsp.readdir(target.absolute)).filter(n=>!n.startsWith('.')).sort((a,b)=>a.localeCompare(b));const entries=[]; for(const name of names.slice(0,2000)){try{const p=normalize(path.join(target.relative,name));const item=await resolve(p),s=await fsp.stat(item.absolute);if(!s.isDirectory()&&!s.isFile())continue;entries.push({name,path:p,kind:s.isDirectory()?'directory':'file',size:s.size,mtime:s.mtime.toISOString()});}catch{}} entries.sort((a,b)=>(a.kind===b.kind?0:a.kind==='directory'?-1:1)||a.name.localeCompare(b.name));return {path:target.relative,entries,truncated:names.length>2000}; } if(method==='read_bytes'){ if(!stat.isFile())throw Error('Not a regular file'); const offset=params.offset??0,length=params.length??196608; if(!Number.isSafeInteger(offset)||offset<0||offset>stat.size||!Number.isSafeInteger(length)||length<1||length>196608)throw Error('Invalid file range'); const file=await fsp.open(target.absolute,'r');let bytes; try{const current=await file.stat();if(params.size!==undefined&&(params.size!==current.size||params.mtime!==current.mtime.toISOString()))throw Error('File changed; retry the download');const buffer=Buffer.alloc(Math.min(length,current.size-offset));const result=await file.read(buffer,0,buffer.length,offset);bytes=buffer.subarray(0,result.bytesRead);}finally{await file.close();} return {path:target.relative,size:stat.size,mtime:stat.mtime.toISOString(),offset,bytes:bytes.toString('base64url')}; } if(method==='read'){ if(!stat.isFile())throw Error('Not a regular file'); // Read a bounded preview rather than loading multi-megabyte assets into memory. const file=await fsp.open(target.absolute,'r');let buffer; try{buffer=Buffer.alloc(Math.min(stat.size,262144));const result=await file.read(buffer,0,buffer.length,0);buffer=buffer.subarray(0,result.bytesRead);}finally{await file.close();} let content='';let binary=buffer.includes(0);if(!binary){try{content=new TextDecoder('utf-8',{fatal:true}).decode(buffer,{stream:stat.size>buffer.length});}catch{binary=true;}} return {path:target.relative,size:stat.size,binary,content:binary?'':content,truncated:stat.size>buffer.length}; } throw Error('Unsupported method'); } let socket=null,watcher=null,retry=null,changeTimer=null,stopping=false,ready=false,retryDelay=1000,active=0; const changes=new Set(); function send(message){if(!ready||!session||socket?.readyState!==WebSocket.OPEN)return;const ws=socket,current=session,n=++txSeq;return sendQueue=sendQueue.then(async()=>{if(ws!==socket||current!==session||!ready)return;const box=await seal(current.tx,{seq:n,...message});if(ws!==socket||current!==session||!ready)return;if(ws.bufferedAmount>=4194304)throw Error('Transport buffer full');ws.send(JSON.stringify(message.type==='event'?{type:'event',event:'encrypted',data:box}:{type:'response',id:message.id,result:box}));}).catch(error=>{if(ws!==socket)return;console.error('WORKSPACE_SEND_ERROR '+JSON.stringify({error:error.message,bufferedAmount:ws.bufferedAmount}));ws.close(4000,'Transport send failed; retrying');});} function changed(filename){if(!verified)return;if(filename==null){changes.add('*');}else{const p=normalize(String(filename));if(hidden(p))return;changes.add(p);}if(changes.size>500){changes.clear();changes.add('*');}clearTimeout(changeTimer);changeTimer=setTimeout(()=>{const paths=changes.has('*')?[]:[...changes];changes.clear();send({type:'event',event:'files_changed',data:{paths}});},300);} function watch(){if(watcher)return;if(root===path.parse(root).root){console.log('Filesystem root selected; use Refresh for file changes.');return;}try{watcher=fs.watch(root,{recursive:true},(_event,filename)=>changed(filename));watcher.on('error',()=>{console.error('Workspace watcher unavailable; use Refresh in the browser.');watcher?.close();watcher=null;});}catch{console.error('Workspace watcher unavailable; use Refresh in the browser.');}} function connect(){if(stopping)return;ready=false;verified=false;pairingCode=null;clearTimeout(pairDeadline);handshake=session=null;rxKey=txKey=null;rxSeq=txSeq=0;sendQueue=Promise.resolve();receiveQueue=Promise.resolve();socket=new WebSocket(options.url);const ws=socket; const helloTimer=setTimeout(()=>{if(ws===socket&&!ready)ws.close(4000,'Relay hello timeout');},12000); ws.onopen=()=>ws.send(JSON.stringify({type:'hello',role:'agent',uid:options.uid,workspace:'Encrypted workspace',name:options.name.slice(0,80),capabilities:['stat']})); ws.onmessage=event=>{receiveQueue=receiveQueue.then(async()=>{if(ws!==socket)return;let message;try{message=JSON.parse(event.data);}catch{return;} if(message.type==='welcome'){clearTimeout(helloTimer);ready=true;retryDelay=1000;console.log('WSS server connection confirmed. Waiting for Noise pairing.');return;} if(message.type==='error'){console.error('WORKSPACE_RELAY_ERROR '+JSON.stringify({code:message.code,retry:message.code==='invalid_hello'}));if(message.code==='invalid_hello'){clearTimeout(helloTimer);ws.close(4000,'Relay rejected hello; retrying');}return;} if(message.type!=='request'||typeof message.id!=='string')return; const relayId=message.id; try{ if(message.params?.handshake){ const frame=message.params.noise; if(frame?.v!==2||typeof frame.data!=='string'||frame.data.length>1024)throw Error('Noise v2 pairing required'); if(frame.stage===1){ // Only an unverified connection can start a new handshake; no mid-session replacement. if(verified||handshake||session)throw Error('Pairing already in progress'); const next=await new NoiseXX(false,await identity,enc.encode('neome-connect-v2:'+options.uid)).initialize();if(ws!==socket||!ready)return;handshake=next; const payload=await next.read(un64(frame.data));if(ws!==socket||handshake!==next||!ready)return;if(payload.length)throw Error('Unexpected handshake payload'); const reply=await next.write();if(ws!==socket||handshake!==next||!ready)return; pairDeadline=setTimeout(()=>ws.close(4008,'Pairing expired'),3600000); ws.send(JSON.stringify({type:'response',id:relayId,result:{v:2,stage:2,data:b64(reply)}}));return; } if(frame.stage===3&&handshake?.stage===2){ const next=handshake;const payload=await next.read(un64(frame.data));if(ws!==socket||handshake!==next||!ready)return;if(payload.length)throw Error('Unexpected handshake payload'); const established=await next.finish();if(ws!==socket||handshake!==next||!ready)return;session=established;handshake=null;rxKey=session.rx;txKey=session.tx;rxSeq=txSeq=0; pairingCode=session.fingerprint; const establishedSession=session;const result=await seal(establishedSession.tx,{type:'pair_ready',fingerprint:pairingCode,autoConnect:true,resumable:trustedPeerKey===establishedSession.peerKey});if(ws!==socket||session!==establishedSession||!ready)return;ws.send(JSON.stringify({type:'response',id:relayId,result})); console.log('Encrypted handshake complete. Connecting workspace automatically.');return; } throw Error('Unexpected handshake stage'); } if(!session)throw Error('Authenticated pairing required'); const receiving=session;const body=await open(receiving.rx,message.params.box);if(ws!==socket||session!==receiving||!ready)return; if(!Number.isSafeInteger(body.seq)||body.seq<=rxSeq||typeof body.rid!=='string'||body.rid.length>64)throw Error('Invalid authenticated request');rxSeq=body.seq; message={...body,id:relayId}; }catch(error){if(ws!==socket)return;console.error('WORKSPACE_PROTOCOL_ERROR '+JSON.stringify({error:error.message}));ws.close(4008,'Encrypted pairing failed');return;} const current=session; if(active>=8&&message.method!=='session_disconnect'){send({type:'response',id:relayId,rid:message.rid,error:{code:'busy',message:'Client is busy; retry shortly.'}});return;} active++; try{let result;if(message.method==='session_connect'){ if(message.params?.fingerprint!==current.fingerprint)throw Error('Encrypted session mismatch'); if(!verified)await approveWorkspace(false);result={connected:true}; }else if(message.method==='session_resume'){ if(!trustedPeerKey||current.peerKey!==trustedPeerKey||message.params?.fingerprint!==current.fingerprint)throw Error('Secure reconnect requires the previously approved browser identity'); if(!verified)await approveWorkspace(true);result={resumed:true}; }else{if(!verified)throw Error('Workspace connection is not active');result=await handle(message.method,message.params||{});}if(ws===socket&¤t===session)send({type:'response',id:message.id,rid:message.rid,result});} catch(error){if(ws===socket&¤t===session)send({type:'response',id:message.id,rid:message.rid,error:{code:'file_error',message:['ENOENT','EACCES','ENOTDIR'].includes(error.code)?error.code:error.message}});} finally{active--;} }).catch(error=>{if(ws!==socket)return;console.error('WORKSPACE_MESSAGE_ERROR '+JSON.stringify({error:error.message}));ws.close(4008,'Encrypted message failed');});}; ws.onerror=event=>{if(ws!==socket)return;console.error('WORKSPACE_SOCKET_ERROR '+JSON.stringify({message:event.message||'Relay connection unavailable'}));}; ws.onclose=event=>{ clearTimeout(helloTimer);if(ws!==socket)return;clearWorldImages(); ready=false;verified=false;pairingCode=null;clearTimeout(pairDeadline);bandwidthTest=null;completedWorldRevision=-1;session=handshake=null;rxKey=txKey=null; clearTimeout(changeTimer);changes.clear();for(const worker of benchmarkWorkers)worker.terminate().catch(()=>{}); for(const [requestId,job]of worldSubmissions){ clearTimeout(job.timer);const feedback={type:'world_submit',requestId,revision:job.revision,ok:false,error:'Connection lost during submission; check the gallery before retrying.'}; console.log('WORLD_SUBMIT '+JSON.stringify(feedback));publishWorldFeedback(feedback).catch(error=>console.error(error.message)); }worldSubmissions.clear(); if(!stopping){console.log('WORKSPACE_RECONNECTING '+JSON.stringify({code:event.code,reason:event.reason||'Transport disconnected',wasClean:event.wasClean}));retry=setTimeout(connect,retryDelay);retryDelay=Math.min(15000,retryDelay*2);} }; } function stop(reason='Client stopped'){for(const job of worldSubmissions.values())clearTimeout(job.timer);worldSubmissions.clear();clearWorldImages();if(stopping)return;const wasVerified=verified;stopping=true;verified=false;trustedPeerKey=null;console.log('WORKSPACE_DISCONNECTED '+JSON.stringify({reason}));for(const worker of benchmarkWorkers)worker.terminate().catch(()=>{});if(wasVerified)privateInbox(root,options.uid,true).then(async directory=>{const temporary=path.join(directory,'.disconnected.tmp');await fsp.writeFile(temporary,JSON.stringify({type:'workspace_disconnected',reason,received_at:new Date().toISOString()}),{mode:0o600});await fsp.rename(temporary,path.join(directory,'.disconnected.json'));}).catch(error=>console.error('Disconnect event could not be saved: '+error.message));clearInterval(resourceTimer);clearTimeout(retry);clearTimeout(changeTimer);clearTimeout(pairDeadline);watcher?.close();socket?.close();setTimeout(()=>process.exit(process.exitCode||0),500);} process.on('SIGINT',stop);process.on('SIGTERM',stop);connect(); ``` ## Next step Continue with https://gpt.neome.com/hello/world. Keep this client alive while authoring and checking the scene. Submit only when publication is part of the user's request, and report publication only after `WORLD_SUBMIT` returns `ok:true` and `saved`.