88 lines
2.9 KiB
TypeScript
88 lines
2.9 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
import type { NewAgentLog } from './agent-logs.js';
|
|
|
|
export type RuntimeAuditOperation =
|
|
| 'session.list'
|
|
| 'session.tree'
|
|
| 'session.stream'
|
|
| 'session.send'
|
|
| 'session.attach'
|
|
| 'session.terminate'
|
|
| 'runtime.capabilities'
|
|
| 'runtime.health'
|
|
| 'runtime.transitional-capabilities';
|
|
|
|
export type RuntimeAuditOutcome = 'requested' | 'succeeded' | 'denied' | 'failed';
|
|
export type RuntimeAuditErrorCode = 'policy_denied' | 'provider_error';
|
|
|
|
/**
|
|
* Deliberately metadata-only runtime audit record. It has no fields for message
|
|
* content, credentials, approval references, tool arguments, or tool output.
|
|
*/
|
|
export interface RuntimeAuditEvent {
|
|
providerId: string;
|
|
operation: RuntimeAuditOperation;
|
|
outcome: RuntimeAuditOutcome;
|
|
actorId: string;
|
|
tenantId: string;
|
|
channelId: string;
|
|
correlationId: string;
|
|
resourceId?: string;
|
|
durationMs?: number;
|
|
errorCode?: RuntimeAuditErrorCode;
|
|
}
|
|
|
|
const SAFE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
|
|
function safeIdentifier(value: string): string {
|
|
if (SAFE_IDENTIFIER.test(value)) return value;
|
|
return hashIdentifier(value);
|
|
}
|
|
|
|
function hashIdentifier(value: string): string {
|
|
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
}
|
|
|
|
/**
|
|
* Converts a typed audit event into a durable log entry using an explicit
|
|
* allowlist. Values that could carry credentials or untrusted content are
|
|
* rejected before persistence or structured log emission.
|
|
*/
|
|
export function createRuntimeAuditLogEntry(event: RuntimeAuditEvent): NewAgentLog {
|
|
const providerId = safeIdentifier(event.providerId);
|
|
const actorId = safeIdentifier(event.actorId);
|
|
const tenantId = safeIdentifier(event.tenantId);
|
|
const channelId = safeIdentifier(event.channelId);
|
|
const correlationId = safeIdentifier(event.correlationId);
|
|
// Provider resource identifiers may be opaque or user-derived, so never persist them raw.
|
|
const resourceId = event.resourceId ? hashIdentifier(event.resourceId) : undefined;
|
|
const persistedUserId = SAFE_IDENTIFIER.test(event.actorId) ? event.actorId : null;
|
|
|
|
if (
|
|
event.durationMs !== undefined &&
|
|
(!Number.isInteger(event.durationMs) || event.durationMs < 0)
|
|
) {
|
|
throw new Error('Runtime audit duration must be a non-negative integer');
|
|
}
|
|
|
|
return {
|
|
sessionId: `runtime:${providerId}`,
|
|
userId: persistedUserId,
|
|
level: event.outcome === 'failed' ? 'error' : event.outcome === 'denied' ? 'warn' : 'info',
|
|
category: event.operation.startsWith('session.') ? 'tool_use' : 'general',
|
|
content: 'runtime.provider.audit',
|
|
metadata: {
|
|
providerId,
|
|
operation: event.operation,
|
|
outcome: event.outcome,
|
|
actorId,
|
|
tenantId,
|
|
channelId,
|
|
correlationId,
|
|
...(resourceId ? { resourceId } : {}),
|
|
...(event.durationMs !== undefined ? { durationMs: event.durationMs } : {}),
|
|
...(event.errorCode ? { errorCode: event.errorCode } : {}),
|
|
},
|
|
};
|
|
}
|