feat(tess): add safe runtime observability
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
This commit is contained in:
@@ -15,3 +15,10 @@ export {
|
||||
type RedactionResult,
|
||||
type SensitiveClassification,
|
||||
} from './redaction.js';
|
||||
export {
|
||||
createRuntimeAuditLogEntry,
|
||||
type RuntimeAuditEvent,
|
||||
type RuntimeAuditErrorCode,
|
||||
type RuntimeAuditOperation,
|
||||
type RuntimeAuditOutcome,
|
||||
} from './runtime-audit.js';
|
||||
|
||||
52
packages/log/src/runtime-audit.test.ts
Normal file
52
packages/log/src/runtime-audit.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
86
packages/log/src/runtime-audit.ts
Normal file
86
packages/log/src/runtime-audit.ts
Normal 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 } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user