79 lines
3.5 KiB
TypeScript
79 lines
3.5 KiB
TypeScript
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
|
|
import { CallId } from '@deepseek-ai/dsh-llm'
|
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
import ApprovalService, { type ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
|
|
import { makeBridgeHarness, type BridgeHarness } from './harness.ts'
|
|
|
|
describe('ACP machine permission policy', () => {
|
|
let harness: BridgeHarness | undefined
|
|
|
|
afterEach(async () => {
|
|
await harness?.dispose()
|
|
harness = undefined
|
|
})
|
|
|
|
async function ownedRequest(overrides: Partial<ApprovalRequest> = {}): Promise<ApprovalRequest> {
|
|
if (harness === undefined) throw new Error('missing harness')
|
|
await harness.ctx.plugin(ApprovalService)
|
|
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
|
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
|
|
const agent = harness.ctx.agents.get(SessionId(sessionId))!
|
|
agent.session.append('turn/start', { turn: 1 })
|
|
return { agent, toolName: 'bash', callId: CallId('call-9'), ...overrides }
|
|
}
|
|
|
|
it('maps the two advertised one-shot choices', async () => {
|
|
harness = await makeBridgeHarness()
|
|
harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'allow-once' } })
|
|
const request = await ownedRequest()
|
|
await expect(harness.ctx.approval.request(request)).resolves.toBe('allowed-once')
|
|
expect(harness.permissionRequests[0]).toMatchObject({
|
|
sessionId: request.agent.session.id,
|
|
toolCall: { toolCallId: 'call-9' },
|
|
options: [
|
|
{ optionId: 'allow-once', kind: 'allow_once' },
|
|
{ optionId: 'reject-once', kind: 'reject_once' },
|
|
],
|
|
})
|
|
|
|
harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'reject-once' } })
|
|
await expect(harness.ctx.approval.request(request)).resolves.toBe('rejected')
|
|
})
|
|
|
|
it('maps cancellation and unknown choices without granting access', async () => {
|
|
harness = await makeBridgeHarness()
|
|
const request = await ownedRequest()
|
|
await expect(harness.ctx.approval.request(request)).resolves.toBe('cancelled')
|
|
harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'unknown-grant' } })
|
|
await expect(harness.ctx.approval.request(request)).resolves.toBe('rejected')
|
|
})
|
|
|
|
it('fails closed when the client errors the permission request', async () => {
|
|
harness = await makeBridgeHarness()
|
|
const request = await ownedRequest()
|
|
harness.onPermission = () => { throw new Error('client gone') }
|
|
await expect(harness.ctx.approval.request(request)).resolves.toBe('unavailable')
|
|
})
|
|
|
|
it('delegates a same-id foreign agent', async () => {
|
|
harness = await makeBridgeHarness()
|
|
const request = await ownedRequest()
|
|
const foreign = {
|
|
session: { id: request.agent.session.id, events: [{ type: 'turn/start' }], append: () => ({}) },
|
|
} as unknown as Agent
|
|
await expect(harness.ctx.approval.request({ agent: foreign, toolName: 'bash', callId: CallId('call') }))
|
|
.resolves.toBe('unavailable')
|
|
expect(harness.permissionRequests).toHaveLength(0)
|
|
})
|
|
|
|
it('delegates requests that have no protocol tool-call identity', async () => {
|
|
harness = await makeBridgeHarness()
|
|
const request = await ownedRequest()
|
|
await expect(harness.ctx.approval.request({ agent: request.agent, toolName: request.toolName }))
|
|
.resolves.toBe('unavailable')
|
|
expect(harness.permissionRequests).toHaveLength(0)
|
|
})
|
|
})
|