262 lines
8.8 KiB
TypeScript
262 lines
8.8 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import type {
|
|
ConnectorExecutionContext,
|
|
ConnectorExecutionGrant,
|
|
ConnectorLease,
|
|
ConnectorLeaseAuditEvent,
|
|
ConnectorLeaseStore,
|
|
FencedConnectorAdapter,
|
|
LogicalAgentBinding,
|
|
} from '@mosaicstack/types';
|
|
import {
|
|
ConnectorLeaseCoordinator,
|
|
MAX_CONNECTOR_GRANT_TTL_MS,
|
|
MAX_CONNECTOR_LEASE_TTL_MS,
|
|
} from './connector-lease.js';
|
|
import type { ConnectorLeaseError } from './connector-lease.js';
|
|
|
|
const identity = { tenantId: 'tenant-a', logicalAgentId: 'mos' } as const;
|
|
const binding: LogicalAgentBinding = { identity, bindingId: 'operator-chat' };
|
|
const activeLease: ConnectorLease = {
|
|
...binding,
|
|
leaseId: '00000000-0000-4000-8000-000000000001',
|
|
connectorId: 'connector-a',
|
|
scopes: ['runtime.send', 'tool.execute'],
|
|
leaseEpoch: '3',
|
|
acquiredAt: '2026-07-14T17:00:00.000Z',
|
|
heartbeatAt: '2026-07-14T17:00:00.000Z',
|
|
expiresAt: '2026-07-14T17:10:00.000Z',
|
|
};
|
|
|
|
class FakeLeaseStore implements ConnectorLeaseStore {
|
|
lease: ConnectorLease | null = activeLease;
|
|
readonly audits: ConnectorLeaseAuditEvent[] = [];
|
|
|
|
async acquire(): Promise<ConnectorLease> {
|
|
if (!this.lease) throw new Error('fixture has no lease');
|
|
return this.lease;
|
|
}
|
|
|
|
async takeover(): Promise<ConnectorLease> {
|
|
if (!this.lease) throw new Error('fixture has no lease');
|
|
return this.lease;
|
|
}
|
|
|
|
async heartbeat(): Promise<ConnectorLease> {
|
|
if (!this.lease) throw new Error('fixture has no lease');
|
|
return this.lease;
|
|
}
|
|
|
|
async release(): Promise<void> {}
|
|
|
|
async findCurrent(): Promise<ConnectorLease | null> {
|
|
return this.lease;
|
|
}
|
|
|
|
async recordAudit(event: ConnectorLeaseAuditEvent): Promise<void> {
|
|
this.audits.push(event);
|
|
}
|
|
}
|
|
|
|
describe('ConnectorLeaseCoordinator fencing', (): void => {
|
|
it('validates a server-minted grant immediately before invoking an adapter side effect', async (): Promise<void> => {
|
|
const store = new FakeLeaseStore();
|
|
const coordinator = new ConnectorLeaseCoordinator(store, {
|
|
now: (): Date => new Date('2026-07-14T17:01:00.000Z'),
|
|
});
|
|
const grant = await coordinator.issueGrant({
|
|
lease: activeLease,
|
|
scopes: ['runtime.send'],
|
|
ttlMs: 30_000,
|
|
correlationId: 'correlation-1',
|
|
});
|
|
const execute = vi.fn(async (_input: string, context: ConnectorExecutionContext) => context);
|
|
const adapter: FencedConnectorAdapter<string, ConnectorExecutionContext> = { execute };
|
|
|
|
const context = await coordinator.executeGrant(grant, 'runtime.send', 'hello', adapter);
|
|
|
|
expect(execute).toHaveBeenCalledOnce();
|
|
expect(context).toMatchObject({
|
|
identity,
|
|
bindingId: 'operator-chat',
|
|
connectorId: 'connector-a',
|
|
leaseEpoch: '3',
|
|
scopes: ['runtime.send'],
|
|
});
|
|
});
|
|
|
|
it('caps grant expiry to the durable current lease instead of submitted metadata', async (): Promise<void> => {
|
|
const store = new FakeLeaseStore();
|
|
store.lease = { ...activeLease, expiresAt: '2026-07-14T17:01:05.000Z' };
|
|
const coordinator = new ConnectorLeaseCoordinator(store, {
|
|
now: (): Date => new Date('2026-07-14T17:01:00.000Z'),
|
|
});
|
|
|
|
const grant = await coordinator.issueGrant({
|
|
lease: { ...activeLease, expiresAt: '2026-07-14T18:00:00.000Z' },
|
|
scopes: ['runtime.send'],
|
|
ttlMs: 30_000,
|
|
correlationId: 'correlation-durable-expiry',
|
|
});
|
|
|
|
expect(grant.expiresAt).toBe('2026-07-14T17:01:05.000Z');
|
|
});
|
|
|
|
it.each([
|
|
['forged clone', (grant: ConnectorExecutionGrant): ConnectorExecutionGrant => ({ ...grant })],
|
|
[
|
|
'cross-tenant clone',
|
|
(grant: ConnectorExecutionGrant): ConnectorExecutionGrant => ({
|
|
...grant,
|
|
identity: { ...grant.identity, tenantId: 'tenant-b' },
|
|
}),
|
|
],
|
|
[
|
|
'cross-agent clone',
|
|
(grant: ConnectorExecutionGrant): ConnectorExecutionGrant => ({
|
|
...grant,
|
|
identity: { ...grant.identity, logicalAgentId: 'other-agent' },
|
|
}),
|
|
],
|
|
[
|
|
'cross-binding clone',
|
|
(grant: ConnectorExecutionGrant): ConnectorExecutionGrant => ({
|
|
...grant,
|
|
bindingId: 'other-binding',
|
|
}),
|
|
],
|
|
[
|
|
'cross-connector clone',
|
|
(grant: ConnectorExecutionGrant): ConnectorExecutionGrant => ({
|
|
...grant,
|
|
connectorId: 'connector-b',
|
|
}),
|
|
],
|
|
])('denies and audits a %s before adapter invocation', async (_label, forge): Promise<void> => {
|
|
const store = new FakeLeaseStore();
|
|
const coordinator = new ConnectorLeaseCoordinator(store, {
|
|
now: (): Date => new Date('2026-07-14T17:01:00.000Z'),
|
|
});
|
|
const grant = await coordinator.issueGrant({
|
|
lease: activeLease,
|
|
scopes: ['runtime.send'],
|
|
ttlMs: 30_000,
|
|
correlationId: 'correlation-forged',
|
|
});
|
|
const adapter = { execute: vi.fn().mockResolvedValue(undefined) };
|
|
|
|
await expect(
|
|
coordinator.executeGrant(forge(grant), 'runtime.send', undefined, adapter),
|
|
).rejects.toMatchObject({ code: 'forged_grant' } satisfies Partial<ConnectorLeaseError>);
|
|
expect(adapter.execute).not.toHaveBeenCalled();
|
|
expect(store.audits.at(-1)).toMatchObject({
|
|
event: 'reject',
|
|
outcome: 'denied',
|
|
reason: 'forged_grant',
|
|
correlationId: 'correlation-forged',
|
|
});
|
|
});
|
|
|
|
it('denies malformed forged grants with sanitized audit metadata', async (): Promise<void> => {
|
|
const store = new FakeLeaseStore();
|
|
const coordinator = new ConnectorLeaseCoordinator(store, {
|
|
now: (): Date => new Date('2026-07-14T17:01:00.000Z'),
|
|
});
|
|
const adapter = { execute: vi.fn().mockResolvedValue(undefined) };
|
|
|
|
await expect(
|
|
coordinator.executeGrant(
|
|
// @ts-expect-error Deliberately exercise malformed runtime input at the trust boundary.
|
|
{},
|
|
'runtime.send',
|
|
undefined,
|
|
adapter,
|
|
),
|
|
).rejects.toMatchObject({ code: 'forged_grant' } satisfies Partial<ConnectorLeaseError>);
|
|
expect(adapter.execute).not.toHaveBeenCalled();
|
|
expect(store.audits).toContainEqual(
|
|
expect.objectContaining({
|
|
identity: { tenantId: 'untrusted', logicalAgentId: 'untrusted' },
|
|
bindingId: 'untrusted',
|
|
connectorId: 'untrusted',
|
|
correlationId: 'untrusted',
|
|
reason: 'forged_grant',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('rejects lease and grant TTLs above server-side safety caps', async (): Promise<void> => {
|
|
const store = new FakeLeaseStore();
|
|
const coordinator = new ConnectorLeaseCoordinator(store, {
|
|
now: (): Date => new Date('2026-07-14T17:01:00.000Z'),
|
|
});
|
|
expect(
|
|
() =>
|
|
new ConnectorLeaseCoordinator(store, {
|
|
maxLeaseTtlMs: MAX_CONNECTOR_LEASE_TTL_MS + 1,
|
|
}),
|
|
).toThrow(/no greater than/);
|
|
|
|
await expect(
|
|
coordinator.acquire({
|
|
identity,
|
|
bindingId: 'operator-chat',
|
|
connectorId: 'connector-a',
|
|
scopes: ['runtime.send'],
|
|
ttlMs: MAX_CONNECTOR_LEASE_TTL_MS + 1,
|
|
correlationId: 'correlation-ttl',
|
|
}),
|
|
).rejects.toThrow(/no greater than/);
|
|
await expect(
|
|
coordinator.issueGrant({
|
|
lease: activeLease,
|
|
scopes: ['runtime.send'],
|
|
ttlMs: MAX_CONNECTOR_GRANT_TTL_MS + 1,
|
|
correlationId: 'correlation-ttl',
|
|
}),
|
|
).rejects.toThrow(/no greater than/);
|
|
});
|
|
|
|
it('denies stale epoch, expired grant, and unauthorized scope before side effects', async (): Promise<void> => {
|
|
let now = new Date('2026-07-14T17:01:00.000Z');
|
|
const store = new FakeLeaseStore();
|
|
const coordinator = new ConnectorLeaseCoordinator(store, { now: (): Date => now });
|
|
const stale = await coordinator.issueGrant({
|
|
lease: activeLease,
|
|
scopes: ['runtime.send'],
|
|
ttlMs: 30_000,
|
|
correlationId: 'correlation-stale',
|
|
});
|
|
store.lease = { ...activeLease, leaseEpoch: '4', connectorId: 'connector-b' };
|
|
const adapter = { execute: vi.fn().mockResolvedValue(undefined) };
|
|
|
|
await expect(
|
|
coordinator.executeGrant(stale, 'runtime.send', undefined, adapter),
|
|
).rejects.toMatchObject({ code: 'stale_epoch' } satisfies Partial<ConnectorLeaseError>);
|
|
|
|
store.lease = activeLease;
|
|
const expiring = await coordinator.issueGrant({
|
|
lease: activeLease,
|
|
scopes: ['runtime.send'],
|
|
ttlMs: 1_000,
|
|
correlationId: 'correlation-expired',
|
|
});
|
|
now = new Date('2026-07-14T17:01:02.000Z');
|
|
await expect(
|
|
coordinator.executeGrant(expiring, 'runtime.send', undefined, adapter),
|
|
).rejects.toMatchObject({ code: 'grant_expired' } satisfies Partial<ConnectorLeaseError>);
|
|
|
|
now = new Date('2026-07-14T17:01:00.000Z');
|
|
const scoped = await coordinator.issueGrant({
|
|
lease: activeLease,
|
|
scopes: ['runtime.send'],
|
|
ttlMs: 30_000,
|
|
correlationId: 'correlation-scope',
|
|
});
|
|
await expect(
|
|
coordinator.executeGrant(scoped, 'tool.execute', undefined, adapter),
|
|
).rejects.toMatchObject({ code: 'scope_denied' } satisfies Partial<ConnectorLeaseError>);
|
|
expect(adapter.execute).not.toHaveBeenCalled();
|
|
});
|
|
});
|