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

Viewing files at commit 32cb43fd4001Commit detailsView latest

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