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

Viewing files at commit 989559e001cdCommit detailsView latest

packages/sdk/src/index.ts 202 lines · 6.9 KB · typescript
1
import {
2
  canonicalHash,
3
  signObject,
4
  type InteractionEvent,
5
  type KeyPair,
6
  type TrustEnvelope,
7
  type FederatedInteraction,
8
  type PublicAgentCard,
9
  RuntimeDeliverySchema,
10
  type RuntimeDelivery,
11
} from '../../protocol/src/index.js';
12
import { createHmac, timingSafeEqual } from 'node:crypto';
13
export { createIdentity } from '../../core/src/index.js';
14
export * from '../../protocol/src/index.js';
15
16
export class OpenClaspClient {
17
  constructor(
18
    readonly baseUrl = 'http://localhost:3100/v0.1',
19
    readonly accessToken?: string,
20
  ) {}
21
  private async request<T>(path: string, init?: RequestInit): Promise<T> {
22
    const response = await fetch(`${this.baseUrl}${path}`, {
23
      ...init,
24
      headers: {
25
        'content-type': 'application/json',
26
        ...(this.accessToken ? { authorization: `Bearer ${this.accessToken}` } : {}),
27
        ...init?.headers,
28
      },
29
    });
30
    const body = (await response.json()) as any;
31
    if (!response.ok) throw new Error(body.error ?? `OpenClasp request failed: ${response.status}`);
32
    return body as T;
33
  }
34
  registerAgent(identity: unknown) {
35
    return this.request('/agents', { method: 'POST', body: JSON.stringify(identity) });
36
  }
37
  getProfile(agentId: string, version: string, taskCategory: string) {
38
    return this.request(
39
      `/profiles/${encodeURIComponent(agentId)}?version=${encodeURIComponent(version)}&taskCategory=${encodeURIComponent(taskCategory)}`,
40
    );
41
  }
42
  saveContract(contract: unknown) {
43
    return this.request('/interactions/contracts', {
44
      method: 'POST',
45
      body: JSON.stringify(contract),
46
    });
47
  }
48
  assess(input: unknown) {
49
    return this.request('/risk/assess', { method: 'POST', body: JSON.stringify(input) });
50
  }
51
  recordEvent(event: unknown) {
52
    return this.request('/events', { method: 'POST', body: JSON.stringify(event) });
53
  }
54
  checkClaim(claim: string) {
55
    return this.request('/claims/check', { method: 'POST', body: JSON.stringify({ claim }) });
56
  }
57
  submitReceipt(receipt: unknown) {
58
    return this.request('/receipts', { method: 'POST', body: JSON.stringify(receipt) });
59
  }
60
  verifyReceipt(receipt: unknown) {
61
    return this.request('/receipts/verify', { method: 'POST', body: JSON.stringify(receipt) });
62
  }
63
  submitFeedback(feedback: unknown) {
64
    return this.request('/feedback', { method: 'POST', body: JSON.stringify(feedback) });
65
  }
66
  setContributionConsent(agentId: string, enabled: boolean) {
67
    return this.request('/contributions/consent', {
68
      method: 'POST',
69
      body: JSON.stringify({ agentId, enabled }),
70
    });
71
  }
72
  getPublicAgentCard(agentId: string): Promise<PublicAgentCard> {
73
    const root = this.baseUrl.replace(/\/v0\.1\/?$/, '');
74
    return fetch(`${root}/agents/${encodeURIComponent(agentId)}/card.json`).then(
75
      async (response) => {
76
        const body = (await response.json()) as any;
77
        if (!response.ok)
78
          throw new Error(body.error ?? `OpenClasp request failed: ${response.status}`);
79
        return body as PublicAgentCard;
80
      },
81
    );
82
  }
83
  createFederatedInteraction(value: FederatedInteraction) {
84
    return this.request<FederatedInteraction>('/federated-interactions', {
85
      method: 'POST',
86
      body: JSON.stringify(value),
87
    });
88
  }
89
  listFederatedInteractions() {
90
    return this.request<FederatedInteraction[]>('/federated-interactions');
91
  }
92
  getFederatedInteraction(interactionId: string) {
93
    return this.request<FederatedInteraction>(
94
      `/federated-interactions/${encodeURIComponent(interactionId)}`,
95
    );
96
  }
97
  respondToFederatedInteraction(
98
    interactionId: string,
99
    agentId: string,
100
    decision: 'accept' | 'reject',
101
  ) {
102
    return this.request<FederatedInteraction>(
103
      `/federated-interactions/${encodeURIComponent(interactionId)}/respond`,
104
      { method: 'POST', body: JSON.stringify({ agentId, decision }) },
105
    );
106
  }
107
}
108
109
export function createSignedEvent(
110
  input: Omit<InteractionEvent, 'payloadHash' | 'signature'>,
111
  key: KeyPair,
112
): InteractionEvent {
113
  return signObject(
114
    { ...input, payloadHash: canonicalHash(input.payload) },
115
    key,
116
  ) as InteractionEvent;
117
}
118
119
export function createSignedEnvelope(
120
  input: Omit<TrustEnvelope, 'signature'>,
121
  key: KeyPair,
122
): TrustEnvelope {
123
  return signObject(input, key) as TrustEnvelope;
124
}
125
126
function validRuntimeSignature(
127
  secret: string,
128
  deliveryId: string,
129
  timestamp: string,
130
  body: string,
131
  signature: string,
132
) {
133
  if (Math.abs(Date.now() - Date.parse(timestamp)) > 5 * 60_000) return false;
134
  const expected = createHmac('sha256', secret)
135
    .update(`${timestamp}.${deliveryId}.${body}`)
136
    .digest();
137
  const actual = Buffer.from(signature.replace(/^v1=/, ''), 'base64url');
138
  return actual.length === expected.length && timingSafeEqual(actual, expected);
139
}
140
141
export function createOpenClaspRuntimeHandler(input: {
142
  signingSecret: string;
143
  onDelivery: (delivery: RuntimeDelivery) => Promise<void> | void;
144
}) {
145
  return async (request: Request): Promise<Response> => {
146
    const body = await request.text();
147
    let parsed: Record<string, unknown>;
148
    try {
149
      parsed = JSON.parse(body) as Record<string, unknown>;
150
    } catch {
151
      return Response.json({ error: 'invalid_json' }, { status: 400 });
152
    }
153
    if (parsed.type === 'openclasp.runtime.verify')
154
      return Response.json({
155
        type: 'openclasp.runtime.verified',
156
        version: '1',
157
        agentId: parsed.agentId,
158
        challenge: parsed.challenge,
159
      });
160
    const deliveryId = request.headers.get('openclasp-delivery-id') ?? '';
161
    const timestamp = request.headers.get('openclasp-timestamp') ?? '';
162
    const signature = request.headers.get('openclasp-signature') ?? '';
163
    if (
164
      !deliveryId ||
165
      !timestamp ||
166
      !signature ||
167
      !validRuntimeSignature(input.signingSecret, deliveryId, timestamp, body, signature)
168
    )
169
      return Response.json({ error: 'invalid_openclasp_signature' }, { status: 401 });
170
    const deliveryResult = RuntimeDeliverySchema.safeParse(parsed);
171
    if (!deliveryResult.success)
172
      return Response.json({ error: 'invalid_runtime_delivery' }, { status: 400 });
173
    const delivery = deliveryResult.data;
174
    if (delivery.deliveryId !== deliveryId)
175
      return Response.json({ error: 'delivery_id_mismatch' }, { status: 400 });
176
    await input.onDelivery(delivery);
177
    return Response.json({ accepted: true, deliveryId }, { status: 202 });
178
  };
179
}
180
181
export async function sendOpenClaspRuntimeReply(
182
  delivery: RuntimeDelivery,
183
  payload: unknown,
184
  requestId = crypto.randomUUID(),
185
) {
186
  const response = await fetch(delivery.reply.endpoint, {
187
    method: 'POST',
188
    headers: {
189
      authorization: `Bearer ${delivery.reply.bearerToken}`,
190
      'content-type': 'application/json',
191
    },
192
    body: JSON.stringify({
193
      jsonrpc: '2.0',
194
      id: requestId,
195
      method: 'message/send',
196
      params: { message: payload },
197
    }),
198
  });
199
  const result = (await response.json()) as unknown;
200
  if (!response.ok) throw new Error(`OpenClasp reply failed with HTTP ${response.status}`);
201
  return result;
202
}