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

Download ZIP
tests/agent-card-onboarding.test.ts 142 lines · 5.0 KB · typescript
1
import { describe, expect, it } from 'vitest';
2
import { buildApi } from '../apps/api/src/app.js';
3
import type { PublicAgentCard } from '../packages/protocol/src/index.js';
4
5
describe('connector-first Agent Card onboarding API', () => {
6
  it('accepts an agent profile, requires owner approval, and blocks publication until runtime verification', async () => {
7
    const rows: { kind: string; recordId: string; payload: any }[] = [];
8
    const cards = new Map<string, PublicAgentCard>();
9
    const claim = {
10
      claimId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
11
      status: 'pending' as const,
12
      runtimeEndpoint: 'https://agent.example/openclasp',
13
      expiresAt: new Date(Date.now() + 60_000).toISOString(),
14
      createdAt: new Date().toISOString(),
15
      profile: {
16
        description: 'Compares approved suppliers and prepares purchases',
17
        framework: 'LangGraph',
18
        agentVersion: '2.1.0',
19
        capabilities: ['compare supplier quotes'],
20
        limitations: ['requires human approval before payment'],
21
      },
22
    };
23
    let runtimeVerified = false;
24
    const repository = {
25
      list: async () => rows,
26
      upsert: async (_operatorId: string, kind: string, recordId: string, payload: any) => {
27
        const current = rows.find((row) => row.kind === kind && row.recordId === recordId);
28
        if (current) current.payload = payload;
29
        else rows.push({ kind, recordId, payload });
30
      },
31
      createConnectorClaim: async () => ({ ...claim, claimSecret: 'oc_cc_secret' }),
32
      getConnectorClaim: async () => claim,
33
      approveConnectorClaim: async (_owner: string, _claimId: string, name: string) => {
34
        const agent = {
35
          agentId: 'agent-connected',
36
          projectId: 'project-connected',
37
          ...claim.profile,
38
          name,
39
          nameProvenance: 'operator_attested',
40
          profileProvenance: 'self_declared',
41
          identityMode: 'connector_claim',
42
          status: 'active',
43
          autoPublish: false,
44
          autoAcceptPolicy: 'off',
45
          autoAcceptTaskCategories: [],
46
          createdAt: new Date().toISOString(),
47
          updatedAt: new Date().toISOString(),
48
        };
49
        rows.push({ kind: 'agent_profile', recordId: agent.agentId, payload: agent });
50
        return { ...claim, status: 'approved', agentId: agent.agentId, agent };
51
      },
52
      rejectConnectorClaim: async () => ({ ...claim, status: 'rejected' }),
53
      listAgentRuntimes: async () =>
54
        runtimeVerified
55
          ? [{ agentId: 'agent-connected', status: 'verified', endpoint: claim.runtimeEndpoint }]
56
          : [],
57
      publishAgent: async (_operatorId: string, card: PublicAgentCard) => {
58
        cards.set(card.agentId, card);
59
        return card;
60
      },
61
      unpublishAgent: async (_operatorId: string, agentId: string) => cards.delete(agentId),
62
      getPublishedAgent: async (agentId: string) => cards.get(agentId),
63
    } as any;
64
    const app = buildApi(undefined, undefined, repository);
65
    await app.ready();
66
    const headers = { 'x-openclasp-operator': 'owner-a' };
67
68
    const start = await app.inject({
69
      method: 'POST',
70
      url: '/v0.1/connector-claims',
71
      payload: {
72
        runtimeEndpoint: claim.runtimeEndpoint,
73
        credentialPublicKey: 'public-key',
74
        profile: claim.profile,
75
      },
76
    });
77
    expect(start.statusCode).toBe(200);
78
    expect(start.json()).toMatchObject({
79
      claimId: claim.claimId,
80
      claimSecret: 'oc_cc_secret',
81
      confirmationUrl: expect.stringContaining(`/connect?claim=${claim.claimId}`),
82
    });
83
84
    const missingName = await app.inject({
85
      method: 'POST',
86
      url: `/v0.1/connector-claims/${claim.claimId}/approve`,
87
      headers,
88
      payload: {},
89
    });
90
    expect(missingName.statusCode).toBe(400);
91
92
    const approved = await app.inject({
93
      method: 'POST',
94
      url: `/v0.1/connector-claims/${claim.claimId}/approve`,
95
      headers,
96
      payload: { name: 'Purchasing agent' },
97
    });
98
    expect(approved.statusCode).toBe(200);
99
    expect(approved.json()).toMatchObject({
100
      status: 'approved',
101
      agent: {
102
        name: 'Purchasing agent',
103
        identityMode: 'connector_claim',
104
        nameProvenance: 'operator_attested',
105
        profileProvenance: 'self_declared',
106
      },
107
    });
108
109
    expect(
110
      (
111
        await app.inject({
112
          method: 'POST',
113
          url: '/v0.1/agents/agent-connected/publication',
114
          headers,
115
          payload: { published: true },
116
        })
117
      ).json(),
118
    ).toEqual({ error: 'Verify the agent runtime before publishing' });
119
120
    runtimeVerified = true;
121
    const publication = await app.inject({
122
      method: 'POST',
123
      url: '/v0.1/agents/agent-connected/publication',
124
      headers,
125
      payload: { published: true },
126
    });
127
    expect(publication.statusCode).toBe(200);
128
    expect(publication.json()).toMatchObject({ published: true, card: { transports: [] } });
129
130
    expect(
131
      (
132
        await app.inject({
133
          method: 'POST',
134
          url: '/v0.1/quickstart/agent',
135
          headers,
136
          payload: {},
137
        })
138
      ).statusCode,
139
    ).toBe(404);
140
    await app.close();
141
  });
142
});