Files
stack/apps/gateway/src/agent/__tests__/runtime-provider-registry.service.test.ts
jason.woltje 0b621660c8
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was canceled
feat(tess): wire durable interaction surfaces (#732)
2026-07-13 10:05:29 +00:00

371 lines
12 KiB
TypeScript

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<RuntimeCapabilitySet> {
this.receivedScopes.push(scope);
return { supported: this.supported };
}
async health(scope: RuntimeScope): Promise<RuntimeHealth> {
this.receivedScopes.push(scope);
return { status: 'healthy', checkedAt: '2026-07-12T00:00:00.000Z' };
}
async listSessions(scope: RuntimeScope): Promise<RuntimeSession[]> {
this.receivedScopes.push(scope);
return [];
}
async getSessionTree(scope: RuntimeScope): Promise<RuntimeSessionTree[]> {
this.receivedScopes.push(scope);
return [];
}
async *streamSession(
_sessionId: string,
_cursor: string | undefined,
scope: RuntimeScope,
): AsyncIterable<RuntimeStreamEvent> {
this.receivedScopes.push(scope);
return;
}
async sendMessage(
_sessionId: string,
message: RuntimeMessage,
scope: RuntimeScope,
): Promise<void> {
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<RuntimeAttachHandle> {
this.receivedScopes.push(scope);
return {
attachmentId: 'attachment-1',
sessionId,
mode,
expiresAt: '2026-07-12T00:00:00.000Z',
};
}
async detach(_attachmentId: string, scope: RuntimeScope): Promise<void> {
this.receivedScopes.push(scope);
}
async terminate(_sessionId: string, _approvalRef: string, scope: RuntimeScope): Promise<void> {
this.receivedScopes.push(scope);
this.terminateCalls += 1;
}
}
class RecordingAuditSink implements RuntimeAuditSink {
readonly events: RuntimeAuditEvent[] = [];
async record(event: RuntimeAuditEvent): Promise<void> {
this.events.push(event);
}
}
class DenyingApprovalVerifier implements RuntimeApprovalVerifier {
async consume(): Promise<boolean> {
return false;
}
}
class AcceptingApprovalVerifier implements RuntimeApprovalVerifier {
consumedAction: Parameters<RuntimeApprovalVerifier['consume']>[1] | undefined;
async consume(
_approvalRef: string,
action: Parameters<RuntimeApprovalVerifier['consume']>[1],
): Promise<boolean> {
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<void> => {
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<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> => {
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<void> => {
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<void> => {
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<void> => {
const provider = new RecordingRuntimeProvider(['session.send']);
const unavailableAudit: RuntimeAuditSink = {
async record(): Promise<void> {
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<void> => {
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<void> => {
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<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> => {
const provider = new RecordingRuntimeProvider(['session.send']);
let auditCalls = 0;
const audit: RuntimeAuditSink = {
async record(): Promise<void> {
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);
});
});