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

Viewing files at commit 4818f40a6317Commit detailsView latest

packages/persistence/src/source-record.ts 175 lines · 4.7 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
  'profile_delta',
46
  'receipt',
47
  'revocation',
48
]);
49
50
export function shouldJournalSourceRecord(kind: string): boolean {
51
  return SOURCE_JOURNAL_RECORD_KINDS.has(kind);
52
}
53
54
const ACCOUNT_RECORD_KINDS = new Set([
55
  'agent_profile',
56
  'installation',
57
  'presence',
58
  'project',
59
  'publication',
60
  'setup_request',
61
]);
62
63
const OPERATIONAL_RECORD_KINDS = new Set(['counterparty_brief', 'feedback_request', 'profile']);
64
65
const SHARED_RECORD_KINDS = new Set([
66
  'completion_report',
67
  'contract',
68
  'interaction_conclusion',
69
  'learning_eligibility',
70
  'receipt',
71
]);
72
73
const ENTITY_REFERENCE_FIELDS = [
74
  'agentId',
75
  'contractHash',
76
  'counterpartyAgentId',
77
  'decisionId',
78
  'deploymentId',
79
  'interactionId',
80
  'projectId',
81
  'reportId',
82
  'reportingAgentId',
83
  'requestId',
84
  'reviewerAgentId',
85
  'subjectAgentId',
86
] as const;
87
88
const REPORTED_AT_FIELDS = [
89
  'occurredAt',
90
  'completedAt',
91
  'decidedAt',
92
  'appliedAt',
93
  'generatedAt',
94
  'createdAt',
95
  'requestedAt',
96
  'updatedAt',
97
] as const;
98
99
function payloadObject(payload: unknown): Record<string, unknown> | undefined {
100
  return payload !== null && typeof payload === 'object' && !Array.isArray(payload)
101
    ? (payload as Record<string, unknown>)
102
    : undefined;
103
}
104
105
function defaultRetentionClass(kind: string): RetentionClass {
106
  if (ACCOUNT_RECORD_KINDS.has(kind)) return 'account';
107
  if (OPERATIONAL_RECORD_KINDS.has(kind)) return 'operational';
108
  return 'audit';
109
}
110
111
function inferredEntityRefs(payload: unknown): Record<string, string> {
112
  const object = payloadObject(payload);
113
  if (!object) return {};
114
  return Object.fromEntries(
115
    ENTITY_REFERENCE_FIELDS.flatMap((field) => {
116
      const value = object[field];
117
      return typeof value === 'string' && value ? [[field, value]] : [];
118
    }),
119
  );
120
}
121
122
function inferredProvenance(payload: unknown): SourceRecordEnvelope['provenance'] {
123
  const value = payloadObject(payload)?.provenance;
124
  const parsed = ProvenanceSchema.safeParse(value);
125
  return parsed.success ? parsed.data : 'observed';
126
}
127
128
function inferredLearningScope(payload: unknown): LearningScope {
129
  const object = payloadObject(payload);
130
  const candidate = object?.contributionMode ?? object?.learningScope;
131
  const parsed = LearningScopeSchema.safeParse(candidate);
132
  return parsed.success ? parsed.data : 'not_evaluated';
133
}
134
135
function inferredReportedAt(payload: unknown, fallback: string): string {
136
  const object = payloadObject(payload);
137
  if (!object) return fallback;
138
  for (const field of REPORTED_AT_FIELDS) {
139
    const value = object[field];
140
    if (typeof value === 'string' && Number.isFinite(Date.parse(value))) return value;
141
  }
142
  return fallback;
143
}
144
145
export function buildSourceRecordEnvelope(input: {
146
  operatorId: string;
147
  kind: string;
148
  recordId: string;
149
  payload: unknown;
150
  metadata?: SourceRecordMetadata;
151
}): SourceRecordEnvelope {
152
  const ingestedAt = input.metadata?.ingestedAt ?? new Date().toISOString();
153
  return SourceRecordEnvelopeSchema.parse({
154
    eventId: input.metadata?.eventId ?? randomUUID(),
155
    operatorId: input.operatorId,
156
    kind: input.kind,
157
    recordId: input.recordId,
158
    schemaName: input.metadata?.schemaName ?? `openclasp.${input.kind}`,
159
    schemaVersion: input.metadata?.schemaVersion ?? '1',
160
    payload: input.payload,
161
    payloadDigest: canonicalHash(input.payload),
162
    entityRefs: {
163
      ...inferredEntityRefs(input.payload),
164
      ...input.metadata?.entityRefs,
165
    },
166
    provenance: input.metadata?.provenance ?? inferredProvenance(input.payload),
167
    visibility:
168
      input.metadata?.visibility ??
169
      (SHARED_RECORD_KINDS.has(input.kind) ? 'shared_participants' : 'local_only'),
170
    retentionClass: input.metadata?.retentionClass ?? defaultRetentionClass(input.kind),
171
    learningScope: input.metadata?.learningScope ?? inferredLearningScope(input.payload),
172
    reportedAt: input.metadata?.reportedAt ?? inferredReportedAt(input.payload, ingestedAt),
173
    ingestedAt,
174
  });
175
}