The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 90ba0b085978Commit detailsView latest

tests/onboarding.test.ts 151 lines · 5.8 KB · typescript
1
import { describe, expect, it } from 'vitest';
2
import {
3
  approveAgentSetup,
4
  createHostedProviderAgent,
5
  getOnboardingState,
6
  requestAgentSetup,
7
  resolveInstallation,
8
  updateAgentProfile,
9
  type OnboardingKind,
10
  type OnboardingStore,
11
} from '../packages/persistence/src/onboarding.js';
12
13
class MemoryOnboardingStore implements OnboardingStore {
14
  private rows = new Map<string, { kind: OnboardingKind; recordId: string; payload: unknown }>();
15
16
  async list(operatorId: string) {
17
    return [...this.rows.entries()]
18
      .filter(([key]) => key.startsWith(`${operatorId}|`))
19
      .map(([, row]) => row);
20
  }
21
22
  async upsert(operatorId: string, kind: OnboardingKind, recordId: string, payload: unknown) {
23
    this.rows.set(`${operatorId}|${kind}|${recordId}`, { kind, recordId, payload });
24
  }
25
}
26
27
describe('agent self-onboarding', () => {
28
  it('creates a separate owner-managed identity for a hosted provider', async () => {
29
    const store = new MemoryOnboardingStore();
30
    const created = await createHostedProviderAgent(store, 'owner', {
31
      provider: 'botpress',
32
      agentName: 'Recruiting agent',
33
      projectName: 'Recruiting',
34
      description: 'Matches candidates to roles',
35
      capabilities: ['candidate matching'],
36
      limitations: ['no final hiring decisions'],
37
    });
38
    expect(created.agent).toMatchObject({
39
      name: 'Recruiting agent',
40
      framework: 'Botpress',
41
      identityMode: 'owner_managed',
42
      agentMode: 'persistent_runtime',
43
      autoPublish: false,
44
      capabilities: ['candidate matching'],
45
    });
46
    const state = await getOnboardingState(store, 'owner');
47
    expect(state.projects).toEqual([expect.objectContaining({ name: 'Recruiting' })]);
48
    expect(state.agentProfiles).toEqual([
49
      expect.objectContaining({ agentId: created.agent.agentId }),
50
    ]);
51
    expect(state.installations).toEqual([]);
52
  });
53
54
  it('requires owner confirmation before binding an installation', async () => {
55
    const store = new MemoryOnboardingStore();
56
    const request = await requestAgentSetup(store, 'owner-a', {
57
      clientId: 'codex-installation-a',
58
      agentName: 'Research agent',
59
      projectName: 'Market research',
60
      framework: 'Codex',
61
      capabilities: ['research'],
62
      limitations: ['No purchases'],
63
      description: 'Browses public sources',
64
      agentVersion: '2.0.0',
65
    });
66
67
    expect(request.status).toBe('pending');
68
    expect(request).toMatchObject({
69
      autoPublish: true,
70
      autoAcceptPolicy: 'safe_matching',
71
      autoAcceptTaskCategories: ['research'],
72
    });
73
    await expect(resolveInstallation(store, 'owner-a', 'codex-installation-a')).resolves.toEqual({
74
      status: 'unbound',
75
    });
76
77
    const approved = await approveAgentSetup(store, 'owner-a', request.requestId);
78
    expect(approved).toMatchObject({
79
      status: 'connected',
80
      agent: {
81
        name: 'Research agent',
82
        capabilities: ['research'],
83
        agentVersion: '2.0.0',
84
        transport: 'direct_a2a',
85
        agentMode: 'temporary_chat',
86
      },
87
      project: { name: 'Market research' },
88
    });
89
    await expect(
90
      resolveInstallation(store, 'owner-a', 'codex-installation-a'),
91
    ).resolves.toMatchObject({ status: 'connected', agent: { name: 'Research agent' } });
92
    await expect(resolveInstallation(store, 'owner-b', 'codex-installation-a')).resolves.toEqual({
93
      status: 'unbound',
94
    });
95
  });
96
97
  it('supports unrelated agents and confirmed installation switching', async () => {
98
    const store = new MemoryOnboardingStore();
99
    const first = await requestAgentSetup(store, 'owner', {
100
      clientId: 'client-a',
101
      agentName: 'Coding agent',
102
      projectName: 'Product A',
103
    });
104
    const second = await requestAgentSetup(store, 'owner', {
105
      clientId: 'client-b',
106
      agentName: 'Support agent',
107
      projectName: 'Product B',
108
    });
109
    const firstBinding = await approveAgentSetup(store, 'owner', first.requestId);
110
    const secondBinding = await approveAgentSetup(store, 'owner', second.requestId);
111
    if (firstBinding.status !== 'connected' || secondBinding.status !== 'connected')
112
      throw new Error('Expected connected agents');
113
114
    const switchRequest = await requestAgentSetup(store, 'owner', {
115
      clientId: 'client-a',
116
      action: 'switch',
117
      existingAgentId: secondBinding.agent.agentId,
118
    });
119
    const beforeSwitch = await resolveInstallation(store, 'owner', 'client-a');
120
    expect(beforeSwitch.status).toBe('connected');
121
    if (beforeSwitch.status !== 'connected') throw new Error('Expected connected installation');
122
    expect(beforeSwitch.agent.agentId).toBe(firstBinding.agent.agentId);
123
    await approveAgentSetup(store, 'owner', switchRequest.requestId);
124
    const afterSwitch = await resolveInstallation(store, 'owner', 'client-a');
125
    expect(afterSwitch.status).toBe('connected');
126
    if (afterSwitch.status !== 'connected') throw new Error('Expected connected installation');
127
    expect(afterSwitch.agent.agentId).toBe(secondBinding.agent.agentId);
128
    const state = await getOnboardingState(store, 'owner');
129
    expect(state.projects).toHaveLength(2);
130
    expect(state.agentProfiles).toHaveLength(2);
131
  });
132
133
  it('updates only the profile bound to the authenticated installation', async () => {
134
    const store = new MemoryOnboardingStore();
135
    const request = await requestAgentSetup(store, 'owner', {
136
      clientId: 'client-a',
137
      agentName: 'Planner',
138
      projectName: 'Launch',
139
    });
140
    await approveAgentSetup(store, 'owner', request.requestId);
141
    const updated = await updateAgentProfile(store, 'owner', 'client-a', {
142
      capabilities: ['planning', 'coordination'],
143
    });
144
    expect(updated.capabilities).toEqual(['planning', 'coordination']);
145
    expect(updated.transport).toBe('direct_a2a');
146
    expect(updated.agentMode).toBe('temporary_chat');
147
    await expect(
148
      updateAgentProfile(store, 'owner', 'different-client', { name: 'Hijacked' }),
149
    ).rejects.toThrow('not connected');
150
  });
151
});