|
@@ -27,8 +27,9 @@ import type { Server } from 'node:http'
|
|
|
import type { WorkspaceRun } from '../../src/shared/workspace'
|
|
import type { WorkspaceRun } from '../../src/shared/workspace'
|
|
|
|
|
|
|
|
const scenarios=['website.login','ssh.exec','sftp.read','sftp.write','website.login.cancel','ssh.exec.cancel','sftp.write.cancel','ssh.exec.password','sftp.read.password','sftp.write.password','ssh.exec.password.cancel','ssh.exec.password.first-use','sftp.read.password.first-use','sftp.write.password.first-use'] as const
|
|
const scenarios=['website.login','ssh.exec','sftp.read','sftp.write','website.login.cancel','ssh.exec.cancel','sftp.write.cancel','ssh.exec.password','sftp.read.password','sftp.write.password','ssh.exec.password.cancel','ssh.exec.password.first-use','sftp.read.password.first-use','sftp.write.password.first-use'] as const
|
|
|
-const cases=[...(['external','local'] as const).flatMap(storage=>scenarios.map(scenario=>({storage,scenario,persistent:false}))),...(['website.login','ssh.exec.password.first-use','sftp.read.password','ssh.exec.password.cancel'] as const).map(scenario=>({storage:'local' as const,scenario,persistent:true}))]
|
|
|
|
|
-it.each(cases)('a Bot turn discovers and uses $storage vault references for dummy $scenario (persistent=$persistent) with sanitized receipts',async({storage,scenario,persistent})=>{
|
|
|
|
|
|
|
+const baseCases=[...(['external','local'] as const).flatMap(storage=>scenarios.map(scenario=>({storage,scenario,persistent:false}))),...(['website.login','ssh.exec.password.first-use','sftp.read.password','ssh.exec.password.cancel'] as const).map(scenario=>({storage:'local' as const,scenario,persistent:true}))]
|
|
|
|
|
+const cases=[...baseCases.map(c=>({...c,autonomous:false})),...(['ssh.exec.password.first-use','ssh.exec.password.cancel'] as const).map(scenario=>({storage:'local' as const,scenario,persistent:true,autonomous:true}))]
|
|
|
|
|
+it.each(cases)('a Bot turn discovers and uses $storage vault references for dummy $scenario (persistent=$persistent, autonomous=$autonomous) with sanitized receipts',async({storage,scenario,persistent,autonomous})=>{
|
|
|
const cancelled=scenario.endsWith('.cancel'),firstUse=scenario.endsWith('.first-use'),operation=scenario.replace('.password','').replace('.first-use','').replace(/\.cancel$/,'') as 'website.login'|'ssh.exec'|'sftp.read'|'sftp.write'
|
|
const cancelled=scenario.endsWith('.cancel'),firstUse=scenario.endsWith('.first-use'),operation=scenario.replace('.password','').replace('.first-use','').replace(/\.cancel$/,'') as 'website.login'|'ssh.exec'|'sftp.read'|'sftp.write'
|
|
|
const home=mkdtempSync(join(tmpdir(),'yaoyao-vault-bot-flow-'))
|
|
const home=mkdtempSync(join(tmpdir(),'yaoyao-vault-bot-flow-'))
|
|
|
const website=await websiteFixture(home),ssh=await sshFixture(scenario.includes('.password')?'password':'privateKey')
|
|
const website=await websiteFixture(home),ssh=await sshFixture(scenario.includes('.password')?'password':'privateKey')
|
|
@@ -37,6 +38,9 @@ it.each(cases)('a Bot turn discovers and uses $storage vault references for dumm
|
|
|
const usage=operation==='website.login'?website.usage:operation==='ssh.exec'?{kind:'ssh.exec',command:'/usr/bin/uptime'}:operation==='sftp.read'?{kind:'sftp.read',remotePath:'/approved/input.txt',maxBytes:64}:{kind:'sftp.write',remotePath:'/approved/output.txt',contents:'DUMMY-fixed-transfer-body'}
|
|
const usage=operation==='website.login'?website.usage:operation==='ssh.exec'?{kind:'ssh.exec',command:'/usr/bin/uptime'}:operation==='sftp.read'?{kind:'sftp.read',remotePath:'/approved/input.txt',maxBytes:64}:{kind:'sftp.write',remotePath:'/approved/output.txt',contents:'DUMMY-fixed-transfer-body'}
|
|
|
const credentialTarget=isWebsite?{kind:'website',origin:website.origin}:firstUse?(({hostKey:_hostKey,...target})=>target)(ssh.target):ssh.target
|
|
const credentialTarget=isWebsite?{kind:'website',origin:website.origin}:firstUse?(({hostKey:_hostKey,...target})=>target)(ssh.target):ssh.target
|
|
|
const expected=operation==='website.login'?{status:'complete',operation}:operation==='ssh.exec'?{status:'complete',operation,exitCode:0}:operation==='sftp.read'?{status:'complete',operation,bytes:ssh.files.get('/approved/input.txt')!.length,sha256:createHash('sha256').update(ssh.files.get('/approved/input.txt')!).digest('hex')}:{status:'complete',operation,bytes:Buffer.byteLength('DUMMY-fixed-transfer-body'),sha256:createHash('sha256').update('DUMMY-fixed-transfer-body').digest('hex')}
|
|
const expected=operation==='website.login'?{status:'complete',operation}:operation==='ssh.exec'?{status:'complete',operation,exitCode:0}:operation==='sftp.read'?{status:'complete',operation,bytes:ssh.files.get('/approved/input.txt')!.length,sha256:createHash('sha256').update(ssh.files.get('/approved/input.txt')!).digest('hex')}:{status:'complete',operation,bytes:Buffer.byteLength('DUMMY-fixed-transfer-body'),sha256:createHash('sha256').update('DUMMY-fixed-transfer-body').digest('hex')}
|
|
|
|
|
+ if(autonomous){ssh.state.execReplies.set('uname -s',{stdout:'Linux\n',stderr:''});ssh.state.execReplies.set('df -h / | head -n 2',{stdout:'Filesystem 40%\n',stderr:''})}
|
|
|
|
|
+ const completed=autonomous?{status:'complete',operation:'ssh.exec',exitCode:0,stdout:'Linux\n',stderr:'',truncated:false}:persistent&&operation==='ssh.exec'?{...expected,stdout:'DUMMY-private-command-output',stderr:'DUMMY-private-stderr',truncated:false}:expected
|
|
|
|
|
+ const expectedEffects=autonomous&&!cancelled?2:1
|
|
|
const effectCount=()=>isWebsite?website.state.logins:operation==='ssh.exec'?ssh.state.commands.length:ssh.state.opens.length
|
|
const effectCount=()=>isWebsite?website.state.logins:operation==='ssh.exec'?ssh.state.commands.length:ssh.state.opens.length
|
|
|
const store=new WorkspaceStore(home),uploads=new UploadStore(home),auth=new LocalAuthStore(home)
|
|
const store=new WorkspaceStore(home),uploads=new UploadStore(home),auth=new LocalAuthStore(home)
|
|
|
const loginHeaders:Record<string,any>={},login={state:{},response:{headers:loginHeaders},get:()=>'',set:(k:string,v:any)=>{loginHeaders[k.toLowerCase()]=v}} as any
|
|
const loginHeaders:Record<string,any>={},login={state:{},response:{headers:loginHeaders},get:()=>'',set:(k:string,v:any)=>{loginHeaders[k.toLowerCase()]=v}} as any
|
|
@@ -85,8 +89,8 @@ it.each(cases)('a Bot turn discovers and uses $storage vault references for dumm
|
|
|
const named=(name:string)=>catalog.tools.find((t:any)=>t.name===name)!.id
|
|
const named=(name:string)=>catalog.tools.find((t:any)=>t.name===name)!.id
|
|
|
expect(named('credential_request')).toBeTruthy();expect(named('credential_refs')).toBeTruthy()
|
|
expect(named('credential_request')).toBeTruthy();expect(named('credential_refs')).toBeTruthy()
|
|
|
const discovered=(await tool('/tools/call',{toolId:named('credential_refs'),arguments:{},callId:'before-approval'})).structuredContent.references
|
|
const discovered=(await tool('/tools/call',{toolId:named('credential_refs'),arguments:{},callId:'before-approval'})).structuredContent.references
|
|
|
- expect(discovered).toHaveLength(1);expect(discovered[0]).toMatchObject({credentialRef,operation,authorized:persistent});references.push(discovered)
|
|
|
|
|
- const call={toolId:named('credential_request'),arguments:{credentialRef:discovered[0].credentialRef,operation},callId:'single-dummy-login'}
|
|
|
|
|
|
|
+ expect(discovered).toHaveLength(1);expect(discovered[0]).toMatchObject({credentialRef,operation:persistent&&!isWebsite?'ssh.exec':operation,authorized:persistent});references.push(discovered)
|
|
|
|
|
+ const call=autonomous?{toolId:named('credential_request'),arguments:{credentialRef:discovered[0].credentialRef,operation:'ssh.exec',command:'uname -s',timeoutSeconds:30},callId:'single-dummy-login'}:{toolId:named('credential_request'),arguments:{credentialRef:discovered[0].credentialRef,operation},callId:'single-dummy-login'}
|
|
|
const pending=tool('/tools/call',call)
|
|
const pending=tool('/tools/call',call)
|
|
|
if(!persistent){let view:any
|
|
if(!persistent){let view:any
|
|
|
await vi.waitFor(async()=>{view=(await ui('')).body;expect(view.requests).toHaveLength(1)})
|
|
await vi.waitFor(async()=>{view=(await ui('')).body;expect(view.requests).toHaveLength(1)})
|
|
@@ -99,8 +103,9 @@ it.each(cases)('a Bot turn discovers and uses $storage vault references for dumm
|
|
|
if(persistent){await ui('/unlock',{password:master}).expect(200);await ui('/bot-revoke',{credentialRef,agentId:persistentBotId}).expect(200)}
|
|
if(persistent){await ui('/unlock',{password:master}).expect(200);await ui('/bot-revoke',{credentialRef,agentId:persistentBotId}).expect(200)}
|
|
|
else await ui('/lock',{}).expect(200)
|
|
else await ui('/lock',{}).expect(200)
|
|
|
}
|
|
}
|
|
|
- const result=await pending;if(cancelled){expect(result.isError).toBe(true);expect(JSON.stringify(result)).toContain('vault_operation_uncertain')}else expect(result.structuredContent).toEqual(expected);results.push(result)
|
|
|
|
|
|
|
+ const result=await pending;if(cancelled){expect(result.isError).toBe(true);expect(JSON.stringify(result)).toContain('vault_operation_uncertain')}else expect(result.structuredContent).toEqual(completed);results.push(result)
|
|
|
expect(await tool('/tools/call',call)).toEqual(result);expect(effectCount()).toBe(1) // Same call ID cannot log in twice.
|
|
expect(await tool('/tools/call',call)).toEqual(result);expect(effectCount()).toBe(1) // Same call ID cannot log in twice.
|
|
|
|
|
+ if(autonomous&&!cancelled){const command=result.structuredContent.stdout.includes('Linux')?'df -h / | head -n 2':'uname -s';const next=await tool('/tools/call',{toolId:named('credential_request'),arguments:{credentialRef,operation:'ssh.exec',command},callId:'followup-from-output'});expect(next.structuredContent).toMatchObject({status:'complete',stdout:'Filesystem 40%\n'});results.push(next)}
|
|
|
}catch(e){integrationError=e}
|
|
}catch(e){integrationError=e}
|
|
|
ws.send(JSON.stringify({method:'event',params:{type:'message.complete',session_id:f.params.session_id,payload:{text:'本地测试登录完成,凭据未展示。',status:'complete'}}}))
|
|
ws.send(JSON.stringify({method:'event',params:{type:'message.complete',session_id:f.params.session_id,payload:{text:'本地测试登录完成,凭据未展示。',status:'complete'}}}))
|
|
|
})()
|
|
})()
|
|
@@ -116,12 +121,12 @@ it.each(cases)('a Bot turn discovers and uses $storage vault references for dumm
|
|
|
const run=runtime.send(owner,conversation.id,{requestId:randomUUID(),content:'请使用我授予当前任务的凭据引用完成本地测试登录。'})
|
|
const run=runtime.send(owner,conversation.id,{requestId:randomUUID(),content:'请使用我授予当前任务的凭据引用完成本地测试登录。'})
|
|
|
await vi.waitFor(()=>expect(['complete','failed']).toContain(store.require<WorkspaceRun>(owner,'run',run.id).status),{timeout:8000})
|
|
await vi.waitFor(()=>expect(['complete','failed']).toContain(store.require<WorkspaceRun>(owner,'run',run.id).status),{timeout:8000})
|
|
|
expect(integrationError).toBeUndefined();expect(store.require<WorkspaceRun>(owner,'run',run.id).status).toBe('complete')
|
|
expect(integrationError).toBeUndefined();expect(store.require<WorkspaceRun>(owner,'run',run.id).status).toBe('complete')
|
|
|
- expect(effectCount()).toBe(1);if(isWebsite){expect(website.state.proofs).toBe(cancelled?0:1);expect(website.state.logouts).toBe(cancelled?0:1)};expect(results).toHaveLength(1);expect(catalogs).toHaveLength(1)
|
|
|
|
|
|
|
+ expect(effectCount()).toBe(expectedEffects);if(isWebsite){expect(website.state.proofs).toBe(cancelled?0:1);expect(website.state.logouts).toBe(cancelled?0:1)};expect(results).toHaveLength(expectedEffects);expect(catalogs).toHaveLength(1)
|
|
|
for(const blob of [JSON.stringify(results),JSON.stringify(references),JSON.stringify(catalogs),JSON.stringify(prompts),JSON.stringify(store.messages(owner,conversation.id)),readFileSync(join(home,storage==='local'?'credential-vault':'vault','audit.jsonl'),'utf8')]){
|
|
for(const blob of [JSON.stringify(results),JSON.stringify(references),JSON.stringify(catalogs),JSON.stringify(prompts),JSON.stringify(store.messages(owner,conversation.id)),readFileSync(join(home,storage==='local'?'credential-vault':'vault','audit.jsonl'),'utf8')]){
|
|
|
- for(const privateValue of [secret,master,username,sessionCookie,token,'DUMMY-private-command-output','DUMMY-private-stderr','DUMMY-private-file-contents','DUMMY-fixed-transfer-body'])expect(blob).not.toContain(privateValue)
|
|
|
|
|
|
|
+ for(const privateValue of [secret,master,username,sessionCookie,token,...(persistent&&operation==='ssh.exec'?[]:['DUMMY-private-command-output','DUMMY-private-stderr']),'DUMMY-private-file-contents','DUMMY-fixed-transfer-body'])expect(blob).not.toContain(privateValue)
|
|
|
}
|
|
}
|
|
|
expect((await ui('')).body.leases).toHaveLength(0)
|
|
expect((await ui('')).body.leases).toHaveLength(0)
|
|
|
- if(firstUse){if(persistent)await ui('/unlock',{password:master}).expect(200);const saved=(await ui('')).body.entries.find((e:any)=>e.id===credentialRef);if(persistent){expect(saved.target.hostKey).toBeUndefined();expect((await (client as LocalCredentialVaultClient).call(owner,'','bot-refs',{agentId:persistentBotId}))[0].allowedTarget.hostKey).toBe(ssh.target.hostKey);expect(saved.revision).toBe(1)}else{expect(saved.target.hostKey).toBe(ssh.target.hostKey);expect(saved.revision).toBe(2)};expect(ssh.state.authMethods).toEqual(['password'])}
|
|
|
|
|
|
|
+ if(firstUse){if(persistent)await ui('/unlock',{password:master}).expect(200);const saved=(await ui('')).body.entries.find((e:any)=>e.id===credentialRef);if(persistent){expect(saved.target.hostKey).toBeUndefined();expect((await (client as LocalCredentialVaultClient).call(owner,'','bot-refs',{agentId:persistentBotId}))[0].allowedTarget.hostKey).toBe(ssh.target.hostKey);expect(saved.revision).toBe(1)}else{expect(saved.target.hostKey).toBe(ssh.target.hostKey);expect(saved.revision).toBe(2)};expect(ssh.state.authMethods).toEqual(Array(expectedEffects).fill('password'))}
|
|
|
}finally{
|
|
}finally{
|
|
|
runtime.close();coordinator.close();await new Promise(resolve=>setTimeout(resolve,10))
|
|
runtime.close();coordinator.close();await new Promise(resolve=>setTimeout(resolve,10))
|
|
|
for(const ws of hermes.clients)ws.terminate();await new Promise<void>(resolve=>hermes.close(()=>resolve()))
|
|
for(const ws of hermes.clients)ws.terminate();await new Promise<void>(resolve=>hermes.close(()=>resolve()))
|