import { describe, expect, it } from 'vitest'; import type { AgentRuntimeProvider, RuntimeAttachHandle, RuntimeAttachMode, RuntimeCapability, RuntimeCapabilitySet, RuntimeHealth, RuntimeMessage, RuntimeScope, RuntimeSession, RuntimeSessionTree, RuntimeStreamEvent, } from '@mosaicstack/types'; import { AgentRuntimeProviderRegistry } from '@mosaicstack/agent'; import type { ActorTenantScope } from '../../auth/session-scope.js'; import { RuntimeProviderAuditService, RuntimeProviderService, type RuntimeAuditEvent, type RuntimeAuditSink, type RuntimeApprovalVerifier, } from '../runtime-provider-registry.service.js'; process.env['MOSAIC_AGENT_NAME'] ??= 'test-runtime-agent'; const OWNER_SCOPE: ActorTenantScope = { userId: 'owner-1', tenantId: 'tenant-1' }; const CONTEXT = { actorScope: OWNER_SCOPE, channelId: 'cli', correlationId: 'correlation-1', }; class RecordingRuntimeProvider implements AgentRuntimeProvider { readonly id = 'fleet'; readonly receivedScopes: RuntimeScope[] = []; readonly sentMessages: RuntimeMessage[] = []; terminateCalls = 0; throwAfterSend = false; throwAuthorization = false; constructor(private readonly supported: RuntimeCapability[]) {} async capabilities(scope: RuntimeScope): Promise { this.receivedScopes.push(scope); return { supported: this.supported }; } async health(scope: RuntimeScope): Promise { this.receivedScopes.push(scope); return { status: 'healthy', checkedAt: '2026-07-12T00:00:00.000Z' }; } async listSessions(scope: RuntimeScope): Promise { this.receivedScopes.push(scope); return []; } async getSessionTree(scope: RuntimeScope): Promise { this.receivedScopes.push(scope); return []; } async *streamSession( _sessionId: string, _cursor: string | undefined, scope: RuntimeScope, ): AsyncIterable { this.receivedScopes.push(scope); return; } async sendMessage( _sessionId: string, message: RuntimeMessage, scope: RuntimeScope, ): Promise { this.receivedScopes.push(scope); this.sentMessages.push(message); if (this.throwAuthorization) { throw Object.assign(new Error('provider authorization denied'), { code: 'forbidden' }); } if (this.throwAfterSend) { throw new Error('provider acknowledgement failed'); } } async attach( sessionId: string, mode: RuntimeAttachMode, scope: RuntimeScope, ): Promise { this.receivedScopes.push(scope); return { attachmentId: 'attachment-1', sessionId, mode, expiresAt: '2026-07-12T00:00:00.000Z', }; } async detach(_attachmentId: string, scope: RuntimeScope): Promise { this.receivedScopes.push(scope); } async terminate(_sessionId: string, _approvalRef: string, scope: RuntimeScope): Promise { this.receivedScopes.push(scope); this.terminateCalls += 1; } } class RecordingAuditSink implements RuntimeAuditSink { readonly events: RuntimeAuditEvent[] = []; async record(event: RuntimeAuditEvent): Promise { this.events.push(event); } } class DenyingApprovalVerifier implements RuntimeApprovalVerifier { async consume(): Promise { return false; } } class AcceptingApprovalVerifier implements RuntimeApprovalVerifier { consumedAction: Parameters[1] | undefined; async consume( _approvalRef: string, action: Parameters[1], ): Promise { this.consumedAction = action; return true; } } function makeService( provider: RecordingRuntimeProvider, audit: RuntimeAuditSink = new RecordingAuditSink(), approval: RuntimeApprovalVerifier = new DenyingApprovalVerifier(), ): RuntimeProviderService { const registry = new AgentRuntimeProviderRegistry(); registry.register(provider); return new RuntimeProviderService(registry, audit, approval); } describe('RuntimeProviderService security boundary', (): void => { it('derives and freezes only the authenticated actor scope while preserving correlation metadata', async (): Promise => { const provider = new RecordingRuntimeProvider(['session.send']); const audit = new RecordingAuditSink(); const service = makeService(provider, audit); await service.sendMessage( 'fleet', 'session-1', { content: 'hello', idempotencyKey: 'key-1' }, CONTEXT, ); const providerScope = provider.receivedScopes[0]; expect(providerScope).toEqual({ actorId: OWNER_SCOPE.userId, tenantId: OWNER_SCOPE.tenantId, channelId: CONTEXT.channelId, correlationId: CONTEXT.correlationId, }); expect(Object.isFrozen(providerScope)).toBe(true); expect(audit.events).toContainEqual( expect.objectContaining({ providerId: 'fleet', operation: 'session.send', outcome: 'succeeded', actorId: OWNER_SCOPE.userId, tenantId: OWNER_SCOPE.tenantId, channelId: CONTEXT.channelId, correlationId: CONTEXT.correlationId, resourceId: 'session-1', durationMs: expect.any(Number), }), ); expect(JSON.stringify(audit.events)).not.toContain('hello'); 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 => { const provider = new RecordingRuntimeProvider(['session.send']); let persisted: unknown; const durableAudit = new RuntimeProviderAuditService({ logs: { ingest: async (entry: unknown): Promise => { 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 => { const provider = new RecordingRuntimeProvider([]); const service = makeService(provider); await expect( service.sendMessage( 'fleet', 'session-1', { content: 'hello', idempotencyKey: 'key-1' }, CONTEXT, ), ).rejects.toThrow(/capability denied/); expect(provider.sentMessages).toEqual([]); }); it('requires a consumed exact-action approval before termination', async (): Promise => { const provider = new RecordingRuntimeProvider(['session.terminate']); const approval = new DenyingApprovalVerifier(); const audit = new RecordingAuditSink(); const service = makeService(provider, audit, approval); await expect( service.terminate('fleet', 'session-1', 'forged-approval', CONTEXT), ).rejects.toThrow(/approval denied/); 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 => { const provider = new RecordingRuntimeProvider(['session.terminate']); const approval = new AcceptingApprovalVerifier(); const service = makeService(provider, new RecordingAuditSink(), approval); await service.terminate('fleet', 'session-1', 'approval-1', CONTEXT); expect(approval.consumedAction).toEqual({ providerId: 'fleet', sessionId: 'session-1', actorId: OWNER_SCOPE.userId, tenantId: OWNER_SCOPE.tenantId, channelId: CONTEXT.channelId, correlationId: CONTEXT.correlationId, agentName: process.env['MOSAIC_AGENT_NAME'], }); expect(provider.terminateCalls).toBe(1); }); it('fails closed before invoking a provider when audit persistence rejects the request', async (): Promise => { const provider = new RecordingRuntimeProvider(['session.send']); const unavailableAudit: RuntimeAuditSink = { async record(): Promise { throw new Error('audit unavailable'); }, }; const service = makeService(provider, unavailableAudit); await expect( service.sendMessage( 'fleet', 'session-1', { content: 'hello', idempotencyKey: 'key-1' }, CONTEXT, ), ).rejects.toThrow(/audit unavailable/); expect(provider.sentMessages).toEqual([]); }); it('records a provider error after invocation as failed rather than denied', async (): Promise => { const provider = new RecordingRuntimeProvider(['session.send']); provider.throwAfterSend = true; const audit = new RecordingAuditSink(); const service = makeService(provider, audit); await expect( service.sendMessage( 'fleet', 'session-1', { content: 'hello', idempotencyKey: 'key-1' }, CONTEXT, ), ).rejects.toThrow(/provider acknowledgement failed/); expect(provider.sentMessages).toHaveLength(1); expect(audit.events.map((event: RuntimeAuditEvent): string => event.outcome)).toEqual([ 'requested', 'failed', ]); expect(audit.events.at(-1)).toMatchObject({ errorCode: 'provider_error', durationMs: expect.any(Number), }); }); it('records a provider authorization rejection as denied rather than provider failure', async (): Promise => { const provider = new RecordingRuntimeProvider(['session.send']); provider.throwAuthorization = true; const audit = new RecordingAuditSink(); const service = makeService(provider, audit); await expect( service.sendMessage( 'fleet', 'session-1', { content: 'hello', idempotencyKey: 'key-1' }, CONTEXT, ), ).rejects.toThrow(/provider authorization denied/); expect(audit.events.at(-1)).toMatchObject({ outcome: 'denied', errorCode: 'policy_denied' }); }); it('persists only metadata-only runtime audit fields', async (): Promise => { let persisted: unknown; const ingest = async (entry: unknown): Promise => { 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 => { const provider = new RecordingRuntimeProvider(['session.send']); let auditCalls = 0; const audit: RuntimeAuditSink = { async record(): Promise { auditCalls += 1; if (auditCalls === 2) { throw new Error('completion audit unavailable'); } }, }; const service = makeService(provider, audit); await expect( service.sendMessage( 'fleet', 'session-1', { content: 'hello', idempotencyKey: 'key-1' }, CONTEXT, ), ).resolves.toBeUndefined(); expect(provider.sentMessages).toHaveLength(1); expect(auditCalls).toBe(2); }); });