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

Viewing files at commit 6b91d3cf5293Commit detailsView latest

packages/persistence/src/source-record.ts 181 lines · 4.9 KB · typescript
1
import { randomUUID } from 'node:crypto';
2
import {
3
  LearningScopeSchema,
4
  ProvenanceSchema,
5
  SourceRecordEnvelopeSchema,
6
  canonicalHash,
7
  type LearningScope,
8
  type RetentionClass,
9
  type SourceRecordEnvelope,
10
} from '../../protocol/src/index.js';
11
12
export type SourceRecordMetadata = Partial<
13
  Pick<
14
    SourceRecordEnvelope,
15
    | 'eventId'
16
    | 'schemaName'
17
    | 'schemaVersion'
18
    | 'entityRefs'
19
    | 'provenance'
20
    | 'visibility'
21
    | 'retentionClass'
22
    | 'learningScope'
23
    | 'reportedAt'
24
    | 'ingestedAt'
25
  >
26
>;
27
28
export type SourceRecordWriteMetadata = SourceRecordMetadata & { journal?: boolean };
29
30
export const SOURCE_JOURNAL_RECORD_KINDS = new Set([
31
  'agent',
32
  'agent_profile',
33
  'completion_report',
34
  'conflict',
35
  'consent',
36
  'contract',
37
  'counterparty_brief',
38
  'delegation',
39
  'event',
40
  'feedback',
41
  'interaction',
42
  'interaction_conclusion',
43
  'interaction_feedback',
44
  'learning_eligibility',
45
  'federated_interaction',
46
  'live_session_event',
47
  'live_session_state',
48
  'profile_delta',
49
  'receipt',
50
  'revocation',
51
]);
52
53
export function shouldJournalSourceRecord(kind: string): boolean {
54
  return SOURCE_JOURNAL_RECORD_KINDS.has(kind);
55
}
56
57
const ACCOUNT_RECORD_KINDS = new Set([
58
  'agent_profile',
59
  'installation',
60
  'presence',
61
  'project',
62
  'publication',
63
  'setup_request',
64
]);
65
66
const OPERATIONAL_RECORD_KINDS = new Set(['counterparty_brief', 'feedback_request', 'profile']);
67
68
const SHARED_RECORD_KINDS = new Set([
69
  'completion_report',
70
  'contract',
71
  'federated_interaction',
72
  'interaction_conclusion',
73
  'learning_eligibility',
74
  'live_session_event',
75
  'live_session_state',
76
  'receipt',
77
]);
78
79
const ENTITY_REFERENCE_FIELDS = [
80
  'agentId',
81
  'contractHash',
82
  'counterpartyAgentId',
83
  'decisionId',
84
  'deploymentId',
85
  'interactionId',
86
  'projectId',
87
  'reportId',
88
  'reportingAgentId',
89
  'requestId',
90
  'reviewerAgentId',
91
  'subjectAgentId',
92
] as const;
93
94
const REPORTED_AT_FIELDS = [
95
  'occurredAt',
96
  'completedAt',
97
  'decidedAt',
98
  'appliedAt',
99
  'generatedAt',
100
  'createdAt',
101
  'requestedAt',
102
  'updatedAt',
103
] as const;
104
105
function payloadObject(payload: unknown): Record<string, unknown> | undefined {
106
  return payload !== null && typeof payload === 'object' && !Array.isArray(payload)
107
    ? (payload as Record<string, unknown>)
108
    : undefined;
109
}
110
111
function defaultRetentionClass(kind: string): RetentionClass {
112
  if (ACCOUNT_RECORD_KINDS.has(kind)) return 'account';
113
  if (OPERATIONAL_RECORD_KINDS.has(kind)) return 'operational';
114
  return 'audit';
115
}
116
117
function inferredEntityRefs(payload: unknown): Record<string, string> {
118
  const object = payloadObject(payload);
119
  if (!object) return {};
120
  return Object.fromEntries(
121
    ENTITY_REFERENCE_FIELDS.flatMap((field) => {
122
      const value = object[field];
123
      return typeof value === 'string' && value ? [[field, value]] : [];
124
    }),
125
  );
126
}
127
128
function inferredProvenance(payload: unknown): SourceRecordEnvelope['provenance'] {
129
  const value = payloadObject(payload)?.provenance;
130
  const parsed = ProvenanceSchema.safeParse(value);
131
  return parsed.success ? parsed.data : 'observed';
132
}
133
134
function inferredLearningScope(payload: unknown): LearningScope {
135
  const object = payloadObject(payload);
136
  const candidate = object?.contributionMode ?? object?.learningScope;
137
  const parsed = LearningScopeSchema.safeParse(candidate);
138
  return parsed.success ? parsed.data : 'not_evaluated';
139
}
140
141
function inferredReportedAt(payload: unknown, fallback: string): string {
142
  const object = payloadObject(payload);
143
  if (!object) return fallback;
144
  for (const field of REPORTED_AT_FIELDS) {
145
    const value = object[field];
146
    if (typeof value === 'string' && Number.isFinite(Date.parse(value))) return value;
147
  }
148
  return fallback;
149
}
150
151
export function buildSourceRecordEnvelope(input: {
152
  operatorId: string;
153
  kind: string;
154
  recordId: string;
155
  payload: unknown;
156
  metadata?: SourceRecordMetadata;
157
}): SourceRecordEnvelope {
158
  const ingestedAt = input.metadata?.ingestedAt ?? new Date().toISOString();
159
  return SourceRecordEnvelopeSchema.parse({
160
    eventId: input.metadata?.eventId ?? randomUUID(),
161
    operatorId: input.operatorId,
162
    kind: input.kind,
163
    recordId: input.recordId,
164
    schemaName: input.metadata?.schemaName ?? `openclasp.${input.kind}`,
165
    schemaVersion: input.metadata?.schemaVersion ?? '1',
166
    payload: input.payload,
167
    payloadDigest: canonicalHash(input.payload),
168
    entityRefs: {
169
      ...inferredEntityRefs(input.payload),
170
      ...input.metadata?.entityRefs,
171
    },
172
    provenance: input.metadata?.provenance ?? inferredProvenance(input.payload),
173
    visibility:
174
      input.metadata?.visibility ??
175
      (SHARED_RECORD_KINDS.has(input.kind) ? 'shared_participants' : 'local_only'),
176
    retentionClass: input.metadata?.retentionClass ?? defaultRetentionClass(input.kind),
177
    learningScope: input.metadata?.learningScope ?? inferredLearningScope(input.payload),
178
    reportedAt: input.metadata?.reportedAt ?? inferredReportedAt(input.payload, ingestedAt),
179
    ingestedAt,
180
  });
181
}