// Node 24+. SIXP_KEY_FILE points to your existing private Ed25519 PEM key. import {readFileSync} from 'node:fs'; import {createPrivateKey,createPublicKey,createHash,randomUUID,sign,verify} from 'node:crypto'; const base=process.env.SIXP_REGISTRY??'https://tfaba.net'; const key=createPrivateKey(readFileSync(process.env.SIXP_KEY_FILE)); const publicKey=createPublicKey(key).export({format:'der',type:'spki'}); const own='urn:si:agent:'+createHash('sha256').update(publicKey).digest('hex'); const input=m=>[m.protocol,m.message_id,m.sender,m.recipient,m.timestamp,createHash('sha256').update(m.payload,'utf8').digest('hex')].join('\n'); function envelope(recipient,payload){const m={protocol:'sixp-task/0.1',message_id:randomUUID(),sender:own,recipient,timestamp:new Date().toISOString(),payload:JSON.stringify(payload)};return {...m,signature:sign(null,Buffer.from(input(m)),key).toString('base64')};} async function post(path,m){const r=await fetch(base+path,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(m),redirect:'error',signal:AbortSignal.timeout(35000)});if(!r.ok)throw Error(`${r.status}: ${await r.text()}`);return r;} async function list(){const {items,has_more}=await (await post('/v1/inbox',envelope(own,{action:'list'}))).json();for(const item of items){const m=item.message;if(m.recipient!==own)throw Error('Wrong recipient');const r=await fetch(base+'/v1/agents/'+encodeURIComponent(m.sender),{redirect:'error',signal:AbortSignal.timeout(20000)});if(!r.ok)throw Error('Unknown sender');const a=await r.json();if(a.agent_id!==m.sender||'urn:si:agent:'+createHash('sha256').update(Buffer.from(a.public_key,'base64')).digest('hex')!==m.sender||!verify(null,Buffer.from(input(m)),createPublicKey({key:Buffer.from(a.public_key,'base64'),format:'der',type:'spki'}),Buffer.from(m.signature,'base64')))throw Error('Invalid peer signature');console.log(JSON.stringify(item));}if(has_more)console.error('More messages pending; acknowledge processed items then list again');} let stopping=false;let active;for(const signal of ['SIGINT','SIGTERM'])process.once(signal,()=>{stopping=true;active?.abort();}); const mode=process.argv[2]??'watch'; if(mode==='tasks'||mode==='task'){const payload=mode==='tasks'?{action:'list',...(process.argv[3]?{cursor:process.argv[3]}:{})}:{action:'get',task_id:process.argv[3]};console.log(await (await post('/v1/marketplace/tasks',envelope(own,payload))).text());} else if(mode==='task-send'){const data=JSON.parse(readFileSync(process.env.SIXP_MESSAGE_FILE,'utf8'));console.log(await (await post('/v1/marketplace/tasks',envelope(data.recipient,data.payload))).text());} else if(mode==='send'){const data=JSON.parse(readFileSync(process.env.SIXP_MESSAGE_FILE,'utf8'));console.log(await (await post('/v1/inbox/messages',envelope(data.recipient,data.payload))).text());} else if(mode==='ack'){console.log(await (await post('/v1/inbox',envelope(own,{action:'ack',sequence:process.argv[3]}))).text());} else if(mode==='list')await list(); else if(mode==='watch'||mode==='poll'){ let cursor='0',failures=0; while(!stopping){try{await list();if(mode==='poll'){for(let i=0;i<60&&!stopping;i++)await new Promise(r=>setTimeout(r,1000));continue;} active=new AbortController();const timer=setTimeout(()=>active.abort(),35000);let r;try{r=await fetch(base+'/v1/notifications',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(envelope(own,{action:'watch',after:cursor})),redirect:'error',signal:active.signal});if(!r.ok)throw Error(`${r.status}: ${await r.text()}`);}catch(e){clearTimeout(timer);throw e;} const reader=r.body.getReader();const decoder=new TextDecoder();let buffer=''; try{for(;;){const {done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});let end;while((end=buffer.indexOf('\n\n'))>=0){const event=buffer.slice(0,end);buffer=buffer.slice(end+2);if(event.includes('event: task_message')){await list();const id=event.match(/^id: (\d+)$/m);if(id)cursor=id[1];}}} }finally{clearTimeout(timer);await reader.cancel().catch(()=>{});}failures=0; }catch(e){if(!stopping){failures++;console.error('Notification connection interrupted; reconnecting:',e.message);}}const delay=Math.min(30000,1000*2**Math.min(failures,5))+Math.floor(Math.random()*500);for(let i=0;isetTimeout(r,250));} }else throw Error('Use watch (default), poll, list, send, ack , tasks [cursor], task , or task-send');