| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677 |
- // @vitest-environment node
- import {expect,it} from 'vitest'
- import {execFile} from 'node:child_process'
- import {promisify} from 'node:util'
- import {mkdtemp,mkdir,writeFile,readFile,chmod,rm,stat} from 'node:fs/promises'
- import {tmpdir} from 'node:os'
- import {join,resolve} from 'node:path'
- import {randomUUID,randomBytes,scryptSync} from 'node:crypto'
- const command=promisify(execFile),image=process.env.YAOYAO_DOCKER_BRIDGE_IMAGE
- const docker=async(...args:string[])=>(await command('docker',args,{timeout:90000,maxBuffer:2*1024*1024})).stdout.trim()
- it.skipIf(!image)('installs bridges into mapped Hermes storage through the real non-root, read-only Docker Web',async()=>{
- const root=await mkdtemp(join(tmpdir(),'yaoyao-docker-bridge-')),hermes=join(root,'hermes')
- const id='yaoyao-bridge-'+randomUUID().slice(0,8),web=id+'-web',upstream=id+'-hermes',network=id+'-net',volume=id+'-data'
- const original=JSON.stringify({model:{default:'fixture-model'},notes:'preserve-default',plugins:{enabled:['existing']}})
- await chmod(root,0o755);await mkdir(join(hermes,'profiles','writer'),{recursive:true})
- // Disposable fixture permissions allow the image's uid 1000 to write; never
- // change permissions on the user's real Hermes directory.
- for(const path of [hermes,join(hermes,'profiles'),join(hermes,'profiles','writer')])await chmod(path,0o777)
- await writeFile(join(hermes,'config.yaml'),original,{mode:0o666});await chmod(join(hermes,'config.yaml'),0o666)
- await writeFile(join(hermes,'profiles','writer','config.yaml'),JSON.stringify({notes:'preserve-writer',plugins:{disabled:['yaoyao-bot-bridge']}}),{mode:0o666})
- await chmod(join(hermes,'profiles','writer','config.yaml'),0o666)
- const salt=randomBytes(16),now=Date.now()
- await writeFile(join(root,'users.json'),JSON.stringify({version:1,users:[{id:randomUUID(),username:'fixture',normalizedUsername:'fixture',role:'admin',enabled:true,mustChangePassword:false,salt:salt.toString('base64'),passwordHash:scryptSync('fixture-pass',salt,32,{N:2**14,r:8,p:1,maxmem:64*1024*1024}).toString('base64'),authVersion:1,createdAt:now,updatedAt:now}]}))
- await writeFile(join(root,'upstream.mjs'),`import {createServer} from 'node:http';const calls=[];createServer((req,res)=>{const path=new URL(req.url,'http://hermes').pathname;calls.push({path,method:req.method});res.setHeader('Content-Type','application/json');let value={ok:true};if(path==='/api/status')value={auth_required:true,gateway_running:true};if(path==='/auth/password-login')res.setHeader('Set-Cookie','hermes_session_at=fixture; Path=/; HttpOnly');if(path==='/api/auth/me')value={user_id:'fixture'};if(path==='/api/profiles')value={profiles:[{name:'default'},{name:'writer'}]};if(path.endsWith('/capabilities'))value={version:1,ready:false,in_process:true};if(path==='/__calls')value=calls;res.end(JSON.stringify(value))}).listen(9119,'0.0.0.0');`)
- const cookies=new Map<string,string>();let csrf=''
- try{
- for(const base of ['compose.yaml','compose.desktops.yaml','compose.desktops.cursor.yaml']){
- const output=await command('docker',['compose','--env-file','/dev/null','-f',base,'-f','compose.hermes-bridge.yaml','config','--format','json'],{env:{...process.env,HERMES_YAOYAO_HERMES_DIR:hermes},maxBuffer:2*1024*1024})
- const config=JSON.parse(output.stdout),service=config.services.web
- expect(service.read_only).toBe(true);expect(service.cap_drop).toEqual(['ALL'])
- expect(service.environment.HERMES_YAOYAO_LOCAL_VM_HOST).toBe('runner')
- expect(service.environment.HERMES_YAOYAO_SUPERVISE_DASHBOARD).toBe('0')
- expect(service.volumes.find((v:any)=>v.target==='/hermes')).toMatchObject({type:'bind',source:hermes,bind:{create_host_path:false}})
- expect(JSON.stringify(service.volumes)).not.toContain('docker.sock')
- }
- await docker('network','create',network);await docker('volume','create',volume)
- await docker('run','--rm','--network','none','--mount',`type=volume,src=${volume},dst=/home/node/.yaoyao`,'--mount',`type=bind,src=${root},dst=/fixture,readonly`,'--entrypoint','node',image!,'-e',"require('fs').copyFileSync('/fixture/users.json','/home/node/.yaoyao/users.json')")
- await docker('run','-d','--name',upstream,'--network',network,'--network-alias','hermes','--no-healthcheck','--mount',`type=bind,src=${root},dst=/fixture,readonly`,image!,'node','/fixture/upstream.mjs')
- await docker('run','-d','--name',web,'--network',network,'--read-only','--cap-drop','ALL','--security-opt','no-new-privileges:true','--tmpfs','/tmp:mode=1777','-p','127.0.0.1::15300',
- '--mount',`type=volume,src=${volume},dst=/home/node/.yaoyao`,'--mount',`type=bind,src=${hermes},dst=/hermes`,
- '-e','HERMES_YAOYAO_UPSTREAM=http://hermes:9119','-e','HERMES_YAOYAO_UPSTREAM_USERNAME=fixture','-e','HERMES_YAOYAO_UPSTREAM_PASSWORD=fixture','-e','HERMES_YAOYAO_ALLOW_INSECURE_LAN=1','-e','HERMES_YAOYAO_ALLOWED_HOSTS=127.0.0.1,localhost',
- '-e','HERMES_YAOYAO_BRIDGE_MOUNTED=1','-e','HERMES_YAOYAO_BRIDGE_HOME=/hermes','-e','HERMES_YAOYAO_BRIDGE_PYTHON=/usr/bin/python3',image!)
- const port=(await docker('port',web,'15300/tcp')).split(':').at(-1)!,origin='http://127.0.0.1:'+port
- const api=async(path:string,body?:unknown,expected=200)=>{
- const response=await fetch(origin+path,{method:body===undefined?'GET':'POST',headers:{Origin:origin,'Content-Type':'application/json',Cookie:[...cookies].map(([key,value])=>key+'='+value).join('; '),...(csrf?{'X-CSRF-Token':csrf}:{})},...(body!==undefined?{body:JSON.stringify(body)}:{})})
- for(const header of response.headers.getSetCookie()){const item=header.split(';')[0],equals=item.indexOf('=');cookies.set(item.slice(0,equals),item.slice(equals+1))}
- const text=await response.text();expect(response.status,text).toBe(expected);return JSON.parse(text)
- }
- await expect.poll(async()=>{try{return(await fetch(origin+'/healthz')).status}catch{return 0}},{timeout:30000}).toBe(200)
- await api('/auth/password-login',{username:'fixture',password:'fixture-pass'})
- csrf=(await api('/api/app/bootstrap?csrfOnly=1')).csrfToken
- const before=await api('/api/app/admin/hermes-bridge')
- expect(before.mapped).toBe(true);expect(before.local).toBe(true);expect(before.profiles[0].canInstall).toBe(true)
- expect(before.profiles[0].state).toBe('missing')
- await api('/api/app/admin/hermes-bridge/install',{profile:'writer',enable:true},409)
- const installed=await api('/api/app/admin/hermes-bridge/install',{profile:'default',enable:true})
- expect(installed.status.profiles[0].state).toBe('restart-required')
- const backup=join(hermes,installed.backup.slice('/hermes/'.length),'config.yaml')
- expect(await readFile(backup,'utf8')).toBe(original)
- expect(await readFile(join(hermes,'config.yaml'),'utf8')).toContain('preserve-default')
- expect(await readFile(join(hermes,'plugins','yaoyao-bot-bridge','bridge_runtime.py'),'utf8')).toContain('computer_runtime_version')
- await api('/api/app/admin/hermes-bridge/install',{profile:'writer',enable:true})
- expect(await readFile(join(hermes,'profiles','writer','config.yaml'),'utf8')).toContain('preserve-writer')
- expect((await stat(join(hermes,'profiles','writer','plugins','yaoyao-bot-bridge','plugin.yaml'))).isFile()).toBe(true)
- expect(await docker('exec',web,'id','-u')).toBe('1000')
- expect(await docker('exec',web,'python3','-c','import yaml;print("yaml-ready")')).toBe('yaml-ready')
- expect(await docker('inspect','--format','{{.HostConfig.ReadonlyRootfs}}',web)).toBe('true')
- const calls=JSON.parse(await docker('exec',web,'node','-e',"fetch('http://hermes:9119/__calls').then(r=>r.text()).then(console.log)"))
- expect(calls.some((c:any)=>/restart|stop|gateway\/start/.test(c.path))).toBe(false)
- }catch(error){try{console.error((await docker('logs','--tail','35',web)).slice(-6000))}catch{}throw error}
- finally{
- await docker('rm','-f',web,upstream).catch(()=>{})
- await docker('volume','rm',volume).catch(()=>{});await docker('network','rm',network).catch(()=>{})
- await rm(root,{recursive:true,force:true})
- }
- },120000)
|