|
@@ -20,11 +20,15 @@ import { CredentialVaultBroker } from '../../src/server/credentialVault/broker'
|
|
|
import { CredentialVaultClient, serveCredentialVault } from '../../src/server/credentialVault/transport'
|
|
import { CredentialVaultClient, serveCredentialVault } from '../../src/server/credentialVault/transport'
|
|
|
import { CredentialVaultCoordinator } from '../../src/server/credentialVault/coordinator'
|
|
import { CredentialVaultCoordinator } from '../../src/server/credentialVault/coordinator'
|
|
|
import { ProtectedCredentialExecutor } from '../../src/server/credentialVault/protectedExecutor'
|
|
import { ProtectedCredentialExecutor } from '../../src/server/credentialVault/protectedExecutor'
|
|
|
|
|
+import { LocalCredentialVaultClient } from '../../src/server/credentialVault/localClient'
|
|
|
import { websiteFixture, sshFixture } from '../fixtures/credentialProtocols'
|
|
import { websiteFixture, sshFixture } from '../fixtures/credentialProtocols'
|
|
|
-import type { RunnerHub } from '../../src/server/runnerHub'
|
|
|
|
|
|
|
+import { RunnerHub } from '../../src/server/runnerHub'
|
|
|
|
|
+import type { Server } from 'node:http'
|
|
|
import type { WorkspaceRun } from '../../src/shared/workspace'
|
|
import type { WorkspaceRun } from '../../src/shared/workspace'
|
|
|
|
|
|
|
|
-it.each(['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'] as const)('a Bot turn uses its real tool catalog, task approval and protected adapter for local dummy %s, returning only a sanitized receipt',async scenario=>{
|
|
|
|
|
|
|
+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'] as const
|
|
|
|
|
+const cases=(['external','local'] as const).flatMap(storage=>scenarios.map(scenario=>({storage,scenario})))
|
|
|
|
|
+it.each(cases)('a Bot turn discovers and uses $storage vault references for dummy $scenario with task approval and sanitized receipts',async({storage,scenario})=>{
|
|
|
const cancelled=scenario.endsWith('.cancel'),operation=scenario.replace('.password','').replace(/\.cancel$/,'') as 'website.login'|'ssh.exec'|'sftp.read'|'sftp.write'
|
|
const cancelled=scenario.endsWith('.cancel'),operation=scenario.replace('.password','').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')
|
|
@@ -39,7 +43,7 @@ it.each(['website.login','ssh.exec','sftp.read','sftp.write','website.login.canc
|
|
|
const user=auth.setupAdmin(login,'dummy-admin','DUMMY-local-account-password'),cookies=String(loginHeaders['set-cookie']).split(';')[0]!,owner=user.id
|
|
const user=auth.setupAdmin(login,'dummy-admin','DUMMY-local-account-password'),cookies=String(loginHeaders['set-cookie']).split(';')[0]!,owner=user.id
|
|
|
saveHostTools(home,{managedBrowser:false,vm:false,serverComputer:false,scriptMachine:false,cloud:false})
|
|
saveHostTools(home,{managedBrowser:false,vm:false,serverComputer:false,scriptMachine:false,cloud:false})
|
|
|
const hermes=new WebSocketServer({port:0,host:'127.0.0.1'});await once(hermes,'listening')
|
|
const hermes=new WebSocketServer({port:0,host:'127.0.0.1'});await once(hermes,'listening')
|
|
|
- const bindings=new Map<string,any>(),prompts:string[]=[],results:any[]=[],catalogs:any[][]=[]
|
|
|
|
|
|
|
+ const bindings=new Map<string,any>(),prompts:string[]=[],results:any[]=[],catalogs:any[][]=[],references:any[]=[]
|
|
|
let integrationError:unknown,credentialRef=''
|
|
let integrationError:unknown,credentialRef=''
|
|
|
const target={url:new URL('http://127.0.0.1:'+(hermes.address() as any).port),client:{directAgent:undefined},session:{webSocketCredential:async()=>({name:'ticket',value:'dummy-ticket'}),request:async(path:string,input?:{body?:any})=>{
|
|
const target={url:new URL('http://127.0.0.1:'+(hermes.address() as any).port),client:{directAgent:undefined},session:{webSocketCredential:async()=>({name:'ticket',value:'dummy-ticket'}),request:async(path:string,input?:{body?:any})=>{
|
|
|
if(path.endsWith('/bind'))bindings.set(input?.body.session_id,input?.body)
|
|
if(path.endsWith('/bind'))bindings.set(input?.body.session_id,input?.body)
|
|
@@ -48,12 +52,19 @@ it.each(['website.login','ssh.exec','sftp.read','sftp.write','website.login.canc
|
|
|
}}} as unknown as GatewayTarget
|
|
}}} as unknown as GatewayTarget
|
|
|
const nodes=new WorkspaceNodes(store,loadServerConfig({HERMES_YAOYAO_HOME:home}),target),runtime=new WorkspaceRuntime(store,nodes,uploads,owner=>auth.isUserActive(owner),owner=>auth.pushAuthorizationVersion(owner)??0)
|
|
const nodes=new WorkspaceNodes(store,loadServerConfig({HERMES_YAOYAO_HOME:home}),target),runtime=new WorkspaceRuntime(store,nodes,uploads,owner=>auth.isUserActive(owner),owner=>auth.pushAuthorizationVersion(owner)??0)
|
|
|
const socket=join(home,'dummy-vault.sock'),tokenFile=join(home,'dummy-control-token'),token=randomBytes(32).toString('base64url')
|
|
const socket=join(home,'dummy-vault.sock'),tokenFile=join(home,'dummy-control-token'),token=randomBytes(32).toString('base64url')
|
|
|
- writeFileSync(tokenFile,token,{mode:0o600})
|
|
|
|
|
- const vault=await CredentialVaultStore.create(join(home,'vault')),broker=new CredentialVaultBroker(vault,Date.now,new ProtectedCredentialExecutor(join(home,'private-executor'),{assert(){}},website.cert)),ipc=serveCredentialVault(broker,token)
|
|
|
|
|
- ipc.listen(socket);await once(ipc,'listening');chmodSync(socket,0o600)
|
|
|
|
|
- const client=new CredentialVaultClient({socket,tokenFile,hermesUid:process.getuid!()+1}),binding={runnerId:randomUUID(),runnerInstance:randomUUID(),runnerEpoch:'dummy-binding-epoch'}
|
|
|
|
|
|
|
+ let client:CredentialVaultClient,ipc:Server|undefined,hub:RunnerHub|undefined
|
|
|
|
|
+ if(storage==='local'){
|
|
|
|
|
+ client=new LocalCredentialVaultClient(home,website.cert)
|
|
|
|
|
+ hub=new RunnerHub(store,auth,target) // The default direct Hermes setup has no registered Runner.
|
|
|
|
|
+ }else{
|
|
|
|
|
+ writeFileSync(tokenFile,token,{mode:0o600})
|
|
|
|
|
+ const vault=await CredentialVaultStore.create(join(home,'vault')),broker=new CredentialVaultBroker(vault,Date.now,new ProtectedCredentialExecutor(join(home,'private-executor'),{assert(){}},website.cert))
|
|
|
|
|
+ ipc=serveCredentialVault(broker,token);ipc.listen(socket);await once(ipc,'listening');chmodSync(socket,0o600)
|
|
|
|
|
+ client=new CredentialVaultClient({socket,tokenFile,hermesUid:process.getuid!()+1})
|
|
|
|
|
+ }
|
|
|
|
|
+ const binding={runnerId:randomUUID(),runnerInstance:randomUUID(),runnerEpoch:'dummy-binding-epoch'}
|
|
|
// This same-process dummy wiring exercises protocol and turn routing, not OS isolation.
|
|
// This same-process dummy wiring exercises protocol and turn routing, not OS isolation.
|
|
|
- const coordinator=new CredentialVaultCoordinator(client,store,auth,{credentialBinding:()=>binding} as unknown as RunnerHub);runtime.credentialVault=coordinator
|
|
|
|
|
|
|
+ const coordinator=new CredentialVaultCoordinator(client,store,auth,hub??{credentialBinding:()=>binding} as unknown as RunnerHub);runtime.credentialVault=coordinator
|
|
|
const app=new Koa();app.use(async(ctx,next)=>{try{await next()}catch(e){ctx.status=(e as any).status??500;ctx.body={code:(e as any).code}}})
|
|
const app=new Koa();app.use(async(ctx,next)=>{try{await next()}catch(e){ctx.status=(e as any).status??500;ctx.body={code:(e as any).code}}})
|
|
|
app.use(async(ctx,next)=>{const chunks:Buffer[]=[];for await(const c of ctx.req)chunks.push(Buffer.from(c));(ctx.request as any).body=chunks.length?JSON.parse(Buffer.concat(chunks).toString()):{};await next()})
|
|
app.use(async(ctx,next)=>{const chunks:Buffer[]=[];for await(const c of ctx.req)chunks.push(Buffer.from(c));(ctx.request as any).body=chunks.length?JSON.parse(Buffer.concat(chunks).toString()):{};await next()})
|
|
|
const router=coordinator.router();app.use(router.routes())
|
|
const router=coordinator.router();app.use(router.routes())
|
|
@@ -73,8 +84,9 @@ it.each(['website.login','ssh.exec','sftp.read','sftp.write','website.login.canc
|
|
|
const catalog=await tool('/tools/list',{});catalogs.push(catalog.tools)
|
|
const catalog=await tool('/tools/list',{});catalogs.push(catalog.tools)
|
|
|
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()
|
|
|
- expect((await tool('/tools/call',{toolId:named('credential_refs'),arguments:{},callId:'before-approval'})).structuredContent.references).toEqual([])
|
|
|
|
|
- const call={toolId:named('credential_request'),arguments:{credentialRef,operation},callId:'single-dummy-login'}
|
|
|
|
|
|
|
+ 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:false});references.push(discovered)
|
|
|
|
|
+ const call={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)
|
|
|
let view:any
|
|
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)})
|
|
@@ -98,14 +110,14 @@ it.each(['website.login','ssh.exec','sftp.read','sftp.write','website.login.canc
|
|
|
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(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)
|
|
|
- for(const blob of [JSON.stringify(results),JSON.stringify(catalogs),JSON.stringify(prompts),JSON.stringify(store.messages(owner,conversation.id)),readFileSync(join(home,'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,'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)
|
|
|
}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()))
|
|
|
- await new Promise<void>(resolve=>ipc.close(()=>resolve()));await website.close();await ssh.close()
|
|
|
|
|
|
|
+ if(ipc)await new Promise<void>(resolve=>ipc!.close(()=>resolve()));hub?.close();await website.close();await ssh.close()
|
|
|
nodes.close();uploads.close();store.close();rmSync(home,{recursive:true,force:true})
|
|
nodes.close();uploads.close();store.close();rmSync(home,{recursive:true,force:true})
|
|
|
}
|
|
}
|
|
|
},15000)
|
|
},15000)
|