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

Viewing files at commit 3f140fdd2001Commit detailsView latest

packages/sidecar/src/index.ts 103 lines · 3.2 KB · typescript
1
import type { AgentCard, AgentExtension } from '@a2a-js/sdk';
2
import {
3
  DEFAULT_EXTENSION_URI,
4
  type InteractionEvent,
5
  type TrustEnvelope,
6
  type PublicAgentCard,
7
} from '../../protocol/src/index.js';
8
import { TrustEngine, type PolicyContext } from '../../core/src/index.js';
9
10
export type A2AMessageLike = {
11
  role: 'ROLE_USER' | 'ROLE_AGENT' | 'user' | 'agent';
12
  parts: unknown[];
13
  extensions?: string[];
14
  metadata?: Record<string, unknown>;
15
};
16
17
export function openClaspA2AExtension(uri = DEFAULT_EXTENSION_URI): AgentExtension {
18
  return {
19
    uri,
20
    description: 'OpenClasp trust, contract, and behavioural-assurance envelope',
21
    required: false,
22
    params: {},
23
  };
24
}
25
26
export function toA2AAgentCard(card: PublicAgentCard): AgentCard {
27
  return {
28
    name: card.name,
29
    description: card.description || `${card.name} via OpenClasp`,
30
    supportedInterfaces: card.transports.map((transport) => ({
31
      url: transport.endpoint,
32
      protocolBinding: transport.protocolBinding,
33
      protocolVersion: '1.0',
34
      tenant: '',
35
    })),
36
    provider: undefined,
37
    version: card.agentVersion,
38
    documentationUrl: card.cardUrl,
39
    capabilities: {
40
      extensions: [
41
        {
42
          ...openClaspA2AExtension(card.extensionUri),
43
          params: {
44
            agentId: card.agentId,
45
            openClaspCardUrl: card.cardUrl,
46
          },
47
        },
48
      ],
49
    },
50
    securitySchemes: {},
51
    securityRequirements: [],
52
    defaultInputModes: ['text/plain', 'application/json'],
53
    defaultOutputModes: ['text/plain', 'application/json'],
54
    skills: card.capabilities.map((capability) => ({
55
      id:
56
        capability
57
          .toLowerCase()
58
          .replace(/[^a-z0-9]+/g, '-')
59
          .replace(/(^-|-$)/g, '') || 'general',
60
      name: capability,
61
      description: capability,
62
      tags: [card.framework],
63
      examples: [],
64
      inputModes: ['text/plain', 'application/json'],
65
      outputModes: ['text/plain', 'application/json'],
66
      securityRequirements: [],
67
    })),
68
    signatures: [],
69
  };
70
}
71
72
export class OpenClaspSidecar {
73
  constructor(
74
    readonly engine: TrustEngine,
75
    readonly extensionUri = DEFAULT_EXTENSION_URI,
76
  ) {}
77
78
  async forward<T>(input: {
79
    message: A2AMessageLike;
80
    envelope: TrustEnvelope;
81
    policy: Omit<PolicyContext, 'envelope'>;
82
    structuredEvents?: InteractionEvent[];
83
    send: (message: A2AMessageLike) => Promise<T>;
84
  }): Promise<{
85
    forwarded: boolean;
86
    decision: ReturnType<TrustEngine['assess']>;
87
    response?: T;
88
    networkPayloads: Record<string, unknown>[];
89
  }> {
90
    const embedded = input.message.metadata?.[this.extensionUri];
91
    if (!embedded) throw new Error('Missing OpenClasp A2A extension metadata');
92
    const decision = this.engine.assess({ envelope: input.envelope, ...input.policy });
93
    if (decision.decision === 'DENY') return { forwarded: false, decision, networkPayloads: [] };
94
    const networkPayloads = (input.structuredEvents ?? [])
95
      .map((event) => {
96
        this.engine.recordEvent(event);
97
        return this.engine.networkContribution(event);
98
      })
99
      .filter((value): value is Record<string, unknown> => value !== null);
100
    const response = await input.send(input.message);
101
    return { forwarded: true, decision, response, networkPayloads };
102
  }
103
}