feat(tess): add safe runtime observability
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
Jarvis
2026-07-12 19:50:11 -05:00
parent 753a360517
commit adfa5c0697
12 changed files with 391 additions and 24 deletions

View File

@@ -15,6 +15,7 @@ import type {
import { AgentRuntimeProviderRegistry } from '@mosaicstack/agent'; import { AgentRuntimeProviderRegistry } from '@mosaicstack/agent';
import type { ActorTenantScope } from '../../auth/session-scope.js'; import type { ActorTenantScope } from '../../auth/session-scope.js';
import { import {
RuntimeProviderAuditService,
RuntimeProviderService, RuntimeProviderService,
type RuntimeAuditEvent, type RuntimeAuditEvent,
type RuntimeAuditSink, type RuntimeAuditSink,
@@ -159,7 +160,8 @@ describe('RuntimeProviderService security boundary', (): void => {
correlationId: CONTEXT.correlationId, correlationId: CONTEXT.correlationId,
}); });
expect(Object.isFrozen(providerScope)).toBe(true); expect(Object.isFrozen(providerScope)).toBe(true);
expect(audit.events).toContainEqual({ expect(audit.events).toContainEqual(
expect.objectContaining({
providerId: 'fleet', providerId: 'fleet',
operation: 'session.send', operation: 'session.send',
outcome: 'succeeded', outcome: 'succeeded',
@@ -168,11 +170,37 @@ describe('RuntimeProviderService security boundary', (): void => {
channelId: CONTEXT.channelId, channelId: CONTEXT.channelId,
correlationId: CONTEXT.correlationId, correlationId: CONTEXT.correlationId,
resourceId: 'session-1', resourceId: 'session-1',
}); durationMs: expect.any(Number),
}),
);
expect(JSON.stringify(audit.events)).not.toContain('hello'); expect(JSON.stringify(audit.events)).not.toContain('hello');
expect(JSON.stringify(audit.events)).not.toContain('key-1'); expect(JSON.stringify(audit.events)).not.toContain('key-1');
}); });
it('does not block a provider operation when an unsafe resource ID is redacted in durable audit', async (): Promise<void> => {
const provider = new RecordingRuntimeProvider(['session.send']);
let persisted: unknown;
const durableAudit = new RuntimeProviderAuditService({
logs: {
ingest: async (entry: unknown): Promise<unknown> => {
persisted = entry;
return entry;
},
},
} as never);
const service = makeService(provider, durableAudit);
await service.sendMessage(
'fleet',
'session/credential-canary=secret-value',
{ content: 'safe message', idempotencyKey: 'key-1' },
CONTEXT,
);
expect(provider.sentMessages).toHaveLength(1);
expect(JSON.stringify(persisted)).not.toContain('secret-value');
});
it('fails closed before a provider side effect when a capability is missing', async (): Promise<void> => { it('fails closed before a provider side effect when a capability is missing', async (): Promise<void> => {
const provider = new RecordingRuntimeProvider([]); const provider = new RecordingRuntimeProvider([]);
const service = makeService(provider); const service = makeService(provider);
@@ -191,12 +219,14 @@ describe('RuntimeProviderService security boundary', (): void => {
it('requires a consumed exact-action approval before termination', async (): Promise<void> => { it('requires a consumed exact-action approval before termination', async (): Promise<void> => {
const provider = new RecordingRuntimeProvider(['session.terminate']); const provider = new RecordingRuntimeProvider(['session.terminate']);
const approval = new DenyingApprovalVerifier(); const approval = new DenyingApprovalVerifier();
const service = makeService(provider, new RecordingAuditSink(), approval); const audit = new RecordingAuditSink();
const service = makeService(provider, audit, approval);
await expect( await expect(
service.terminate('fleet', 'session-1', 'forged-approval', CONTEXT), service.terminate('fleet', 'session-1', 'forged-approval', CONTEXT),
).rejects.toThrow(/approval denied/); ).rejects.toThrow(/approval denied/);
expect(provider.terminateCalls).toBe(0); expect(provider.terminateCalls).toBe(0);
expect(audit.events.at(-1)).toMatchObject({ outcome: 'denied', errorCode: 'policy_denied' });
}); });
it('binds an accepted termination approval to provider, session, immutable scope, and correlation', async (): Promise<void> => { it('binds an accepted termination approval to provider, session, immutable scope, and correlation', async (): Promise<void> => {
@@ -256,6 +286,37 @@ describe('RuntimeProviderService security boundary', (): void => {
'requested', 'requested',
'failed', 'failed',
]); ]);
expect(audit.events.at(-1)).toMatchObject({
errorCode: 'provider_error',
durationMs: expect.any(Number),
});
});
it('persists only metadata-only runtime audit fields', async (): Promise<void> => {
let persisted: unknown;
const ingest = async (entry: unknown): Promise<unknown> => {
persisted = entry;
return entry;
};
const service = new RuntimeProviderAuditService({ logs: { ingest } } as never);
await service.record({
providerId: 'fleet',
operation: 'session.send',
outcome: 'succeeded',
actorId: 'owner-1',
tenantId: 'tenant-1',
channelId: 'cli',
correlationId: 'correlation-1',
resourceId: 'session-1',
durationMs: 12,
});
expect(persisted).toMatchObject({
content: 'runtime.provider.audit',
metadata: expect.objectContaining({ correlationId: 'correlation-1', durationMs: 12 }),
});
expect(JSON.stringify(persisted)).not.toContain('approval');
}); });
it('does not misreport a completed provider side effect when completion auditing fails', async (): Promise<void> => { it('does not misreport a completed provider side effect when completion auditing fails', async (): Promise<void> => {

View File

@@ -14,6 +14,7 @@ import { CoordModule } from '../coord/coord.module.js';
import { McpClientModule } from '../mcp-client/mcp-client.module.js'; import { McpClientModule } from '../mcp-client/mcp-client.module.js';
import { SkillsModule } from '../skills/skills.module.js'; import { SkillsModule } from '../skills/skills.module.js';
import { GCModule } from '../gc/gc.module.js'; import { GCModule } from '../gc/gc.module.js';
import { LogModule } from '../log/log.module.js';
import { import {
AGENT_RUNTIME_PROVIDER_REGISTRY, AGENT_RUNTIME_PROVIDER_REGISTRY,
DenyRuntimeApprovalVerifier, DenyRuntimeApprovalVerifier,
@@ -25,7 +26,7 @@ import {
@Global() @Global()
@Module({ @Module({
imports: [CoordModule, McpClientModule, SkillsModule, GCModule], imports: [CoordModule, McpClientModule, SkillsModule, GCModule, LogModule],
providers: [ providers: [
ProviderService, ProviderService,
ProviderCredentialsService, ProviderCredentialsService,

View File

@@ -107,8 +107,7 @@ export class ProviderService implements OnModuleInit, OnModuleDestroy {
* Interval is configurable via PROVIDER_HEALTH_INTERVAL env (seconds, default 60). * Interval is configurable via PROVIDER_HEALTH_INTERVAL env (seconds, default 60).
*/ */
private startHealthCheckScheduler(): void { private startHealthCheckScheduler(): void {
const intervalSecs = const intervalSecs = this.effectiveHealthCheckIntervalSecs();
parseInt(process.env['PROVIDER_HEALTH_INTERVAL'] ?? '', 10) || DEFAULT_HEALTH_INTERVAL_SECS;
const intervalMs = intervalSecs * 1000; const intervalMs = intervalSecs * 1000;
// Run an initial check immediately (non-blocking) // Run an initial check immediately (non-blocking)
@@ -176,6 +175,28 @@ export class ProviderService implements OnModuleInit, OnModuleDestroy {
}); });
} }
/**
* Returns the effective provider operational policy without credentials,
* endpoints, request content, or provider error details.
*/
getEffectivePolicyStatus(): {
healthCheckIntervalSecs: number;
configuredProviders: string[];
availableModelCount: number;
} {
return {
healthCheckIntervalSecs: this.effectiveHealthCheckIntervalSecs(),
configuredProviders: this.adapters.map((adapter) => adapter.name),
availableModelCount: this.registry?.getAvailable().length ?? 0,
};
}
private effectiveHealthCheckIntervalSecs(): number {
return (
parseInt(process.env['PROVIDER_HEALTH_INTERVAL'] ?? '', 10) || DEFAULT_HEALTH_INTERVAL_SECS
);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Adapter-pattern API // Adapter-pattern API
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -0,0 +1,46 @@
import { describe, expect, it, vi } from 'vitest';
import { ProvidersController } from './providers.controller.js';
describe('ProvidersController operational status', (): void => {
it('reports provider latency and effective policy without exposing provider error details', (): void => {
const providerService = {
getProvidersHealth: vi.fn(() => [
{
name: 'fleet',
status: 'down',
latencyMs: 42,
lastChecked: '2026-07-12T00:00:00.000Z',
modelCount: 0,
error: 'credential-canary=secret-value',
},
]),
getEffectivePolicyStatus: vi.fn(() => ({
healthCheckIntervalSecs: 60,
configuredProviders: ['fleet'],
availableModelCount: 0,
})),
};
const controller = new ProvidersController(providerService as never, {} as never, {} as never);
const status = controller.status();
expect(status).toEqual({
providers: [
{
name: 'fleet',
status: 'down',
latencyMs: 42,
lastChecked: '2026-07-12T00:00:00.000Z',
modelCount: 0,
errorCode: 'provider_unavailable',
},
],
effectivePolicy: {
healthCheckIntervalSecs: 60,
configuredProviders: ['fleet'],
availableModelCount: 0,
},
});
expect(JSON.stringify(status)).not.toContain('secret-value');
});
});

View File

@@ -33,7 +33,20 @@ export class ProvidersController {
@Get('health') @Get('health')
health() { health() {
return { providers: this.providerService.getProvidersHealth() }; return { providers: this.safeProviderHealth() };
}
/**
* Safe operational status for troubleshooting and readiness checks. Provider
* errors are reduced to a stable code so credentials and remote responses
* cannot leak through this endpoint.
*/
@Get('status')
status() {
return {
providers: this.safeProviderHealth(),
effectivePolicy: this.providerService.getEffectivePolicyStatus(),
};
} }
@Post('test') @Post('test')
@@ -51,6 +64,13 @@ export class ProvidersController {
return this.routingService.rank(criteria); return this.routingService.rank(criteria);
} }
private safeProviderHealth() {
return this.providerService.getProvidersHealth().map(({ error, ...provider }) => ({
...provider,
...(error ? { errorCode: 'provider_unavailable' } : {}),
}));
}
// ── Credential CRUD ────────────────────────────────────────────────────── // ── Credential CRUD ──────────────────────────────────────────────────────
/** /**

View File

@@ -1,5 +1,10 @@
import { ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { AgentRuntimeProviderRegistry } from '@mosaicstack/agent'; import { AgentRuntimeProviderRegistry } from '@mosaicstack/agent';
import {
createRuntimeAuditLogEntry,
type LogService,
type RuntimeAuditErrorCode,
} from '@mosaicstack/log';
import type { import type {
AgentRuntimeProvider, AgentRuntimeProvider,
RuntimeAttachHandle, RuntimeAttachHandle,
@@ -14,6 +19,7 @@ import type {
RuntimeStreamEvent, RuntimeStreamEvent,
} from '@mosaicstack/types'; } from '@mosaicstack/types';
import type { ActorTenantScope } from '../auth/session-scope.js'; import type { ActorTenantScope } from '../auth/session-scope.js';
import { LOG_SERVICE } from '../log/log.tokens.js';
export const AGENT_RUNTIME_PROVIDER_REGISTRY = Symbol('AGENT_RUNTIME_PROVIDER_REGISTRY'); export const AGENT_RUNTIME_PROVIDER_REGISTRY = Symbol('AGENT_RUNTIME_PROVIDER_REGISTRY');
export const RUNTIME_PROVIDER_AUDIT_SINK = Symbol('RUNTIME_PROVIDER_AUDIT_SINK'); export const RUNTIME_PROVIDER_AUDIT_SINK = Symbol('RUNTIME_PROVIDER_AUDIT_SINK');
@@ -42,6 +48,8 @@ export interface RuntimeAuditEvent {
channelId: string; channelId: string;
correlationId: string; correlationId: string;
resourceId?: string; resourceId?: string;
durationMs?: number;
errorCode?: RuntimeAuditErrorCode;
} }
export interface RuntimeAuditSink { export interface RuntimeAuditSink {
@@ -87,8 +95,12 @@ export class DenyRuntimeApprovalVerifier implements RuntimeApprovalVerifier {
export class RuntimeProviderAuditService implements RuntimeAuditSink { export class RuntimeProviderAuditService implements RuntimeAuditSink {
private readonly logger = new Logger(RuntimeProviderAuditService.name); private readonly logger = new Logger(RuntimeProviderAuditService.name);
constructor(@Inject(LOG_SERVICE) private readonly logService: LogService) {}
async record(event: RuntimeAuditEvent): Promise<void> { async record(event: RuntimeAuditEvent): Promise<void> {
this.logger.log(JSON.stringify(event)); const entry = createRuntimeAuditLogEntry(event);
await this.logService.logs.ingest(entry);
this.logger.log(JSON.stringify({ event: entry.content, metadata: entry.metadata }));
} }
} }
@@ -267,6 +279,7 @@ export class RuntimeProviderService {
invoke: (provider: AgentRuntimeProvider, scope: RuntimeScope) => Promise<T>, invoke: (provider: AgentRuntimeProvider, scope: RuntimeScope) => Promise<T>,
): Promise<T> { ): Promise<T> {
const scope = this.deriveScope(context); const scope = this.deriveScope(context);
const startedAt = Date.now();
await this.record(providerId, operation, 'requested', scope, resourceId); await this.record(providerId, operation, 'requested', scope, resourceId);
let invocationStarted = false; let invocationStarted = false;
try { try {
@@ -276,13 +289,22 @@ export class RuntimeProviderService {
} }
invocationStarted = true; invocationStarted = true;
const result = await invoke(provider, scope); const result = await invoke(provider, scope);
await this.recordCompletion(providerId, operation, scope, resourceId); await this.recordCompletion(providerId, operation, scope, resourceId, Date.now() - startedAt);
return result; return result;
} catch (error: unknown) { } catch (error: unknown) {
const durationMs = Date.now() - startedAt;
if (invocationStarted && !(error instanceof RuntimeApprovalDeniedError)) { if (invocationStarted && !(error instanceof RuntimeApprovalDeniedError)) {
await this.recordFailure(providerId, operation, scope, resourceId); await this.recordFailure(providerId, operation, scope, resourceId, durationMs);
} else { } else {
await this.record(providerId, operation, 'denied', scope, resourceId); await this.record(
providerId,
operation,
'denied',
scope,
resourceId,
durationMs,
'policy_denied',
);
} }
throw error; throw error;
} }
@@ -300,6 +322,7 @@ export class RuntimeProviderService {
) => AsyncIterable<RuntimeStreamEvent>, ) => AsyncIterable<RuntimeStreamEvent>,
): AsyncIterable<RuntimeStreamEvent> { ): AsyncIterable<RuntimeStreamEvent> {
const scope = this.deriveScope(context); const scope = this.deriveScope(context);
const startedAt = Date.now();
await this.record(providerId, operation, 'requested', scope, resourceId); await this.record(providerId, operation, 'requested', scope, resourceId);
let invocationStarted = false; let invocationStarted = false;
try { try {
@@ -309,12 +332,21 @@ export class RuntimeProviderService {
for await (const event of invoke(provider, scope)) { for await (const event of invoke(provider, scope)) {
yield event; yield event;
} }
await this.recordCompletion(providerId, operation, scope, resourceId); await this.recordCompletion(providerId, operation, scope, resourceId, Date.now() - startedAt);
} catch (error: unknown) { } catch (error: unknown) {
const durationMs = Date.now() - startedAt;
if (invocationStarted) { if (invocationStarted) {
await this.recordFailure(providerId, operation, scope, resourceId); await this.recordFailure(providerId, operation, scope, resourceId, durationMs);
} else { } else {
await this.record(providerId, operation, 'denied', scope, resourceId); await this.record(
providerId,
operation,
'denied',
scope,
resourceId,
durationMs,
'policy_denied',
);
} }
throw error; throw error;
} }
@@ -358,9 +390,18 @@ export class RuntimeProviderService {
operation: RuntimeProviderOperation, operation: RuntimeProviderOperation,
scope: RuntimeScope, scope: RuntimeScope,
resourceId: string | undefined, resourceId: string | undefined,
durationMs: number,
): Promise<void> { ): Promise<void> {
try { try {
await this.record(providerId, operation, 'failed', scope, resourceId); await this.record(
providerId,
operation,
'failed',
scope,
resourceId,
durationMs,
'provider_error',
);
} catch { } catch {
this.logger.error( this.logger.error(
`Runtime provider failure audit failed provider=${providerId} operation=${operation} correlation=${scope.correlationId}`, `Runtime provider failure audit failed provider=${providerId} operation=${operation} correlation=${scope.correlationId}`,
@@ -373,9 +414,10 @@ export class RuntimeProviderService {
operation: RuntimeProviderOperation, operation: RuntimeProviderOperation,
scope: RuntimeScope, scope: RuntimeScope,
resourceId: string | undefined, resourceId: string | undefined,
durationMs: number,
): Promise<void> { ): Promise<void> {
try { try {
await this.record(providerId, operation, 'succeeded', scope, resourceId); await this.record(providerId, operation, 'succeeded', scope, resourceId, durationMs);
} catch { } catch {
this.logger.error( this.logger.error(
`Runtime provider completion audit failed provider=${providerId} operation=${operation} correlation=${scope.correlationId}`, `Runtime provider completion audit failed provider=${providerId} operation=${operation} correlation=${scope.correlationId}`,
@@ -389,6 +431,8 @@ export class RuntimeProviderService {
outcome: RuntimeProviderAuditOutcome, outcome: RuntimeProviderAuditOutcome,
scope: RuntimeScope, scope: RuntimeScope,
resourceId: string | undefined, resourceId: string | undefined,
durationMs?: number,
errorCode?: RuntimeAuditErrorCode,
): Promise<void> { ): Promise<void> {
await this.audit.record({ await this.audit.record({
providerId, providerId,
@@ -399,6 +443,8 @@ export class RuntimeProviderService {
channelId: scope.channelId, channelId: scope.channelId,
correlationId: scope.correlationId, correlationId: scope.correlationId,
...(resourceId ? { resourceId } : {}), ...(resourceId ? { resourceId } : {}),
...(durationMs !== undefined ? { durationMs } : {}),
...(errorCode ? { errorCode } : {}),
}); });
} }
} }

View File

@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest';
import { HealthController } from './health.controller.js';
describe('HealthController', (): void => {
it('exposes liveness and readiness without configuration details', (): void => {
const controller = new HealthController();
expect(controller.check()).toEqual({ status: 'ok' });
expect(controller.ready()).toEqual({ status: 'ready' });
expect(JSON.stringify(controller.ready())).not.toContain('credential');
});
});

View File

@@ -6,4 +6,10 @@ export class HealthController {
check(): { status: string } { check(): { status: string } {
return { status: 'ok' }; return { status: 'ok' };
} }
/** Readiness intentionally exposes no configuration, provider, or credential details. */
@Get('ready')
ready(): { status: string } {
return { status: 'ready' };
}
} }

View File

@@ -0,0 +1,9 @@
# TESS-M1-OBS-001 Scratchpad
- Branch: `feat/tess-observability-terra` (the requested name is checked out by an abandoned worktree; orchestrator approved this clean branch).
- Base: `origin/main` at `e92186d7`.
- Scope: correlation propagation; metadata-only structured runtime/provider/tool audit; health/readiness; safe effective-policy status.
- Security invariant: audit and status data use an allowlist; no message bodies, credentials, approval references, tool arguments, or tool output.
- TDD: `packages/log/src/runtime-audit.test.ts` and `apps/gateway/src/health/health.controller.test.ts` failed before implementation and now pass.
- Verification: full `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, and `pnpm test` passed after implementation (2026-07-12).
- Review: corrected audit sanitizer findings by hashing every resource ID. Durable audit persistence remains fail-closed by design: the pre-existing M1 provider-boundary suite requires it to prevent an unaudited side effect.

View File

@@ -10,3 +10,10 @@ export {
type LogQuery, type LogQuery,
} from './agent-logs.js'; } from './agent-logs.js';
export { registerLogCommand } from './cli.js'; export { registerLogCommand } from './cli.js';
export {
createRuntimeAuditLogEntry,
type RuntimeAuditEvent,
type RuntimeAuditErrorCode,
type RuntimeAuditOperation,
type RuntimeAuditOutcome,
} from './runtime-audit.js';

View File

@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest';
import { createRuntimeAuditLogEntry } from './runtime-audit.js';
describe('createRuntimeAuditLogEntry', (): void => {
it('serializes only allowlisted runtime audit metadata', (): void => {
const entry = createRuntimeAuditLogEntry({
providerId: 'fleet',
operation: 'session.send',
outcome: 'succeeded',
actorId: 'actor-1',
tenantId: 'tenant-1',
channelId: 'discord',
correlationId: 'correlation-1',
resourceId: 'session-1',
durationMs: 12,
});
expect(entry).toMatchObject({
sessionId: 'runtime:fleet',
userId: 'actor-1',
level: 'info',
category: 'tool_use',
content: 'runtime.provider.audit',
metadata: {
providerId: 'fleet',
operation: 'session.send',
outcome: 'succeeded',
correlationId: 'correlation-1',
resourceId: expect.stringMatching(/^sha256:/),
durationMs: 12,
},
});
expect(JSON.stringify(entry)).not.toContain('approvalRef');
});
it('hashes every resource ID without blocking a runtime audit or persisting its raw value', (): void => {
const entry = createRuntimeAuditLogEntry({
providerId: 'fleet',
operation: 'session.send',
outcome: 'succeeded',
actorId: 'actor-1',
tenantId: 'tenant-1',
channelId: 'discord',
correlationId: 'correlation-1',
resourceId: 'credential-canary:secret-value',
durationMs: 12,
});
expect(entry.metadata).toMatchObject({ resourceId: expect.stringMatching(/^sha256:/) });
expect(JSON.stringify(entry)).not.toContain('secret-value');
});
});

View File

@@ -0,0 +1,86 @@
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';
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 } : {}),
},
};
}