refactor(#747): de-hardcode interaction coordination names

This commit is contained in:
Hermes Agent
2026-07-13 12:17:21 -05:00
committed by Jarvis
parent 8dd4e9d541
commit 83a96b0081
31 changed files with 474 additions and 303 deletions

View File

@@ -7,7 +7,7 @@ import {
} from '@mosaicstack/discord-plugin'; } from '@mosaicstack/discord-plugin';
import { InteractionController } from '../../agent/interaction.controller.js'; import { InteractionController } from '../../agent/interaction.controller.js';
import { RuntimeProviderService } from '../../agent/runtime-provider-registry.service.js'; import { RuntimeProviderService } from '../../agent/runtime-provider-registry.service.js';
import { TessDurableSessionService } from '../../agent/tess-durable-session.service.js'; import { DurableSessionService } from '../../agent/durable-session.service.js';
import { ChatGateway } from '../../chat/chat.gateway.js'; import { ChatGateway } from '../../chat/chat.gateway.js';
import { CommandAuthorizationService } from '../../commands/command-authorization.service.js'; import { CommandAuthorizationService } from '../../commands/command-authorization.service.js';
@@ -51,7 +51,7 @@ function authorization(): CommandAuthorizationService {
); );
} }
describe('Tess Discord/CLI durable-session integration', () => { describe('interaction Discord/CLI durable-session integration', () => {
afterEach(() => { afterEach(() => {
for (const key of envKeys) { for (const key of envKeys) {
const value = priorEnv.get(key); const value = priorEnv.get(key);
@@ -80,7 +80,7 @@ describe('Tess Discord/CLI durable-session integration', () => {
}, },
]); ]);
const durable = new TessDurableSessionService( const durable = new DurableSessionService(
new InMemoryDurableSessionStore() as never, new InMemoryDurableSessionStore() as never,
{} as never, {} as never,
); );

View File

@@ -11,8 +11,8 @@ import { SessionsController } from './sessions.controller.js';
import { AgentConfigsController } from './agent-configs.controller.js'; import { AgentConfigsController } from './agent-configs.controller.js';
import { InteractionController } from './interaction.controller.js'; import { InteractionController } from './interaction.controller.js';
import { RoutingController } from './routing/routing.controller.js'; import { RoutingController } from './routing/routing.controller.js';
import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; import { DurableSessionRepository } from './durable-session.repository.js';
import { TessDurableSessionService } from './tess-durable-session.service.js'; import { DurableSessionService } from './durable-session.service.js';
import { CoordModule } from '../coord/coord.module.js'; 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';
@@ -44,8 +44,8 @@ export function createGatewayRuntimeProviderRegistry(): AgentRuntimeProviderRegi
RoutingService, RoutingService,
RoutingEngineService, RoutingEngineService,
SkillLoaderService, SkillLoaderService,
TessDurableSessionRepository, DurableSessionRepository,
TessDurableSessionService, DurableSessionService,
{ {
provide: AGENT_RUNTIME_PROVIDER_REGISTRY, provide: AGENT_RUNTIME_PROVIDER_REGISTRY,
useFactory: createGatewayRuntimeProviderRegistry, useFactory: createGatewayRuntimeProviderRegistry,
@@ -76,7 +76,7 @@ export function createGatewayRuntimeProviderRegistry(): AgentRuntimeProviderRegi
RoutingService, RoutingService,
RoutingEngineService, RoutingEngineService,
SkillLoaderService, SkillLoaderService,
TessDurableSessionService, DurableSessionService,
RuntimeProviderService, RuntimeProviderService,
AGENT_RUNTIME_PROVIDER_REGISTRY, AGENT_RUNTIME_PROVIDER_REGISTRY,
], ],

View File

@@ -1,7 +1,7 @@
import type { RuntimeProviderRequestContext } from './runtime-provider-registry.service.js'; import type { RuntimeProviderRequestContext } from './runtime-provider-registry.service.js';
/** Server-side request for a replay-safe provider message. */ /** Server-side request for a replay-safe provider message. */
export interface TessProviderOutboxDto { export interface ProviderOutboxDto {
sessionId: string; sessionId: string;
idempotencyKey: string; idempotencyKey: string;
correlationId: string; correlationId: string;

View File

@@ -6,8 +6,8 @@ import { eq, sql, interactionCheckpoints, interactionInbox } from '@mosaicstack/
import { DurableSessionCoordinator, type DurableSessionIdentity } from '@mosaicstack/agent'; import { DurableSessionCoordinator, type DurableSessionIdentity } from '@mosaicstack/agent';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { createPgliteDb, runPgliteMigrations, type DbHandle } from '@mosaicstack/db'; import { createPgliteDb, runPgliteMigrations, type DbHandle } from '@mosaicstack/db';
import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; import { DurableSessionRepository } from './durable-session.repository.js';
import { TessDurableSessionService } from './tess-durable-session.service.js'; import { DurableSessionService } from './durable-session.service.js';
const IDENTITY: DurableSessionIdentity = { const IDENTITY: DurableSessionIdentity = {
agentName: 'Nova', agentName: 'Nova',
@@ -18,7 +18,7 @@ const IDENTITY: DurableSessionIdentity = {
runtimeSessionId: 'nova', runtimeSessionId: 'nova',
}; };
describe('TessDurableSessionRepository', () => { describe('DurableSessionRepository', () => {
let dataDir: string | undefined; let dataDir: string | undefined;
let handle: DbHandle; let handle: DbHandle;
let previousAuthSecret: string | undefined; let previousAuthSecret: string | undefined;
@@ -48,9 +48,7 @@ describe('TessDurableSessionRepository', () => {
}); });
it('survives a full PGlite close/reopen mid-session without duplicate inbox or outbox side effects', async () => { it('survives a full PGlite close/reopen mid-session without duplicate inbox or outbox side effects', async () => {
const beforeRestart = new DurableSessionCoordinator( const beforeRestart = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
new TessDurableSessionRepository(handle.db),
);
await beforeRestart.create(IDENTITY); await beforeRestart.create(IDENTITY);
await beforeRestart.receive({ await beforeRestart.receive({
sessionId: IDENTITY.sessionId, sessionId: IDENTITY.sessionId,
@@ -92,7 +90,7 @@ describe('TessDurableSessionRepository', () => {
await handle.close(); await handle.close();
handle = createPgliteDb(dataDir!); handle = createPgliteDb(dataDir!);
const afterRestart = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); const afterRestart = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
const recovered = await afterRestart.recover(IDENTITY.sessionId); const recovered = await afterRestart.recover(IDENTITY.sessionId);
const resumedHandoff = await afterRestart.resumeHandoff('handoff-before-kill'); const resumedHandoff = await afterRestart.resumeHandoff('handoff-before-kill');
const handled: string[] = []; const handled: string[] = [];
@@ -120,7 +118,7 @@ describe('TessDurableSessionRepository', () => {
}, 30_000); }, 30_000);
it('redacts sensitive durable payloads before persistence', async () => { it('redacts sensitive durable payloads before persistence', async () => {
const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
await coordinator.create(IDENTITY); await coordinator.create(IDENTITY);
await coordinator.receive({ await coordinator.receive({
sessionId: IDENTITY.sessionId, sessionId: IDENTITY.sessionId,
@@ -157,7 +155,7 @@ describe('TessDurableSessionRepository', () => {
}, 30_000); }, 30_000);
it('fails closed when the configured idempotency secret is unavailable', async () => { it('fails closed when the configured idempotency secret is unavailable', async () => {
const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
await coordinator.create(IDENTITY); await coordinator.create(IDENTITY);
const secret = process.env['BETTER_AUTH_SECRET']; const secret = process.env['BETTER_AUTH_SECRET'];
delete process.env['BETTER_AUTH_SECRET']; delete process.env['BETTER_AUTH_SECRET'];
@@ -177,7 +175,7 @@ describe('TessDurableSessionRepository', () => {
}, 30_000); }, 30_000);
it('uses keyed pre-redaction digests to reject distinct sensitive checkpoint payloads', async () => { it('uses keyed pre-redaction digests to reject distinct sensitive checkpoint payloads', async () => {
const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
await coordinator.create(IDENTITY); await coordinator.create(IDENTITY);
const input = { const input = {
sessionId: IDENTITY.sessionId, sessionId: IDENTITY.sessionId,
@@ -227,7 +225,7 @@ describe('TessDurableSessionRepository', () => {
}, 30_000); }, 30_000);
it('rejects distinct sensitive inbox and outbox payloads under reused idempotency keys', async () => { it('rejects distinct sensitive inbox and outbox payloads under reused idempotency keys', async () => {
const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
await coordinator.create(IDENTITY); await coordinator.create(IDENTITY);
const inbox = { const inbox = {
sessionId: IDENTITY.sessionId, sessionId: IDENTITY.sessionId,
@@ -255,7 +253,7 @@ describe('TessDurableSessionRepository', () => {
}, 30_000); }, 30_000);
it('rejects database inbox and outbox idempotency-key conflicts', async () => { it('rejects database inbox and outbox idempotency-key conflicts', async () => {
const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
await coordinator.create(IDENTITY); await coordinator.create(IDENTITY);
await coordinator.receive({ await coordinator.receive({
sessionId: IDENTITY.sessionId, sessionId: IDENTITY.sessionId,
@@ -293,10 +291,10 @@ describe('TessDurableSessionRepository', () => {
}, 30_000); }, 30_000);
it('does not requeue a live outbox claim during a normal scoped dispatch', async () => { it('does not requeue a live outbox claim during a normal scoped dispatch', async () => {
const repository = new TessDurableSessionRepository(handle.db); const repository = new DurableSessionRepository(handle.db);
const coordinator = new DurableSessionCoordinator(repository); const coordinator = new DurableSessionCoordinator(repository);
const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) }; const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) };
const service = new TessDurableSessionService(repository, runtimeProviders as never); const service = new DurableSessionService(repository, runtimeProviders as never);
const input = { const input = {
sessionId: IDENTITY.sessionId, sessionId: IDENTITY.sessionId,
idempotencyKey: 'live-effect', idempotencyKey: 'live-effect',
@@ -333,10 +331,10 @@ describe('TessDurableSessionRepository', () => {
}, 30_000); }, 30_000);
it('rejects an outbox correlation mismatch before claiming the pending effect', async () => { it('rejects an outbox correlation mismatch before claiming the pending effect', async () => {
const repository = new TessDurableSessionRepository(handle.db); const repository = new DurableSessionRepository(handle.db);
const coordinator = new DurableSessionCoordinator(repository); const coordinator = new DurableSessionCoordinator(repository);
const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) }; const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) };
const service = new TessDurableSessionService(repository, runtimeProviders as never); const service = new DurableSessionService(repository, runtimeProviders as never);
const input = { const input = {
sessionId: IDENTITY.sessionId, sessionId: IDENTITY.sessionId,
idempotencyKey: 'mismatch-effect', idempotencyKey: 'mismatch-effect',
@@ -366,10 +364,10 @@ describe('TessDurableSessionRepository', () => {
}, 30_000); }, 30_000);
it('dispatches only the outbox record bound to the supplied correlation and channel', async () => { it('dispatches only the outbox record bound to the supplied correlation and channel', async () => {
const repository = new TessDurableSessionRepository(handle.db); const repository = new DurableSessionRepository(handle.db);
const coordinator = new DurableSessionCoordinator(repository); const coordinator = new DurableSessionCoordinator(repository);
const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) }; const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) };
const service = new TessDurableSessionService(repository, runtimeProviders as never); const service = new DurableSessionService(repository, runtimeProviders as never);
const first = { const first = {
sessionId: IDENTITY.sessionId, sessionId: IDENTITY.sessionId,
idempotencyKey: 'scoped-effect-one', idempotencyKey: 'scoped-effect-one',

View File

@@ -33,7 +33,7 @@ import type {
import { DB } from '../database/database.module.js'; import { DB } from '../database/database.module.js';
@Injectable() @Injectable()
export class TessDurableSessionRepository implements DurableSessionStore { export class DurableSessionRepository implements DurableSessionStore {
constructor(@Inject(DB) private readonly db: Db) {} constructor(@Inject(DB) private readonly db: Db) {}
async create(identity: DurableSessionIdentity): Promise<void> { async create(identity: DurableSessionIdentity): Promise<void> {
@@ -51,7 +51,7 @@ export class TessDurableSessionRepository implements DurableSessionStore {
const existing = await this.session(identity.sessionId); const existing = await this.session(identity.sessionId);
if (!existing || !sameEnrollmentScope(existing, identity)) { if (!existing || !sameEnrollmentScope(existing, identity)) {
throw new Error(`Durable Tess session identity conflict: ${identity.sessionId}`); throw new Error(`Durable session identity conflict: ${identity.sessionId}`);
} }
// A recovered/re-enrolled runtime can receive a new provider session ID; // A recovered/re-enrolled runtime can receive a new provider session ID;
// the conversation handle and owner scope remain immutable. // the conversation handle and owner scope remain immutable.
@@ -135,13 +135,13 @@ export class TessDurableSessionRepository implements DurableSessionStore {
), ),
) )
.limit(1); .limit(1);
if (!existing[0]) throw new Error(`Durable Tess inbox enqueue failed: ${input.idempotencyKey}`); if (!existing[0]) throw new Error(`Durable inbox enqueue failed: ${input.idempotencyKey}`);
const entry = toInbox(existing[0]); const entry = toInbox(existing[0]);
if ( if (
!sameInbox(entry, record) || !sameInbox(entry, record) ||
!matchesContentDigest(existing[0].contentDigest, input.content) !matchesContentDigest(existing[0].contentDigest, input.content)
) { ) {
throw new Error(`Durable Tess inbox idempotency conflict: ${input.idempotencyKey}`); throw new Error(`Durable inbox idempotency conflict: ${input.idempotencyKey}`);
} }
return { accepted: false, status: entry.status }; return { accepted: false, status: entry.status };
} }
@@ -224,14 +224,13 @@ export class TessDurableSessionRepository implements DurableSessionStore {
), ),
) )
.limit(1); .limit(1);
if (!existing[0]) if (!existing[0]) throw new Error(`Durable outbox enqueue failed: ${input.idempotencyKey}`);
throw new Error(`Durable Tess outbox enqueue failed: ${input.idempotencyKey}`);
const entry = toOutbox(existing[0]); const entry = toOutbox(existing[0]);
if ( if (
!sameOutbox(entry, record) || !sameOutbox(entry, record) ||
!matchesContentDigest(existing[0].contentDigest, input.content) !matchesContentDigest(existing[0].contentDigest, input.content)
) { ) {
throw new Error(`Durable Tess outbox idempotency conflict: ${input.idempotencyKey}`); throw new Error(`Durable outbox idempotency conflict: ${input.idempotencyKey}`);
} }
return { accepted: false, status: entry.status }; return { accepted: false, status: entry.status };
} }
@@ -338,7 +337,7 @@ export class TessDurableSessionRepository implements DurableSessionStore {
!sameCheckpoint(toCheckpoint(existing[0]), checkpoint) || !sameCheckpoint(toCheckpoint(existing[0]), checkpoint) ||
!matchesCheckpointDigest(existing[0].contentDigest, digest) !matchesCheckpointDigest(existing[0].contentDigest, digest)
) { ) {
throw new Error(`Durable Tess checkpoint identity conflict: ${input.checkpointId}`); throw new Error(`Durable checkpoint identity conflict: ${input.checkpointId}`);
} }
} }
@@ -360,7 +359,7 @@ export class TessDurableSessionRepository implements DurableSessionStore {
async handoff(input: DurableHandoffInput): Promise<void> { async handoff(input: DurableHandoffInput): Promise<void> {
const checkpoint = await this.findCheckpoint(input.sessionId, input.checkpointId); const checkpoint = await this.findCheckpoint(input.sessionId, input.checkpointId);
if (!checkpoint) { if (!checkpoint) {
throw new Error(`Durable Tess handoff checkpoint is unavailable: ${input.checkpointId}`); throw new Error(`Durable handoff checkpoint is unavailable: ${input.checkpointId}`);
} }
const inserted = await this.db const inserted = await this.db
.insert(interactionHandoffs) .insert(interactionHandoffs)
@@ -371,7 +370,7 @@ export class TessDurableSessionRepository implements DurableSessionStore {
const existing = await this.findHandoff(input.handoffId); const existing = await this.findHandoff(input.handoffId);
if (!existing || !sameHandoff(existing, input)) { if (!existing || !sameHandoff(existing, input)) {
throw new Error(`Durable Tess handoff identity conflict: ${input.handoffId}`); throw new Error(`Durable handoff identity conflict: ${input.handoffId}`);
} }
} }

View File

@@ -1,23 +1,23 @@
import { ForbiddenException, Inject, Injectable } from '@nestjs/common'; import { ForbiddenException, Inject, Injectable } from '@nestjs/common';
import { DurableSessionCoordinator, type DurableSessionIdentity } from '@mosaicstack/agent'; import { DurableSessionCoordinator, type DurableSessionIdentity } from '@mosaicstack/agent';
import type { TessProviderOutboxDto } from './tess-durable-session.dto.js'; import type { ProviderOutboxDto } from './durable-session.dto.js';
import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; import { DurableSessionRepository } from './durable-session.repository.js';
import { import {
RuntimeProviderService, RuntimeProviderService,
type RuntimeProviderRequestContext, type RuntimeProviderRequestContext,
} from './runtime-provider-registry.service.js'; } from './runtime-provider-registry.service.js';
/** /**
* Scoped gateway boundary for the canonical Tess state machine. It deliberately * Scoped gateway boundary for the canonical durable session state machine. It deliberately
* uses composition: raw state methods cannot be injected into channel, CLI, or * uses composition: raw state methods cannot be injected into channel, CLI, or
* MCP adapters without a server-derived actor/tenant/correlation context. * MCP adapters without a server-derived actor/tenant/correlation context.
*/ */
@Injectable() @Injectable()
export class TessDurableSessionService { export class DurableSessionService {
private readonly coordinator: DurableSessionCoordinator; private readonly coordinator: DurableSessionCoordinator;
constructor( constructor(
@Inject(TessDurableSessionRepository) repository: TessDurableSessionRepository, @Inject(DurableSessionRepository) repository: DurableSessionRepository,
@Inject(RuntimeProviderService) private readonly runtimeProviders: RuntimeProviderService, @Inject(RuntimeProviderService) private readonly runtimeProviders: RuntimeProviderService,
) { ) {
this.coordinator = new DurableSessionCoordinator(repository); this.coordinator = new DurableSessionCoordinator(repository);
@@ -32,12 +32,12 @@ export class TessDurableSessionService {
identity.ownerId !== context.actorScope.userId || identity.ownerId !== context.actorScope.userId ||
identity.tenantId !== context.actorScope.tenantId identity.tenantId !== context.actorScope.tenantId
) { ) {
throw new ForbiddenException('Durable Tess enrollment scope mismatch'); throw new ForbiddenException('Durable session enrollment scope mismatch');
} }
await this.coordinator.create(identity); await this.coordinator.create(identity);
} }
async queueProviderSend(input: TessProviderOutboxDto): Promise<void> { async queueProviderSend(input: ProviderOutboxDto): Promise<void> {
const snapshot = await this.coordinator.snapshot(input.sessionId); const snapshot = await this.coordinator.snapshot(input.sessionId);
this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input); this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input);
await this.coordinator.enqueueOutbox({ await this.coordinator.enqueueOutbox({
@@ -50,9 +50,9 @@ export class TessDurableSessionService {
}); });
} }
async dispatchProviderOutbox(sessionId: string, input: TessProviderOutboxDto): Promise<void> { async dispatchProviderOutbox(sessionId: string, input: ProviderOutboxDto): Promise<void> {
if (sessionId !== input.sessionId) { if (sessionId !== input.sessionId) {
throw new ForbiddenException('Durable Tess outbox session mismatch'); throw new ForbiddenException('Durable outbox session mismatch');
} }
const snapshot = await this.coordinator.snapshot(sessionId); const snapshot = await this.coordinator.snapshot(sessionId);
this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input); this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input);
@@ -92,7 +92,7 @@ export class TessDurableSessionService {
} }
/** Startup/recovery-only path; normal queue/dispatch methods never requeue live work. */ /** Startup/recovery-only path; normal queue/dispatch methods never requeue live work. */
async recoverProviderSession(sessionId: string, input: TessProviderOutboxDto): Promise<void> { async recoverProviderSession(sessionId: string, input: ProviderOutboxDto): Promise<void> {
const snapshot = await this.coordinator.snapshot(sessionId); const snapshot = await this.coordinator.snapshot(sessionId);
this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input); this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input);
await this.coordinator.recover(sessionId); await this.coordinator.recover(sessionId);
@@ -100,24 +100,24 @@ export class TessDurableSessionService {
private assertOutboxScope( private assertOutboxScope(
entry: { kind: string; correlationId: string; channelId: string }, entry: { kind: string; correlationId: string; channelId: string },
input: TessProviderOutboxDto, input: ProviderOutboxDto,
): void { ): void {
if ( if (
entry.kind !== 'provider.send' || entry.kind !== 'provider.send' ||
entry.correlationId !== input.correlationId || entry.correlationId !== input.correlationId ||
entry.channelId !== input.context.channelId entry.channelId !== input.context.channelId
) { ) {
throw new ForbiddenException('Durable Tess outbox scope or correlation mismatch'); throw new ForbiddenException('Durable outbox scope or correlation mismatch');
} }
} }
private assertScope(ownerId: string, tenantId: string, input: TessProviderOutboxDto): void { private assertScope(ownerId: string, tenantId: string, input: ProviderOutboxDto): void {
if ( if (
input.context.actorScope.userId !== ownerId || input.context.actorScope.userId !== ownerId ||
input.context.actorScope.tenantId !== tenantId || input.context.actorScope.tenantId !== tenantId ||
input.context.correlationId !== input.correlationId input.context.correlationId !== input.correlationId
) { ) {
throw new ForbiddenException('Durable Tess session scope or correlation mismatch'); throw new ForbiddenException('Durable session scope or correlation mismatch');
} }
} }
} }

View File

@@ -21,8 +21,8 @@ import {
RUNTIME_PROVIDER_AUDIT_SINK, RUNTIME_PROVIDER_AUDIT_SINK,
RuntimeProviderAuditService, RuntimeProviderAuditService,
} from './runtime-provider-registry.service.js'; } from './runtime-provider-registry.service.js';
import { TessDurableSessionService } from './tess-durable-session.service.js'; import { DurableSessionService } from './durable-session.service.js';
import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; import { DurableSessionRepository } from './durable-session.repository.js';
import { AgentService } from './agent.service.js'; import { AgentService } from './agent.service.js';
import { ProviderService } from './provider.service.js'; import { ProviderService } from './provider.service.js';
import { ProviderCredentialsService } from './provider-credentials.service.js'; import { ProviderCredentialsService } from './provider-credentials.service.js';
@@ -88,9 +88,9 @@ describe('Hermes runtime provider reachability', (): void => {
.useValue({ record: vi.fn().mockResolvedValue(undefined) }) .useValue({ record: vi.fn().mockResolvedValue(undefined) })
.overrideProvider(RUNTIME_APPROVAL_VERIFIER) .overrideProvider(RUNTIME_APPROVAL_VERIFIER)
.useValue({ consume: vi.fn().mockResolvedValue(false) }) .useValue({ consume: vi.fn().mockResolvedValue(false) })
.overrideProvider(TessDurableSessionService) .overrideProvider(DurableSessionService)
.useValue({}) .useValue({})
.overrideProvider(TessDurableSessionRepository) .overrideProvider(DurableSessionRepository)
.useValue({}) .useValue({})
.overrideProvider(AgentService) .overrideProvider(AgentService)
.useValue({}) .useValue({})

View File

@@ -17,7 +17,7 @@ import { Observable } from 'rxjs';
import { AuthGuard } from '../auth/auth.guard.js'; import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js'; import { CurrentUser } from '../auth/current-user.decorator.js';
import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js'; import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js';
import { TessDurableSessionService } from './tess-durable-session.service.js'; import { DurableSessionService } from './durable-session.service.js';
import { RuntimeApprovalDeniedFilter } from './runtime-approval-denied.filter.js'; import { RuntimeApprovalDeniedFilter } from './runtime-approval-denied.filter.js';
import { import {
RuntimeProviderService, RuntimeProviderService,
@@ -34,7 +34,7 @@ import {
export class InteractionController { export class InteractionController {
constructor( constructor(
@Inject(RuntimeProviderService) private readonly runtime: RuntimeProviderService, @Inject(RuntimeProviderService) private readonly runtime: RuntimeProviderService,
@Inject(TessDurableSessionService) private readonly durable: TessDurableSessionService, @Inject(DurableSessionService) private readonly durable: DurableSessionService,
) {} ) {}
@Get('sessions') @Get('sessions')

View File

@@ -37,7 +37,7 @@ import {
RuntimeProviderService, RuntimeProviderService,
type RuntimeAuditSink, type RuntimeAuditSink,
} from '../agent/runtime-provider-registry.service.js'; } from '../agent/runtime-provider-registry.service.js';
import { TessDurableSessionService } from '../agent/tess-durable-session.service.js'; import { DurableSessionService } from '../agent/durable-session.service.js';
import { AUTH } from '../auth/auth.tokens.js'; import { AUTH } from '../auth/auth.tokens.js';
import { import {
scopeFromUser, scopeFromUser,
@@ -140,8 +140,8 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
@Inject(RuntimeProviderService) @Inject(RuntimeProviderService)
private readonly runtimeRegistry: RuntimeProviderService | null = null, private readonly runtimeRegistry: RuntimeProviderService | null = null,
@Optional() @Optional()
@Inject(TessDurableSessionService) @Inject(DurableSessionService)
private readonly durableSessions: TessDurableSessionService | null = null, private readonly durableSessions: DurableSessionService | null = null,
@Optional() @Optional()
@Inject(RUNTIME_PROVIDER_AUDIT_SINK) @Inject(RUNTIME_PROVIDER_AUDIT_SINK)
private readonly runtimeAudit: RuntimeAuditSink | null = null, private readonly runtimeAudit: RuntimeAuditSink | null = null,

View File

@@ -1,31 +1,32 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { InMemoryMosCoordinationPort } from '@mosaicstack/coord'; import { InMemoryInteractionCoordinationPort } from '@mosaicstack/coord';
import { CoordService } from './coord.service.js'; import { CoordService } from './coord.service.js';
import { CoordController } from './coord.controller.js'; import { CoordController } from './coord.controller.js';
import { MosCoordinationController } from './mos-coordination.controller.js'; import { InteractionCoordinationController } from './interaction-coordination.controller.js';
import { import {
MOS_COORDINATION_CONFIG, COORDINATION_CONFIG,
MOS_COORDINATION_PORT, COORDINATION_PORT,
MosCoordinationService, InteractionCoordinationService,
} from './mos-coordination.service.js'; } from './interaction-coordination.service.js';
@Module({ @Module({
providers: [ providers: [
CoordService, CoordService,
{ {
provide: MOS_COORDINATION_PORT, provide: COORDINATION_PORT,
useFactory: (): InMemoryMosCoordinationPort => new InMemoryMosCoordinationPort(), useFactory: (): InMemoryInteractionCoordinationPort =>
new InMemoryInteractionCoordinationPort(),
}, },
{ {
provide: MOS_COORDINATION_CONFIG, provide: COORDINATION_CONFIG,
useFactory: () => ({ useFactory: () => ({
interactionAgentId: process.env['MOSAIC_AGENT_NAME'], interactionAgentId: process.env['MOSAIC_AGENT_NAME'],
orchestrationAgentId: process.env['MOSAIC_ORCHESTRATOR_AGENT_NAME'], orchestrationAgentId: process.env['MOSAIC_ORCHESTRATOR_AGENT_NAME'],
}), }),
}, },
MosCoordinationService, InteractionCoordinationService,
], ],
controllers: [CoordController, MosCoordinationController], controllers: [CoordController, InteractionCoordinationController],
exports: [CoordService, MosCoordinationService], exports: [CoordService, InteractionCoordinationService],
}) })
export class CoordModule {} export class CoordModule {}

View File

@@ -1,16 +1,16 @@
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { MosCoordinationController } from './mos-coordination.controller.js'; import { InteractionCoordinationController } from './interaction-coordination.controller.js';
const user = { id: 'operator-1', tenantId: 'tenant-a' }; const user = { id: 'operator-1', tenantId: 'tenant-a' };
describe('MosCoordinationController', () => { describe('InteractionCoordinationController', () => {
it('derives actor and tenant from the authenticated user rather than handoff input', async () => { it('derives actor and tenant from the authenticated user rather than handoff input', async () => {
const coordination = { const coordination = {
handoff: vi.fn(async () => ({ handoffId: 'handoff-1' })), handoff: vi.fn(async () => ({ handoffId: 'handoff-1' })),
observe: vi.fn(), observe: vi.fn(),
result: vi.fn(), result: vi.fn(),
}; };
const controller = new MosCoordinationController(coordination as never); const controller = new InteractionCoordinationController(coordination as never);
await controller.handoff({ idempotencyKey: 'request-1', summary: 'Implement' }, user, 'corr-1'); await controller.handoff({ idempotencyKey: 'request-1', summary: 'Implement' }, user, 'corr-1');
@@ -26,7 +26,7 @@ describe('MosCoordinationController', () => {
it('requires a correlation header before invoking the coordination service', async () => { it('requires a correlation header before invoking the coordination service', async () => {
const coordination = { handoff: vi.fn(), observe: vi.fn(), result: vi.fn() }; const coordination = { handoff: vi.fn(), observe: vi.fn(), result: vi.fn() };
const controller = new MosCoordinationController(coordination as never); const controller = new InteractionCoordinationController(coordination as never);
await expect( await expect(
controller.handoff({ idempotencyKey: 'request-1', summary: 'Implement' }, user, undefined), controller.handoff({ idempotencyKey: 'request-1', summary: 'Implement' }, user, undefined),

View File

@@ -14,27 +14,28 @@ import { CurrentUser } from '../auth/current-user.decorator.js';
import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js'; import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js';
import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js'; import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js';
import type { import type {
MosCoordinationObservationDto, InteractionCoordinationObservationDto,
MosCoordinationResponseDto, InteractionCoordinationResponseDto,
MosCoordinationResultDto, InteractionCoordinationResultDto,
CreateMosHandoffDto, CreateHandoffDto,
} from './mos-coordination.dto.js'; } from './interaction-coordination.dto.js';
import { MosCoordinationService } from './mos-coordination.service.js'; import { InteractionCoordinationService } from './interaction-coordination.service.js';
/** Authenticated interaction-plane boundary for the handoff/observe/result-only Mos contract. */ /** Authenticated interaction-plane boundary for the handoff/observe/result-only interaction coordination contract. */
@Controller('api/coord/mos') @Controller('api/coord/mos')
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
export class MosCoordinationController { export class InteractionCoordinationController {
constructor( constructor(
@Inject(MosCoordinationService) private readonly coordination: MosCoordinationService, @Inject(InteractionCoordinationService)
private readonly coordination: InteractionCoordinationService,
) {} ) {}
@Post('handoff') @Post('handoff')
async handoff( async handoff(
@Body() request: CreateMosHandoffDto, @Body() request: CreateHandoffDto,
@CurrentUser() user: AuthenticatedUserLike, @CurrentUser() user: AuthenticatedUserLike,
@Headers('x-correlation-id') correlationId?: string, @Headers('x-correlation-id') correlationId?: string,
): Promise<MosCoordinationResponseDto> { ): Promise<InteractionCoordinationResponseDto> {
return { receipt: await this.coordination.handoff(request, this.context(user, correlationId)) }; return { receipt: await this.coordination.handoff(request, this.context(user, correlationId)) };
} }
@@ -43,7 +44,7 @@ export class MosCoordinationController {
@Param('handoffId') handoffId: string, @Param('handoffId') handoffId: string,
@CurrentUser() user: AuthenticatedUserLike, @CurrentUser() user: AuthenticatedUserLike,
@Headers('x-correlation-id') correlationId?: string, @Headers('x-correlation-id') correlationId?: string,
): Promise<MosCoordinationObservationDto> { ): Promise<InteractionCoordinationObservationDto> {
return { return {
observation: await this.coordination.observe(handoffId, this.context(user, correlationId)), observation: await this.coordination.observe(handoffId, this.context(user, correlationId)),
}; };
@@ -54,7 +55,7 @@ export class MosCoordinationController {
@Param('handoffId') handoffId: string, @Param('handoffId') handoffId: string,
@CurrentUser() user: AuthenticatedUserLike, @CurrentUser() user: AuthenticatedUserLike,
@Headers('x-correlation-id') correlationId?: string, @Headers('x-correlation-id') correlationId?: string,
): Promise<MosCoordinationResultDto> { ): Promise<InteractionCoordinationResultDto> {
return { result: await this.coordination.result(handoffId, this.context(user, correlationId)) }; return { result: await this.coordination.result(handoffId, this.context(user, correlationId)) };
} }

View File

@@ -1,25 +1,25 @@
import type { import type {
CoordinationObservation, CoordinationObservation,
CoordinationResult, CoordinationResult,
MosHandoffReceipt, HandoffReceipt,
} from '@mosaicstack/coord'; } from '@mosaicstack/coord';
/** Input accepted at the gateway coordination boundary. Agent identity is not caller-controlled. */ /** Input accepted at the gateway coordination boundary. Agent identity is not caller-controlled. */
export interface CreateMosHandoffDto { export interface CreateHandoffDto {
idempotencyKey: string; idempotencyKey: string;
summary: string; summary: string;
context?: string; context?: string;
missionId?: string; missionId?: string;
} }
export interface MosCoordinationResponseDto { export interface InteractionCoordinationResponseDto {
receipt: MosHandoffReceipt; receipt: HandoffReceipt;
} }
export interface MosCoordinationObservationDto { export interface InteractionCoordinationObservationDto {
observation: CoordinationObservation; observation: CoordinationObservation;
} }
export interface MosCoordinationResultDto { export interface InteractionCoordinationResultDto {
result: CoordinationResult; result: CoordinationResult;
} }

View File

@@ -1,15 +1,15 @@
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { import {
InMemoryMosCoordinationPort, InMemoryInteractionCoordinationPort,
type MosCoordinationPort, type InteractionCoordinationPort,
type MosHandoff, type Handoff,
} from '@mosaicstack/coord'; } from '@mosaicstack/coord';
import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js'; import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js';
import { import {
MosCoordinationService, InteractionCoordinationService,
type MosCoordinationConfig, type InteractionCoordinationConfig,
type MosCoordinationGatewayError, type InteractionCoordinationGatewayError,
} from './mos-coordination.service.js'; } from './interaction-coordination.service.js';
const context: RuntimeProviderRequestContext = { const context: RuntimeProviderRequestContext = {
actorScope: { userId: 'operator-1', tenantId: 'tenant-a' }, actorScope: { userId: 'operator-1', tenantId: 'tenant-a' },
@@ -17,28 +17,28 @@ const context: RuntimeProviderRequestContext = {
correlationId: 'corr-1', correlationId: 'corr-1',
}; };
const config: MosCoordinationConfig = { const config: InteractionCoordinationConfig = {
interactionAgentId: 'Nova', interactionAgentId: 'Nova',
orchestrationAgentId: 'Conductor', orchestrationAgentId: 'Conductor',
}; };
function service( function service(
port: MosCoordinationPort = new InMemoryMosCoordinationPort(), port: InteractionCoordinationPort = new InMemoryInteractionCoordinationPort(),
options: { options: {
config?: MosCoordinationConfig; config?: InteractionCoordinationConfig;
handoffIdFactory?: () => string; handoffIdFactory?: () => string;
} = {}, } = {},
): MosCoordinationService { ): InteractionCoordinationService {
return new MosCoordinationService( return new InteractionCoordinationService(
port, port,
options.config ?? config, options.config ?? config,
options.handoffIdFactory ?? (() => 'handoff-1'), options.handoffIdFactory ?? (() => 'handoff-1'),
); );
} }
describe('MosCoordinationService authority boundary', (): void => { describe('InteractionCoordinationService authority boundary', (): void => {
it('derives identity and actor/tenant scope server-side, then round-trips the native adapter', async (): Promise<void> => { it('derives identity and actor/tenant scope server-side, then round-trips the native adapter', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort(); const adapter = new InMemoryInteractionCoordinationPort();
const coordination = service(adapter); const coordination = service(adapter);
await expect( await expect(
@@ -53,8 +53,8 @@ describe('MosCoordinationService authority boundary', (): void => {
correlationId: 'corr-1', correlationId: 'corr-1',
}); });
adapter.recordActivity('handoff-1', 'running', 'Mos accepted the request'); adapter.recordActivity('handoff-1', 'running', 'Orchestrator accepted the request');
adapter.recordResult('handoff-1', 'completed', 'Merged by Mos'); adapter.recordResult('handoff-1', 'completed', 'Merged by orchestrator');
const followUpContext = { ...context, correlationId: 'corr-2' }; const followUpContext = { ...context, correlationId: 'corr-2' };
await expect(coordination.observe('handoff-1', followUpContext)).resolves.toMatchObject({ await expect(coordination.observe('handoff-1', followUpContext)).resolves.toMatchObject({
@@ -64,12 +64,12 @@ describe('MosCoordinationService authority boundary', (): void => {
await expect(coordination.result('handoff-1', followUpContext)).resolves.toMatchObject({ await expect(coordination.result('handoff-1', followUpContext)).resolves.toMatchObject({
targetAgentId: 'Conductor', targetAgentId: 'Conductor',
status: 'completed', status: 'completed',
summary: 'Merged by Mos', summary: 'Merged by orchestrator',
}); });
}); });
it('fails closed without calling a port when the interaction requester is unconfigured', async (): Promise<void> => { it('fails closed without calling a port when the interaction requester is unconfigured', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort(); const adapter = new InMemoryInteractionCoordinationPort();
const handoff = vi.spyOn(adapter, 'handoff'); const handoff = vi.spyOn(adapter, 'handoff');
const coordination = service(adapter, { const coordination = service(adapter, {
config: { interactionAgentId: '', orchestrationAgentId: 'Conductor' }, config: { interactionAgentId: '', orchestrationAgentId: 'Conductor' },
@@ -82,12 +82,12 @@ describe('MosCoordinationService authority boundary', (): void => {
), ),
).rejects.toMatchObject({ ).rejects.toMatchObject({
code: 'unconfigured_requester', code: 'unconfigured_requester',
} satisfies Partial<MosCoordinationGatewayError>); } satisfies Partial<InteractionCoordinationGatewayError>);
expect(handoff).not.toHaveBeenCalled(); expect(handoff).not.toHaveBeenCalled();
}); });
it('rejects self-delegation configuration before delivering work', async (): Promise<void> => { it('rejects self-delegation configuration before delivering work', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort(); const adapter = new InMemoryInteractionCoordinationPort();
const handoff = vi.spyOn(adapter, 'handoff'); const handoff = vi.spyOn(adapter, 'handoff');
const coordination = service(adapter, { const coordination = service(adapter, {
config: { interactionAgentId: 'Nova', orchestrationAgentId: 'Nova' }, config: { interactionAgentId: 'Nova', orchestrationAgentId: 'Nova' },
@@ -103,7 +103,7 @@ describe('MosCoordinationService authority boundary', (): void => {
}); });
it('denies cross-tenant observe and result before calling the adapter', async (): Promise<void> => { it('denies cross-tenant observe and result before calling the adapter', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort(); const adapter = new InMemoryInteractionCoordinationPort();
const observe = vi.spyOn(adapter, 'observe'); const observe = vi.spyOn(adapter, 'observe');
const result = vi.spyOn(adapter, 'result'); const result = vi.spyOn(adapter, 'result');
const coordination = service(adapter); const coordination = service(adapter);
@@ -118,10 +118,10 @@ describe('MosCoordinationService authority boundary', (): void => {
}; };
await expect(coordination.observe('handoff-1', otherTenant)).rejects.toMatchObject({ await expect(coordination.observe('handoff-1', otherTenant)).rejects.toMatchObject({
code: 'cross_tenant_forbidden', code: 'cross_tenant_forbidden',
} satisfies Partial<MosCoordinationGatewayError>); } satisfies Partial<InteractionCoordinationGatewayError>);
await expect(coordination.result('handoff-1', otherTenant)).rejects.toMatchObject({ await expect(coordination.result('handoff-1', otherTenant)).rejects.toMatchObject({
code: 'cross_tenant_forbidden', code: 'cross_tenant_forbidden',
} satisfies Partial<MosCoordinationGatewayError>); } satisfies Partial<InteractionCoordinationGatewayError>);
expect(observe).not.toHaveBeenCalled(); expect(observe).not.toHaveBeenCalled();
expect(result).not.toHaveBeenCalled(); expect(result).not.toHaveBeenCalled();
}); });
@@ -132,8 +132,8 @@ describe('MosCoordinationService authority boundary', (): void => {
const delivered = new Promise<void>((resolve: () => void): void => { const delivered = new Promise<void>((resolve: () => void): void => {
release = resolve; release = resolve;
}); });
const adapter: MosCoordinationPort = { const adapter: InteractionCoordinationPort = {
handoff: vi.fn(async (handoff: MosHandoff) => { handoff: vi.fn(async (handoff: Handoff) => {
await delivered; await delivered;
return { return {
handoffId: handoff.handoffId, handoffId: handoff.handoffId,
@@ -169,7 +169,7 @@ describe('MosCoordinationService authority boundary', (): void => {
}); });
it('rejects idempotency-key payload drift and malformed handoff input before delivery', async (): Promise<void> => { it('rejects idempotency-key payload drift and malformed handoff input before delivery', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort(); const adapter = new InMemoryInteractionCoordinationPort();
const handoff = vi.spyOn(adapter, 'handoff'); const handoff = vi.spyOn(adapter, 'handoff');
const coordination = service(adapter); const coordination = service(adapter);
await coordination.handoff( await coordination.handoff(
@@ -181,23 +181,23 @@ describe('MosCoordinationService authority boundary', (): void => {
coordination.handoff({ idempotencyKey: 'request-1', summary: 'Different work' }, context), coordination.handoff({ idempotencyKey: 'request-1', summary: 'Different work' }, context),
).rejects.toMatchObject({ ).rejects.toMatchObject({
code: 'handoff_conflict', code: 'handoff_conflict',
} satisfies Partial<MosCoordinationGatewayError>); } satisfies Partial<InteractionCoordinationGatewayError>);
await expect( await expect(
coordination.handoff({ idempotencyKey: 'request-2', summary: '' }, context), coordination.handoff({ idempotencyKey: 'request-2', summary: '' }, context),
).rejects.toMatchObject({ ).rejects.toMatchObject({
code: 'invalid_request', code: 'invalid_request',
} satisfies Partial<MosCoordinationGatewayError>); } satisfies Partial<InteractionCoordinationGatewayError>);
await expect( await expect(
coordination.handoff({ idempotencyKey: 'request-3', summary: 'x'.repeat(2_049) }, context), coordination.handoff({ idempotencyKey: 'request-3', summary: 'x'.repeat(2_049) }, context),
).rejects.toMatchObject({ ).rejects.toMatchObject({
code: 'invalid_request', code: 'invalid_request',
} satisfies Partial<MosCoordinationGatewayError>); } satisfies Partial<InteractionCoordinationGatewayError>);
expect(handoff).toHaveBeenCalledTimes(1); expect(handoff).toHaveBeenCalledTimes(1);
}); });
it('fails closed when the port reports a target that drifts from configuration', async (): Promise<void> => { it('fails closed when the port reports a target that drifts from configuration', async (): Promise<void> => {
const adapter: MosCoordinationPort = { const adapter: InteractionCoordinationPort = {
handoff: vi.fn(async (handoff: MosHandoff) => ({ handoff: vi.fn(async (handoff: Handoff) => ({
handoffId: handoff.handoffId, handoffId: handoff.handoffId,
targetAgentId: 'Unexpected', targetAgentId: 'Unexpected',
status: 'accepted' as const, status: 'accepted' as const,

View File

@@ -1,18 +1,18 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { import {
MosCoordinationClient, InteractionCoordinationClient,
type CoordinationObservation, type CoordinationObservation,
type CoordinationResult, type CoordinationResult,
type CoordinationScope, type CoordinationScope,
type MosCoordinationIdentity, type InteractionCoordinationIdentity,
type MosCoordinationPort, type InteractionCoordinationPort,
type MosHandoffReceipt, type HandoffReceipt,
} from '@mosaicstack/coord'; } from '@mosaicstack/coord';
import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js'; import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js';
import type { CreateMosHandoffDto } from './mos-coordination.dto.js'; import type { CreateHandoffDto } from './interaction-coordination.dto.js';
export const MOS_COORDINATION_PORT = Symbol('MOS_COORDINATION_PORT'); export const COORDINATION_PORT = Symbol('COORDINATION_PORT');
export const MOS_COORDINATION_CONFIG = Symbol('MOS_COORDINATION_CONFIG'); export const COORDINATION_CONFIG = Symbol('COORDINATION_CONFIG');
const HANDOFF_TRACKING_TTL_MS = 60 * 60 * 1_000; const HANDOFF_TRACKING_TTL_MS = 60 * 60 * 1_000;
const MAX_TRACKED_HANDOFFS = 1_000; const MAX_TRACKED_HANDOFFS = 1_000;
@@ -21,7 +21,7 @@ const MAX_SUMMARY_LENGTH = 2_048;
const MAX_CONTEXT_LENGTH = 8_192; const MAX_CONTEXT_LENGTH = 8_192;
const MAX_MISSION_ID_LENGTH = 128; const MAX_MISSION_ID_LENGTH = 128;
export interface MosCoordinationConfig { export interface InteractionCoordinationConfig {
interactionAgentId?: string; interactionAgentId?: string;
orchestrationAgentId?: string; orchestrationAgentId?: string;
} }
@@ -34,7 +34,7 @@ interface HandoffOwner {
expiresAt: number; expiresAt: number;
} }
interface NormalizedMosHandoffRequest { interface NormalizedHandoffRequest {
idempotencyKey: string; idempotencyKey: string;
summary: string; summary: string;
context?: string; context?: string;
@@ -42,31 +42,31 @@ interface NormalizedMosHandoffRequest {
} }
interface TrackedHandoff { interface TrackedHandoff {
request: NormalizedMosHandoffRequest; request: NormalizedHandoffRequest;
receipt: Promise<MosHandoffReceipt>; receipt: Promise<HandoffReceipt>;
expiresAt: number; expiresAt: number;
} }
/** /**
* Gateway authority boundary for the interaction agent. It derives requester, * Gateway authority boundary for the interaction agent. It derives requester,
* actor, and tenant from trusted server configuration and authentication; no * actor, and tenant from trusted server configuration and authentication; no
* channel request can name a target or gain Mos-owned orchestration verbs. * channel request can name a target or gain orchestrator-owned orchestration verbs.
*/ */
@Injectable() @Injectable()
export class MosCoordinationService { export class InteractionCoordinationService {
private readonly owners = new Map<string, HandoffOwner>(); private readonly owners = new Map<string, HandoffOwner>();
private readonly handoffsByIdempotencyKey = new Map<string, TrackedHandoff>(); private readonly handoffsByIdempotencyKey = new Map<string, TrackedHandoff>();
constructor( constructor(
@Inject(MOS_COORDINATION_PORT) private readonly port: MosCoordinationPort, @Inject(COORDINATION_PORT) private readonly port: InteractionCoordinationPort,
@Inject(MOS_COORDINATION_CONFIG) private readonly config: MosCoordinationConfig, @Inject(COORDINATION_CONFIG) private readonly config: InteractionCoordinationConfig,
private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(), private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(),
) {} ) {}
async handoff( async handoff(
request: CreateMosHandoffDto, request: CreateHandoffDto,
context: RuntimeProviderRequestContext, context: RuntimeProviderRequestContext,
): Promise<MosHandoffReceipt> { ): Promise<HandoffReceipt> {
this.pruneExpiredTracking(); this.pruneExpiredTracking();
const normalized = this.normalizeRequest(request); const normalized = this.normalizeRequest(request);
const scope = this.scope(context); const scope = this.scope(context);
@@ -74,9 +74,9 @@ export class MosCoordinationService {
const existing = this.handoffsByIdempotencyKey.get(idempotencyKey); const existing = this.handoffsByIdempotencyKey.get(idempotencyKey);
if (existing !== undefined) { if (existing !== undefined) {
if (!sameRequest(existing.request, normalized)) { if (!sameRequest(existing.request, normalized)) {
throw new MosCoordinationGatewayError( throw new InteractionCoordinationGatewayError(
'handoff_conflict', 'handoff_conflict',
'Mos handoff idempotency key is already bound to different immutable input', 'Handoff idempotency key is already bound to different immutable input',
); );
} }
return existing.receipt; return existing.receipt;
@@ -122,9 +122,9 @@ export class MosCoordinationService {
private async deliverHandoff( private async deliverHandoff(
handoffId: string, handoffId: string,
request: NormalizedMosHandoffRequest, request: NormalizedHandoffRequest,
scope: CoordinationScope, scope: CoordinationScope,
): Promise<MosHandoffReceipt> { ): Promise<HandoffReceipt> {
const receipt = await this.client((): string => handoffId).handoff(request, scope); const receipt = await this.client((): string => handoffId).handoff(request, scope);
const owner: HandoffOwner = { const owner: HandoffOwner = {
actorId: scope.actorId, actorId: scope.actorId,
@@ -135,9 +135,9 @@ export class MosCoordinationService {
}; };
const existing = this.owners.get(receipt.handoffId); const existing = this.owners.get(receipt.handoffId);
if (existing !== undefined && !sameOwner(existing, owner)) { if (existing !== undefined && !sameOwner(existing, owner)) {
throw new MosCoordinationGatewayError( throw new InteractionCoordinationGatewayError(
'handoff_conflict', 'handoff_conflict',
'Mos handoff ID is already bound to a different authenticated scope', 'Handoff ID is already bound to a different authenticated scope',
); );
} }
this.owners.set(receipt.handoffId, owner); this.owners.set(receipt.handoffId, owner);
@@ -145,21 +145,21 @@ export class MosCoordinationService {
return receipt; return receipt;
} }
private client(handoffIdFactory?: () => string): MosCoordinationClient { private client(handoffIdFactory?: () => string): InteractionCoordinationClient {
return new MosCoordinationClient(this.identity(), this.port, handoffIdFactory); return new InteractionCoordinationClient(this.identity(), this.port, handoffIdFactory);
} }
private identity(): MosCoordinationIdentity { private identity(): InteractionCoordinationIdentity {
const interactionAgentId = this.config.interactionAgentId?.trim(); const interactionAgentId = this.config.interactionAgentId?.trim();
const orchestrationAgentId = this.config.orchestrationAgentId?.trim(); const orchestrationAgentId = this.config.orchestrationAgentId?.trim();
if (!interactionAgentId) { if (!interactionAgentId) {
throw new MosCoordinationGatewayError( throw new InteractionCoordinationGatewayError(
'unconfigured_requester', 'unconfigured_requester',
'Interaction agent identity is not configured', 'Interaction agent identity is not configured',
); );
} }
if (!orchestrationAgentId) { if (!orchestrationAgentId) {
throw new MosCoordinationGatewayError( throw new InteractionCoordinationGatewayError(
'unconfigured_target', 'unconfigured_target',
'Orchestration agent identity is not configured', 'Orchestration agent identity is not configured',
); );
@@ -177,9 +177,12 @@ export class MosCoordinationService {
}); });
} }
private normalizeRequest(request: CreateMosHandoffDto): NormalizedMosHandoffRequest { private normalizeRequest(request: CreateHandoffDto): NormalizedHandoffRequest {
if (typeof request !== 'object' || request === null) { if (typeof request !== 'object' || request === null) {
throw new MosCoordinationGatewayError('invalid_request', 'Mos handoff request is invalid'); throw new InteractionCoordinationGatewayError(
'invalid_request',
'Handoff request is invalid',
);
} }
const idempotencyKey = this.requiredString( const idempotencyKey = this.requiredString(
request.idempotencyKey, request.idempotencyKey,
@@ -203,14 +206,17 @@ export class MosCoordinationService {
private requiredString(value: unknown, field: string, maximumLength: number): string { private requiredString(value: unknown, field: string, maximumLength: number): string {
if (typeof value !== 'string') { if (typeof value !== 'string') {
throw new MosCoordinationGatewayError( throw new InteractionCoordinationGatewayError(
'invalid_request', 'invalid_request',
`Mos handoff ${field} must be a string`, `Handoff ${field} must be a string`,
); );
} }
const normalized = value.trim(); const normalized = value.trim();
if (normalized.length === 0 || normalized.length > maximumLength) { if (normalized.length === 0 || normalized.length > maximumLength) {
throw new MosCoordinationGatewayError('invalid_request', `Mos handoff ${field} is invalid`); throw new InteractionCoordinationGatewayError(
'invalid_request',
`Handoff ${field} is invalid`,
);
} }
return normalized; return normalized;
} }
@@ -245,23 +251,23 @@ export class MosCoordinationService {
private ownerFor(handoffId: string, scope: CoordinationScope): HandoffOwner { private ownerFor(handoffId: string, scope: CoordinationScope): HandoffOwner {
const owner = this.owners.get(handoffId); const owner = this.owners.get(handoffId);
if (owner === undefined) { if (owner === undefined) {
throw new MosCoordinationGatewayError('not_found', 'Mos handoff was not found'); throw new InteractionCoordinationGatewayError('not_found', 'Handoff was not found');
} }
if ( if (
owner.tenantId !== scope.tenantId || owner.tenantId !== scope.tenantId ||
owner.actorId !== scope.actorId || owner.actorId !== scope.actorId ||
owner.requesterAgentId !== scope.requesterAgentId owner.requesterAgentId !== scope.requesterAgentId
) { ) {
throw new MosCoordinationGatewayError( throw new InteractionCoordinationGatewayError(
'cross_tenant_forbidden', 'cross_tenant_forbidden',
'Mos handoff is outside the authenticated scope', 'Handoff is outside the authenticated scope',
); );
} }
return owner; return owner;
} }
} }
export type MosCoordinationGatewayErrorCode = export type InteractionCoordinationGatewayErrorCode =
| 'cross_tenant_forbidden' | 'cross_tenant_forbidden'
| 'handoff_conflict' | 'handoff_conflict'
| 'invalid_request' | 'invalid_request'
@@ -277,10 +283,7 @@ function sameOwner(left: HandoffOwner, right: HandoffOwner): boolean {
); );
} }
function sameRequest( function sameRequest(left: NormalizedHandoffRequest, right: NormalizedHandoffRequest): boolean {
left: NormalizedMosHandoffRequest,
right: NormalizedMosHandoffRequest,
): boolean {
return ( return (
left.idempotencyKey === right.idempotencyKey && left.idempotencyKey === right.idempotencyKey &&
left.summary === right.summary && left.summary === right.summary &&
@@ -289,12 +292,12 @@ function sameRequest(
); );
} }
export class MosCoordinationGatewayError extends Error { export class InteractionCoordinationGatewayError extends Error {
constructor( constructor(
readonly code: MosCoordinationGatewayErrorCode, readonly code: InteractionCoordinationGatewayErrorCode,
message: string, message: string,
) { ) {
super(message); super(message);
this.name = MosCoordinationGatewayError.name; this.name = InteractionCoordinationGatewayError.name;
} }
} }

View File

@@ -0,0 +1,43 @@
# #747 — De-hardcode orchestrator and interaction agent names
## Objective
Replace branded Mos/Tess symbols, filenames, DI tokens, and error prose with role-neutral orchestrator/interaction vocabulary without changing env-driven runtime identity behavior. Add optional roster `alias` and `provider` fields and show aliases in `mosaic fleet ps` with name fallback.
## Scope and constraints
- Requirements: `/home/hermes/agent-work/reviews/747-wsa-dehardcode-brief.md`.
- Branch: `feat/747-dehardcode-orchestrator-interaction-names` from `main` at `e72388b2`.
- Keep `MOSAIC_AGENT_NAME` and `MOSAIC_ORCHESTRATOR_AGENT_NAME` unchanged.
- Sample/test data may retain operator display names.
- No behavior change beyond optional roster metadata and alias display.
- Budget: no explicit cap; conservative mechanical-rename scope only.
- TDD: optional and skipped because this is a mechanical rename with existing focused coverage; add focused alias/schema regression coverage before completion.
## Plan
1. Rename coordination and durable-session files and symbols using canonical vocabulary.
2. Scrub branded symbol names and error prose in the assigned source trees while preserving allowed sample data.
3. Extend roster schema with optional `alias` and `provider`; update fleet roster typing/rendering and focused tests.
4. Run grep-clean verification, build, typecheck, lint/format, focused coord/durable-session/fleet tests, and roster validation.
5. Commit, queue-guard, push, open a Gitea PR closing #747, and report to the coordinator.
## Progress
- 2026-07-13: Task resumed from coordinator brief; repository clean at `e72388b2`.
- Renamed coordination and durable-session files, exports, gateway DI symbols, DTOs, services, repositories, and tests.
- Replaced branded authority/error prose while preserving the existing `/api/coord/mos` compatibility route and env-variable identity inputs.
- Added optional roster `alias`/`provider` support and alias-first `fleet ps` display with canonical-name fallback.
## Verification
- `pnpm typecheck`: passed (42 tasks).
- `pnpm build`: passed (23 tasks).
- `pnpm lint`: passed (23 tasks).
- `pnpm format:check`: passed.
- Coordination tests: 7 passed.
- Agent durable-session/runtime tests: 23 passed.
- Gateway coordination/durable-session/integration tests: 20 passed.
- Full `fleet.spec.ts`: 192 passed, including alias/provider parsing and alias display.
- JSON Schema 2020 validation: legacy minimal roster and extended alias/provider roster passed; `alias` and `provider` remain absent from `required`.
- Grep verification: no branded symbol/type/file/DI names or error prose remain in assigned source trees; one allowed `Tess Owner` test-data display name remains.

View File

@@ -3,7 +3,7 @@ import {
DurableSessionCoordinator, DurableSessionCoordinator,
InMemoryDurableSessionStore, InMemoryDurableSessionStore,
type DurableSessionIdentity, type DurableSessionIdentity,
} from './tess-durable-session.js'; } from './durable-session.js';
const IDENTITY: DurableSessionIdentity = { const IDENTITY: DurableSessionIdentity = {
agentName: 'Nova', agentName: 'Nova',

View File

@@ -102,7 +102,7 @@ export interface DurableSessionStore {
export class DurableSessionNotFoundError extends Error { export class DurableSessionNotFoundError extends Error {
constructor(sessionId: string) { constructor(sessionId: string) {
super(`Durable Tess session not found: ${sessionId}`); super(`Durable session not found: ${sessionId}`);
this.name = 'DurableSessionNotFoundError'; this.name = 'DurableSessionNotFoundError';
} }
} }
@@ -212,11 +212,11 @@ export class DurableSessionCoordinator {
async resumeHandoff(handoffId: string): Promise<DurableHandoffRecovery> { async resumeHandoff(handoffId: string): Promise<DurableHandoffRecovery> {
const handoff = await this.store.findHandoff(handoffId); const handoff = await this.store.findHandoff(handoffId);
if (!handoff) throw new Error(`Durable Tess handoff not found: ${handoffId}`); if (!handoff) throw new Error(`Durable handoff not found: ${handoffId}`);
const snapshot = await this.snapshot(handoff.sessionId); const snapshot = await this.snapshot(handoff.sessionId);
const checkpoint = await this.store.findCheckpoint(handoff.sessionId, handoff.checkpointId); const checkpoint = await this.store.findCheckpoint(handoff.sessionId, handoff.checkpointId);
if (!checkpoint) { if (!checkpoint) {
throw new Error(`Durable Tess handoff checkpoint is unavailable: ${handoff.checkpointId}`); throw new Error(`Durable handoff checkpoint is unavailable: ${handoff.checkpointId}`);
} }
return { identity: snapshot.identity, checkpoint, handoff }; return { identity: snapshot.identity, checkpoint, handoff };
} }
@@ -257,7 +257,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore {
const existing = this.sessions.get(identity.sessionId); const existing = this.sessions.get(identity.sessionId);
if (existing) { if (existing) {
if (!sameEnrollmentScope(existing.identity, identity)) { if (!sameEnrollmentScope(existing.identity, identity)) {
throw new Error(`Durable Tess session identity conflict: ${identity.sessionId}`); throw new Error(`Durable session identity conflict: ${identity.sessionId}`);
} }
existing.identity.providerId = identity.providerId; existing.identity.providerId = identity.providerId;
existing.identity.runtimeSessionId = identity.runtimeSessionId; existing.identity.runtimeSessionId = identity.runtimeSessionId;
@@ -290,7 +290,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore {
const existing = state.inbox.get(input.idempotencyKey); const existing = state.inbox.get(input.idempotencyKey);
if (existing) { if (existing) {
if (!sameInbox(existing, input)) { if (!sameInbox(existing, input)) {
throw new Error(`Durable Tess inbox idempotency conflict: ${input.idempotencyKey}`); throw new Error(`Durable inbox idempotency conflict: ${input.idempotencyKey}`);
} }
return { accepted: false, status: existing.status }; return { accepted: false, status: existing.status };
} }
@@ -323,7 +323,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore {
const existing = state.outbox.get(input.idempotencyKey); const existing = state.outbox.get(input.idempotencyKey);
if (existing) { if (existing) {
if (!sameOutbox(existing, input)) { if (!sameOutbox(existing, input)) {
throw new Error(`Durable Tess outbox idempotency conflict: ${input.idempotencyKey}`); throw new Error(`Durable outbox idempotency conflict: ${input.idempotencyKey}`);
} }
return { accepted: false, status: existing.status }; return { accepted: false, status: existing.status };
} }
@@ -364,7 +364,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore {
const checkpoints = this.require(input.sessionId).checkpoints; const checkpoints = this.require(input.sessionId).checkpoints;
const existing = checkpoints.get(input.checkpointId); const existing = checkpoints.get(input.checkpointId);
if (existing && !sameCheckpoint(existing, input)) { if (existing && !sameCheckpoint(existing, input)) {
throw new Error(`Durable Tess checkpoint identity conflict: ${input.checkpointId}`); throw new Error(`Durable checkpoint identity conflict: ${input.checkpointId}`);
} }
if (!existing) checkpoints.set(input.checkpointId, { ...input }); if (!existing) checkpoints.set(input.checkpointId, { ...input });
} }
@@ -377,11 +377,11 @@ export class InMemoryDurableSessionStore implements DurableSessionStore {
async handoff(input: DurableHandoffInput): Promise<void> { async handoff(input: DurableHandoffInput): Promise<void> {
const state = this.require(input.sessionId); const state = this.require(input.sessionId);
if (!state.checkpoints.has(input.checkpointId)) { if (!state.checkpoints.has(input.checkpointId)) {
throw new Error(`Durable Tess handoff checkpoint is unavailable: ${input.checkpointId}`); throw new Error(`Durable handoff checkpoint is unavailable: ${input.checkpointId}`);
} }
const existing = state.handoffs.get(input.handoffId); const existing = state.handoffs.get(input.handoffId);
if (existing && !sameHandoff(existing, input)) { if (existing && !sameHandoff(existing, input)) {
throw new Error(`Durable Tess handoff identity conflict: ${input.handoffId}`); throw new Error(`Durable handoff identity conflict: ${input.handoffId}`);
} }
if (!existing) state.handoffs.set(input.handoffId, { ...input }); if (!existing) state.handoffs.set(input.handoffId, { ...input });
} }
@@ -413,7 +413,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore {
kind: string, kind: string,
): T { ): T {
const entry = records.get(idempotencyKey); const entry = records.get(idempotencyKey);
if (!entry) throw new Error(`Durable Tess ${kind} entry not found: ${idempotencyKey}`); if (!entry) throw new Error(`Durable ${kind} entry not found: ${idempotencyKey}`);
return entry; return entry;
} }
} }

View File

@@ -4,4 +4,4 @@ export * from './runtime-provider-registry.js';
export * from './tmux-fleet-runtime-provider.js'; export * from './tmux-fleet-runtime-provider.js';
export * from './hermes-runtime-provider.js'; export * from './hermes-runtime-provider.js';
export * from './matrix-native-runtime-provider.js'; export * from './matrix-native-runtime-provider.js';
export * from './tess-durable-session.js'; export * from './durable-session.js';

View File

@@ -117,7 +117,7 @@ class DenyMatrixWriteAuthority implements MatrixWriteAuthority {
async assertAuthorized(): Promise<void> { async assertAuthorized(): Promise<void> {
throw new MatrixRuntimeProviderError( throw new MatrixRuntimeProviderError(
'forbidden', 'forbidden',
'Matrix runtime writes require Mos authority', 'Matrix runtime writes require orchestrator authority',
); );
} }
} }

View File

@@ -202,7 +202,7 @@ describe('TmuxFleetRuntimeProvider security policy', (): void => {
expect(fleet.terminate).not.toHaveBeenCalled(); expect(fleet.terminate).not.toHaveBeenCalled();
}); });
it('rejects an unverified target before consulting Mos write authority', async (): Promise<void> => { it('rejects an unverified target before consulting orchestrator write authority', async (): Promise<void> => {
const fleet = transport(); const fleet = transport();
fleet.verifySession = vi.fn(async (): Promise<FleetRuntimeTarget> => { fleet.verifySession = vi.fn(async (): Promise<FleetRuntimeTarget> => {
throw new Error('target identity mismatch'); throw new Error('target identity mismatch');
@@ -220,7 +220,20 @@ describe('TmuxFleetRuntimeProvider security policy', (): void => {
expect(fleet.sendMessage).not.toHaveBeenCalled(); expect(fleet.sendMessage).not.toHaveBeenCalled();
}); });
it('passes an exact session ID to the fleet transport only through authorized Mos writes', async (): Promise<void> => { it('uses the role-neutral interaction source label by default', async (): Promise<void> => {
const fleet = transport();
const writeAuthority: FleetWriteAuthority = {
canWrite: vi.fn(async (): Promise<boolean> => true),
assertAuthorized: vi.fn(async (): Promise<void> => undefined),
};
const provider = new TmuxFleetRuntimeProvider({ transport: fleet, writeAuthority });
await provider.sendMessage('coder0', { content: 'hello', idempotencyKey: 'message-1' }, scope);
expect(fleet.sendMessage).toHaveBeenCalledWith('coder0', 'hello', 'interaction');
});
it('passes an exact session ID to the fleet transport only through orchestrator-authorized writes', async (): Promise<void> => {
const fleet = transport(); const fleet = transport();
const writeAuthority: FleetWriteAuthority = { const writeAuthority: FleetWriteAuthority = {
canWrite: vi.fn(async (): Promise<boolean> => true), canWrite: vi.fn(async (): Promise<boolean> => true),
@@ -228,7 +241,7 @@ describe('TmuxFleetRuntimeProvider security policy', (): void => {
}; };
const provider = new TmuxFleetRuntimeProvider({ const provider = new TmuxFleetRuntimeProvider({
transport: fleet, transport: fleet,
sourceLabel: 'tess', sourceLabel: 'operator',
writeAuthority, writeAuthority,
}); });
@@ -246,7 +259,7 @@ describe('TmuxFleetRuntimeProvider security policy', (): void => {
scope, scope,
approvalRef: 'approval-1', approvalRef: 'approval-1',
}); });
expect(fleet.sendMessage).toHaveBeenCalledWith('coder0', 'hello', 'tess'); expect(fleet.sendMessage).toHaveBeenCalledWith('coder0', 'hello', 'operator');
expect(fleet.terminate).toHaveBeenCalledWith('coder0'); expect(fleet.terminate).toHaveBeenCalledWith('coder0');
}); });

View File

@@ -64,14 +64,14 @@ export interface FleetWriteAuthorization {
} }
/** /**
* Mos is the only authority that may permit Tess write/control requests to a * The orchestrator is the only authority that may permit interaction-plane write/control requests to a
* fleet peer. Gateway records the request and denial/success around provider * fleet peer. Gateway records the request and denial/success around provider
* invocation; the default authority prevents direct Tess writes by design. * invocation; the default authority prevents direct interaction-plane writes by design.
*/ */
export interface FleetWriteAuthority { export interface FleetWriteAuthority {
/** Non-consuming preflight used before probing the fleet transport. */ /** Non-consuming preflight used before probing the fleet transport. */
canWrite(authorization: FleetWriteAuthorization): Promise<boolean>; canWrite(authorization: FleetWriteAuthorization): Promise<boolean>;
/** Final exact-target authorization; may consume a Mos grant. */ /** Final exact-target authorization; may consume an orchestrator grant. */
assertAuthorized(authorization: FleetWriteAuthorization): Promise<void>; assertAuthorized(authorization: FleetWriteAuthorization): Promise<void>;
} }
@@ -116,7 +116,7 @@ class DenyFleetWriteAuthority implements FleetWriteAuthority {
async assertAuthorized(_authorization: FleetWriteAuthorization): Promise<void> { async assertAuthorized(_authorization: FleetWriteAuthorization): Promise<void> {
throw new FleetRuntimeProviderError( throw new FleetRuntimeProviderError(
'forbidden', 'forbidden',
'Fleet writes require an explicit Mos authority decision', 'Fleet writes require an explicit orchestrator authority decision',
); );
} }
} }
@@ -124,7 +124,7 @@ class DenyFleetWriteAuthority implements FleetWriteAuthority {
/** /**
* A capability-limited provider for rostered local fleet peers. It never * A capability-limited provider for rostered local fleet peers. It never
* permits raw tmux socket/target selection, interactive control attach, or * permits raw tmux socket/target selection, interactive control attach, or
* direct Tess writes; all side effects pass through exact transport checks. * direct interaction-plane writes; all side effects pass through exact transport checks.
*/ */
export class TmuxFleetRuntimeProvider implements AgentRuntimeProvider { export class TmuxFleetRuntimeProvider implements AgentRuntimeProvider {
readonly id = FLEET_PROVIDER_ID; readonly id = FLEET_PROVIDER_ID;
@@ -139,7 +139,7 @@ export class TmuxFleetRuntimeProvider implements AgentRuntimeProvider {
constructor(private readonly options: TmuxFleetRuntimeProviderOptions) { constructor(private readonly options: TmuxFleetRuntimeProviderOptions) {
this.readAuthority = options.readAuthority ?? new DenyFleetReadAuthority(); this.readAuthority = options.readAuthority ?? new DenyFleetReadAuthority();
this.writeAuthority = options.writeAuthority ?? new DenyFleetWriteAuthority(); this.writeAuthority = options.writeAuthority ?? new DenyFleetWriteAuthority();
this.sourceLabel = options.sourceLabel ?? 'tess'; this.sourceLabel = options.sourceLabel ?? 'interaction';
this.attachmentIdFactory = options.attachmentIdFactory ?? randomUUID; this.attachmentIdFactory = options.attachmentIdFactory ?? randomUUID;
this.now = options.now ?? (() => new Date()); this.now = options.now ?? (() => new Date());
this.attachmentTtlMs = options.attachmentTtlMs ?? ATTACHMENT_TTL_MS; this.attachmentTtlMs = options.attachmentTtlMs ?? ATTACHMENT_TTL_MS;

View File

@@ -1,10 +1,10 @@
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { import {
InMemoryMosCoordinationPort, InMemoryInteractionCoordinationPort,
MosCoordinationClient, InteractionCoordinationClient,
type CoordinationScope, type CoordinationScope,
type MosCoordinationAuthorityError, type InteractionCoordinationAuthorityError,
type MosCoordinationPort, type InteractionCoordinationPort,
} from '../index.js'; } from '../index.js';
const scope: CoordinationScope = { const scope: CoordinationScope = {
@@ -15,19 +15,19 @@ const scope: CoordinationScope = {
}; };
function client( function client(
port: MosCoordinationPort, port: InteractionCoordinationPort,
handoffIdFactory: () => string = (): string => 'handoff-1', handoffIdFactory: () => string = (): string => 'handoff-1',
): MosCoordinationClient { ): InteractionCoordinationClient {
return new MosCoordinationClient( return new InteractionCoordinationClient(
{ interactionAgentId: 'Nova', orchestrationAgentId: 'Conductor' }, { interactionAgentId: 'Nova', orchestrationAgentId: 'Conductor' },
port, port,
handoffIdFactory, handoffIdFactory,
); );
} }
describe('MosCoordinationClient', (): void => { describe('InteractionCoordinationClient', (): void => {
it('round-trips handoff, observation, and result through the native port with identities as data', async (): Promise<void> => { it('round-trips handoff, observation, and result through the native port with identities as data', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort(); const adapter = new InMemoryInteractionCoordinationPort();
const coordination = client(adapter); const coordination = client(adapter);
await expect( await expect(
@@ -42,8 +42,8 @@ describe('MosCoordinationClient', (): void => {
correlationId: 'corr-1', correlationId: 'corr-1',
}); });
adapter.recordActivity('handoff-1', 'running', 'Mos accepted the request'); adapter.recordActivity('handoff-1', 'running', 'Orchestrator accepted the request');
adapter.recordResult('handoff-1', 'completed', 'Merged by Mos'); adapter.recordResult('handoff-1', 'completed', 'Merged by orchestrator');
await expect(coordination.observe('handoff-1', scope)).resolves.toMatchObject({ await expect(coordination.observe('handoff-1', scope)).resolves.toMatchObject({
status: 'completed', status: 'completed',
@@ -59,7 +59,7 @@ describe('MosCoordinationClient', (): void => {
targetAgentId: 'Conductor', targetAgentId: 'Conductor',
status: 'completed', status: 'completed',
correlationId: 'corr-1', correlationId: 'corr-1',
summary: 'Merged by Mos', summary: 'Merged by orchestrator',
}); });
expect(coordination).not.toHaveProperty('dispatch'); expect(coordination).not.toHaveProperty('dispatch');
@@ -69,8 +69,8 @@ describe('MosCoordinationClient', (): void => {
expect(coordination).not.toHaveProperty('cancel'); expect(coordination).not.toHaveProperty('cancel');
}); });
it('fails closed before delivery when an unconfigured agent requests Mos work', async (): Promise<void> => { it('fails closed before delivery when an unconfigured agent requests orchestrator work', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort(); const adapter = new InMemoryInteractionCoordinationPort();
await expect( await expect(
client(adapter).handoff( client(adapter).handoff(
@@ -79,31 +79,31 @@ describe('MosCoordinationClient', (): void => {
), ),
).rejects.toMatchObject({ ).rejects.toMatchObject({
code: 'requester_forbidden', code: 'requester_forbidden',
} satisfies Partial<MosCoordinationAuthorityError>); } satisfies Partial<InteractionCoordinationAuthorityError>);
}); });
it('rejects self-delegation configuration before constructing a client', (): void => { it('rejects self-delegation configuration before constructing a client', (): void => {
expect( expect(
(): MosCoordinationClient => (): InteractionCoordinationClient =>
new MosCoordinationClient( new InteractionCoordinationClient(
{ interactionAgentId: 'Nova', orchestrationAgentId: 'Nova' }, { interactionAgentId: 'Nova', orchestrationAgentId: 'Nova' },
new InMemoryMosCoordinationPort(), new InMemoryInteractionCoordinationPort(),
), ),
).toThrow('Interaction and orchestration identities must differ'); ).toThrow('Interaction and orchestration identities must differ');
}); });
it('rejects whitespace-equivalent self-delegation identities', (): void => { it('rejects whitespace-equivalent self-delegation identities', (): void => {
expect( expect(
(): MosCoordinationClient => (): InteractionCoordinationClient =>
new MosCoordinationClient( new InteractionCoordinationClient(
{ interactionAgentId: 'Nova ', orchestrationAgentId: 'Nova' }, { interactionAgentId: 'Nova ', orchestrationAgentId: 'Nova' },
new InMemoryMosCoordinationPort(), new InMemoryInteractionCoordinationPort(),
), ),
).toThrow('Interaction and orchestration identities must differ'); ).toThrow('Interaction and orchestration identities must differ');
}); });
it('does not expose another tenant handoff to observe or result', async (): Promise<void> => { it('does not expose another tenant handoff to observe or result', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort(); const adapter = new InMemoryInteractionCoordinationPort();
const coordination = client(adapter); const coordination = client(adapter);
await coordination.handoff( await coordination.handoff(
{ idempotencyKey: 'handoff-request-1', summary: 'Implement the requested feature' }, { idempotencyKey: 'handoff-request-1', summary: 'Implement the requested feature' },
@@ -120,7 +120,7 @@ describe('MosCoordinationClient', (): void => {
}); });
it('bounds native handoff retention by evicting the oldest handoff', async (): Promise<void> => { it('bounds native handoff retention by evicting the oldest handoff', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort({ maxHandoffs: 1 }); const adapter = new InMemoryInteractionCoordinationPort({ maxHandoffs: 1 });
const first = client(adapter, (): string => 'handoff-1'); const first = client(adapter, (): string => 'handoff-1');
const second = client(adapter, (): string => 'handoff-2'); const second = client(adapter, (): string => 'handoff-2');
await first.handoff({ idempotencyKey: 'handoff-request-1', summary: 'First request' }, scope); await first.handoff({ idempotencyKey: 'handoff-request-1', summary: 'First request' }, scope);
@@ -131,7 +131,7 @@ describe('MosCoordinationClient', (): void => {
}); });
it('fails closed when a transport reports target drift', async (): Promise<void> => { it('fails closed when a transport reports target drift', async (): Promise<void> => {
const adapter: MosCoordinationPort = { const adapter: InteractionCoordinationPort = {
handoff: vi.fn(async () => ({ handoff: vi.fn(async () => ({
handoffId: 'handoff-1', handoffId: 'handoff-1',
targetAgentId: 'Unexpected', targetAgentId: 'Unexpected',
@@ -149,6 +149,6 @@ describe('MosCoordinationClient', (): void => {
), ),
).rejects.toMatchObject({ ).rejects.toMatchObject({
code: 'target_drift', code: 'target_drift',
} satisfies Partial<MosCoordinationAuthorityError>); } satisfies Partial<InteractionCoordinationAuthorityError>);
}); });
}); });

View File

@@ -2,25 +2,25 @@ import {
type CoordinationObservation, type CoordinationObservation,
type CoordinationResult, type CoordinationResult,
type CoordinationScope, type CoordinationScope,
type MosCoordinationActivity, type InteractionCoordinationActivity,
type MosCoordinationPort, type InteractionCoordinationPort,
type MosHandoff, type Handoff,
type MosHandoffReceipt, type HandoffReceipt,
type MosHandoffStatus, type HandoffStatus,
} from './mos-coordination.js'; } from './interaction-coordination.js';
const DEFAULT_HANDOFF_TTL_MS = 60 * 60 * 1_000; const DEFAULT_HANDOFF_TTL_MS = 60 * 60 * 1_000;
const DEFAULT_MAX_HANDOFFS = 1_000; const DEFAULT_MAX_HANDOFFS = 1_000;
interface StoredHandoff { interface StoredHandoff {
readonly handoff: MosHandoff; readonly handoff: Handoff;
status: MosHandoffStatus; status: HandoffStatus;
readonly activity: MosCoordinationActivity[]; readonly activity: InteractionCoordinationActivity[];
readonly expiresAt: number; readonly expiresAt: number;
result?: CoordinationResult; result?: CoordinationResult;
} }
export interface InMemoryMosCoordinationPortOptions { export interface InMemoryInteractionCoordinationPortOptions {
now?: () => Date; now?: () => Date;
handoffTtlMs?: number; handoffTtlMs?: number;
maxHandoffs?: number; maxHandoffs?: number;
@@ -29,21 +29,21 @@ export interface InMemoryMosCoordinationPortOptions {
/** /**
* Native deterministic queue/port adapter for the coordination boundary. * Native deterministic queue/port adapter for the coordination boundary.
* It intentionally has no fleet/tmux dependency. A future deployment adapter * It intentionally has no fleet/tmux dependency. A future deployment adapter
* implements MosCoordinationPort without changing interaction-plane callers. * implements InteractionCoordinationPort without changing interaction-plane callers.
*/ */
export class InMemoryMosCoordinationPort implements MosCoordinationPort { export class InMemoryInteractionCoordinationPort implements InteractionCoordinationPort {
private readonly handoffs = new Map<string, StoredHandoff>(); private readonly handoffs = new Map<string, StoredHandoff>();
private readonly now: () => Date; private readonly now: () => Date;
private readonly handoffTtlMs: number; private readonly handoffTtlMs: number;
private readonly maxHandoffs: number; private readonly maxHandoffs: number;
constructor(options: InMemoryMosCoordinationPortOptions = {}) { constructor(options: InMemoryInteractionCoordinationPortOptions = {}) {
this.now = options.now ?? (() => new Date()); this.now = options.now ?? (() => new Date());
this.handoffTtlMs = options.handoffTtlMs ?? DEFAULT_HANDOFF_TTL_MS; this.handoffTtlMs = options.handoffTtlMs ?? DEFAULT_HANDOFF_TTL_MS;
this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS; this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS;
} }
async handoff(handoff: MosHandoff): Promise<MosHandoffReceipt> { async handoff(handoff: Handoff): Promise<HandoffReceipt> {
this.pruneExpiredHandoffs(); this.pruneExpiredHandoffs();
const existing = this.handoffs.get(handoff.handoffId); const existing = this.handoffs.get(handoff.handoffId);
if (existing !== undefined) { if (existing !== undefined) {
@@ -88,14 +88,14 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
} }
/** Host-side progression seam; interaction clients never receive this capability. */ /** Host-side progression seam; interaction clients never receive this capability. */
recordActivity(handoffId: string, status: MosHandoffStatus, summary: string): void { recordActivity(handoffId: string, status: HandoffStatus, summary: string): void {
this.pruneExpiredHandoffs(); this.pruneExpiredHandoffs();
const stored = this.requireHandoff(handoffId); const stored = this.requireHandoff(handoffId);
stored.status = status; stored.status = status;
stored.activity.push(activity(status, summary, this.now)); stored.activity.push(activity(status, summary, this.now));
} }
/** Host-side result seam for deterministic qualification; not a Mos consumer. */ /** Host-side result seam for deterministic qualification; not an orchestrator consumer. */
recordResult(handoffId: string, status: 'completed' | 'failed', summary: string): void { recordResult(handoffId: string, status: 'completed' | 'failed', summary: string): void {
this.pruneExpiredHandoffs(); this.pruneExpiredHandoffs();
const stored = this.requireHandoff(handoffId); const stored = this.requireHandoff(handoffId);
@@ -110,7 +110,7 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
}; };
} }
private receipt(handoff: MosHandoff, status: MosHandoffStatus): MosHandoffReceipt { private receipt(handoff: Handoff, status: HandoffStatus): HandoffReceipt {
return { return {
handoffId: handoff.handoffId, handoffId: handoff.handoffId,
targetAgentId: handoff.targetAgentId, targetAgentId: handoff.targetAgentId,
@@ -141,7 +141,7 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
stored.handoff.scope.actorId !== scope.actorId || stored.handoff.scope.actorId !== scope.actorId ||
stored.handoff.scope.requesterAgentId !== scope.requesterAgentId stored.handoff.scope.requesterAgentId !== scope.requesterAgentId
) { ) {
throw new InMemoryMosCoordinationError('forbidden', 'Handoff scope does not match'); throw new InMemoryInteractionCoordinationError('forbidden', 'Handoff scope does not match');
} }
return stored; return stored;
} }
@@ -149,12 +149,12 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
private requireHandoff(handoffId: string): StoredHandoff { private requireHandoff(handoffId: string): StoredHandoff {
const stored = this.handoffs.get(handoffId); const stored = this.handoffs.get(handoffId);
if (stored === undefined) { if (stored === undefined) {
throw new InMemoryMosCoordinationError('not_found', 'Handoff was not found'); throw new InMemoryInteractionCoordinationError('not_found', 'Handoff was not found');
} }
return stored; return stored;
} }
private assertSameHandoff(existing: MosHandoff, incoming: MosHandoff): void { private assertSameHandoff(existing: Handoff, incoming: Handoff): void {
if ( if (
existing.targetAgentId !== incoming.targetAgentId || existing.targetAgentId !== incoming.targetAgentId ||
existing.request.idempotencyKey !== incoming.request.idempotencyKey || existing.request.idempotencyKey !== incoming.request.idempotencyKey ||
@@ -166,7 +166,7 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
existing.scope.correlationId !== incoming.scope.correlationId || existing.scope.correlationId !== incoming.scope.correlationId ||
existing.scope.requesterAgentId !== incoming.scope.requesterAgentId existing.scope.requesterAgentId !== incoming.scope.requesterAgentId
) { ) {
throw new InMemoryMosCoordinationError( throw new InMemoryInteractionCoordinationError(
'conflict', 'conflict',
'Handoff ID is already bound to different immutable input', 'Handoff ID is already bound to different immutable input',
); );
@@ -174,31 +174,31 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
} }
} }
export type InMemoryMosCoordinationErrorCode = 'conflict' | 'forbidden' | 'not_found'; export type InMemoryInteractionCoordinationErrorCode = 'conflict' | 'forbidden' | 'not_found';
export class InMemoryMosCoordinationError extends Error { export class InMemoryInteractionCoordinationError extends Error {
constructor( constructor(
readonly code: InMemoryMosCoordinationErrorCode, readonly code: InMemoryInteractionCoordinationErrorCode,
message: string, message: string,
) { ) {
super(message); super(message);
this.name = InMemoryMosCoordinationError.name; this.name = InMemoryInteractionCoordinationError.name;
} }
} }
function activity( function activity(
status: MosHandoffStatus, status: HandoffStatus,
summary: string, summary: string,
now: () => Date, now: () => Date,
): MosCoordinationActivity { ): InteractionCoordinationActivity {
return { occurredAt: now().toISOString(), status, summary }; return { occurredAt: now().toISOString(), status, summary };
} }
function copyActivity(entry: MosCoordinationActivity): MosCoordinationActivity { function copyActivity(entry: InteractionCoordinationActivity): InteractionCoordinationActivity {
return { ...entry }; return { ...entry };
} }
function snapshotHandoff(handoff: MosHandoff): MosHandoff { function snapshotHandoff(handoff: Handoff): Handoff {
return Object.freeze({ return Object.freeze({
handoffId: handoff.handoffId, handoffId: handoff.handoffId,
targetAgentId: handoff.targetAgentId, targetAgentId: handoff.targetAgentId,

View File

@@ -3,22 +3,25 @@ export { parseTasksFile, updateTaskStatus, writeTasksFile } from './tasks-file.j
export { runTask, resumeTask } from './runner.js'; export { runTask, resumeTask } from './runner.js';
export { getMissionStatus, getTaskStatus } from './status.js'; export { getMissionStatus, getTaskStatus } from './status.js';
export { export {
InMemoryMosCoordinationError, InMemoryInteractionCoordinationError,
InMemoryMosCoordinationPort, InMemoryInteractionCoordinationPort,
} from './in-memory-mos-coordination-port.js'; } from './in-memory-interaction-coordination-port.js';
export { MosCoordinationAuthorityError, MosCoordinationClient } from './mos-coordination.js'; export {
InteractionCoordinationAuthorityError,
InteractionCoordinationClient,
} from './interaction-coordination.js';
export type { export type {
CoordinationObservation, CoordinationObservation,
CoordinationResult, CoordinationResult,
CoordinationScope, CoordinationScope,
MosCoordinationActivity, InteractionCoordinationActivity,
MosCoordinationIdentity, InteractionCoordinationIdentity,
MosCoordinationPort, InteractionCoordinationPort,
MosHandoff, Handoff,
MosHandoffReceipt, HandoffReceipt,
MosHandoffRequest, HandoffRequest,
MosHandoffStatus, HandoffStatus,
} from './mos-coordination.js'; } from './interaction-coordination.js';
export type { export type {
CreateMissionOptions, CreateMissionOptions,
Mission, Mission,

View File

@@ -1,4 +1,4 @@
export type MosHandoffStatus = 'queued' | 'accepted' | 'running' | 'completed' | 'failed'; export type HandoffStatus = 'queued' | 'accepted' | 'running' | 'completed' | 'failed';
export interface CoordinationScope { export interface CoordinationScope {
readonly actorId: string; readonly actorId: string;
@@ -8,44 +8,44 @@ export interface CoordinationScope {
readonly requesterAgentId: string; readonly requesterAgentId: string;
} }
export interface MosCoordinationIdentity { export interface InteractionCoordinationIdentity {
readonly interactionAgentId: string; readonly interactionAgentId: string;
readonly orchestrationAgentId: string; readonly orchestrationAgentId: string;
} }
export interface MosHandoffRequest { export interface HandoffRequest {
readonly idempotencyKey: string; readonly idempotencyKey: string;
readonly summary: string; readonly summary: string;
readonly context?: string; readonly context?: string;
readonly missionId?: string; readonly missionId?: string;
} }
export interface MosHandoff { export interface Handoff {
readonly handoffId: string; readonly handoffId: string;
readonly targetAgentId: string; readonly targetAgentId: string;
readonly request: MosHandoffRequest; readonly request: HandoffRequest;
readonly scope: CoordinationScope; readonly scope: CoordinationScope;
} }
export interface MosHandoffReceipt { export interface HandoffReceipt {
readonly handoffId: string; readonly handoffId: string;
readonly targetAgentId: string; readonly targetAgentId: string;
readonly status: 'queued' | 'accepted'; readonly status: 'queued' | 'accepted';
readonly correlationId: string; readonly correlationId: string;
} }
export interface MosCoordinationActivity { export interface InteractionCoordinationActivity {
readonly occurredAt: string; readonly occurredAt: string;
readonly status: MosHandoffStatus; readonly status: HandoffStatus;
readonly summary: string; readonly summary: string;
} }
export interface CoordinationObservation { export interface CoordinationObservation {
readonly handoffId: string; readonly handoffId: string;
readonly targetAgentId: string; readonly targetAgentId: string;
readonly status: MosHandoffStatus; readonly status: HandoffStatus;
readonly correlationId: string; readonly correlationId: string;
readonly activity: readonly MosCoordinationActivity[]; readonly activity: readonly InteractionCoordinationActivity[];
} }
export interface CoordinationResult { export interface CoordinationResult {
@@ -61,25 +61,25 @@ export interface CoordinationResult {
* its progress/result, but it cannot issue worker, review, merge, or other * its progress/result, but it cannot issue worker, review, merge, or other
* general orchestration commands. * general orchestration commands.
*/ */
export interface MosCoordinationPort { export interface InteractionCoordinationPort {
handoff(handoff: MosHandoff): Promise<MosHandoffReceipt>; handoff(handoff: Handoff): Promise<HandoffReceipt>;
observe(handoffId: string, scope: CoordinationScope): Promise<CoordinationObservation>; observe(handoffId: string, scope: CoordinationScope): Promise<CoordinationObservation>;
result(handoffId: string, scope: CoordinationScope): Promise<CoordinationResult>; result(handoffId: string, scope: CoordinationScope): Promise<CoordinationResult>;
} }
export type MosCoordinationAuthorityErrorCode = export type InteractionCoordinationAuthorityErrorCode =
| 'invalid_identity' | 'invalid_identity'
| 'requester_forbidden' | 'requester_forbidden'
| 'target_drift' | 'target_drift'
| 'correlation_drift'; | 'correlation_drift';
export class MosCoordinationAuthorityError extends Error { export class InteractionCoordinationAuthorityError extends Error {
constructor( constructor(
readonly code: MosCoordinationAuthorityErrorCode, readonly code: InteractionCoordinationAuthorityErrorCode,
message: string, message: string,
) { ) {
super(message); super(message);
this.name = MosCoordinationAuthorityError.name; this.name = InteractionCoordinationAuthorityError.name;
} }
} }
@@ -87,20 +87,20 @@ export class MosCoordinationAuthorityError extends Error {
* Enforces the interaction-to-orchestration authority boundary before a * Enforces the interaction-to-orchestration authority boundary before a
* transport is reached. Identity names remain configuration data. * transport is reached. Identity names remain configuration data.
*/ */
export class MosCoordinationClient { export class InteractionCoordinationClient {
private readonly identity: MosCoordinationIdentity; private readonly identity: InteractionCoordinationIdentity;
constructor( constructor(
identity: MosCoordinationIdentity, identity: InteractionCoordinationIdentity,
private readonly port: MosCoordinationPort, private readonly port: InteractionCoordinationPort,
private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(), private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(),
) { ) {
this.identity = normalizeIdentity(identity); this.identity = normalizeIdentity(identity);
} }
async handoff(request: MosHandoffRequest, scope: CoordinationScope): Promise<MosHandoffReceipt> { async handoff(request: HandoffRequest, scope: CoordinationScope): Promise<HandoffReceipt> {
this.assertRequester(scope); this.assertRequester(scope);
const handoff: MosHandoff = { const handoff: Handoff = {
handoffId: this.handoffIdFactory(), handoffId: this.handoffIdFactory(),
targetAgentId: this.identity.orchestrationAgentId, targetAgentId: this.identity.orchestrationAgentId,
request: snapshotRequest(request), request: snapshotRequest(request),
@@ -111,15 +111,15 @@ export class MosCoordinationClient {
receipt.handoffId !== handoff.handoffId || receipt.handoffId !== handoff.handoffId ||
receipt.targetAgentId !== handoff.targetAgentId receipt.targetAgentId !== handoff.targetAgentId
) { ) {
throw new MosCoordinationAuthorityError( throw new InteractionCoordinationAuthorityError(
'target_drift', 'target_drift',
'Mos coordination transport returned a mismatched handoff target', 'Interaction coordination transport returned a mismatched handoff target',
); );
} }
if (receipt.correlationId !== handoff.scope.correlationId) { if (receipt.correlationId !== handoff.scope.correlationId) {
throw new MosCoordinationAuthorityError( throw new InteractionCoordinationAuthorityError(
'correlation_drift', 'correlation_drift',
'Mos coordination transport returned a mismatched correlation ID', 'Interaction coordination transport returned a mismatched correlation ID',
); );
} }
return receipt; return receipt;
@@ -137,7 +137,7 @@ export class MosCoordinationClient {
private assertRequester(scope: CoordinationScope): void { private assertRequester(scope: CoordinationScope): void {
if (scope.requesterAgentId !== this.identity.interactionAgentId) { if (scope.requesterAgentId !== this.identity.interactionAgentId) {
throw new MosCoordinationAuthorityError( throw new InteractionCoordinationAuthorityError(
'requester_forbidden', 'requester_forbidden',
'Requester is not the configured interaction agent', 'Requester is not the configured interaction agent',
); );
@@ -149,15 +149,15 @@ export class MosCoordinationClient {
scope: CoordinationScope, scope: CoordinationScope,
): CoordinationObservation { ): CoordinationObservation {
if (observation.targetAgentId !== this.identity.orchestrationAgentId) { if (observation.targetAgentId !== this.identity.orchestrationAgentId) {
throw new MosCoordinationAuthorityError( throw new InteractionCoordinationAuthorityError(
'target_drift', 'target_drift',
'Mos coordination transport returned an unexpected observation target', 'Interaction coordination transport returned an unexpected observation target',
); );
} }
if (observation.correlationId !== scope.correlationId) { if (observation.correlationId !== scope.correlationId) {
throw new MosCoordinationAuthorityError( throw new InteractionCoordinationAuthorityError(
'correlation_drift', 'correlation_drift',
'Mos coordination transport returned a mismatched observation correlation ID', 'Interaction coordination transport returned a mismatched observation correlation ID',
); );
} }
return observation; return observation;
@@ -165,32 +165,34 @@ export class MosCoordinationClient {
private assertResult(result: CoordinationResult, scope: CoordinationScope): CoordinationResult { private assertResult(result: CoordinationResult, scope: CoordinationScope): CoordinationResult {
if (result.targetAgentId !== this.identity.orchestrationAgentId) { if (result.targetAgentId !== this.identity.orchestrationAgentId) {
throw new MosCoordinationAuthorityError( throw new InteractionCoordinationAuthorityError(
'target_drift', 'target_drift',
'Mos coordination transport returned an unexpected result target', 'Interaction coordination transport returned an unexpected result target',
); );
} }
if (result.correlationId !== scope.correlationId) { if (result.correlationId !== scope.correlationId) {
throw new MosCoordinationAuthorityError( throw new InteractionCoordinationAuthorityError(
'correlation_drift', 'correlation_drift',
'Mos coordination transport returned a mismatched result correlation ID', 'Interaction coordination transport returned a mismatched result correlation ID',
); );
} }
return result; return result;
} }
} }
function normalizeIdentity(identity: MosCoordinationIdentity): MosCoordinationIdentity { function normalizeIdentity(
identity: InteractionCoordinationIdentity,
): InteractionCoordinationIdentity {
const interactionAgentId = identity.interactionAgentId.trim(); const interactionAgentId = identity.interactionAgentId.trim();
const orchestrationAgentId = identity.orchestrationAgentId.trim(); const orchestrationAgentId = identity.orchestrationAgentId.trim();
if (interactionAgentId.length === 0 || orchestrationAgentId.length === 0) { if (interactionAgentId.length === 0 || orchestrationAgentId.length === 0) {
throw new MosCoordinationAuthorityError( throw new InteractionCoordinationAuthorityError(
'invalid_identity', 'invalid_identity',
'Interaction and orchestration identities are required', 'Interaction and orchestration identities are required',
); );
} }
if (interactionAgentId === orchestrationAgentId) { if (interactionAgentId === orchestrationAgentId) {
throw new MosCoordinationAuthorityError( throw new InteractionCoordinationAuthorityError(
'invalid_identity', 'invalid_identity',
'Interaction and orchestration identities must differ', 'Interaction and orchestration identities must differ',
); );
@@ -198,7 +200,7 @@ function normalizeIdentity(identity: MosCoordinationIdentity): MosCoordinationId
return Object.freeze({ interactionAgentId, orchestrationAgentId }); return Object.freeze({ interactionAgentId, orchestrationAgentId });
} }
function snapshotRequest(request: MosHandoffRequest): MosHandoffRequest { function snapshotRequest(request: HandoffRequest): HandoffRequest {
return Object.freeze({ ...request }); return Object.freeze({ ...request });
} }

View File

@@ -2,10 +2,10 @@
The **operator-interaction** role is the authorized human interaction plane for The **operator-interaction** role is the authorized human interaction plane for
Mosaic. It presents runtime and fleet state, mediates approved actions, and Mosaic. It presents runtime and fleet state, mediates approved actions, and
hands coding or general orchestration work to Mos. hands coding or general orchestration work to the orchestrator.
## Boundaries ## Boundaries
- It does not claim Mos-owned coding or general orchestration work. - It does not claim orchestrator-owned coding or general orchestration work.
- It exposes only the configured, observable tool policy. - It exposes only the configured, observable tool policy.
- It does not receive or surface credentials in its effective policy. - It does not receive or surface credentials in its effective policy.

View File

@@ -75,6 +75,14 @@
"type": "string", "type": "string",
"pattern": "^[A-Za-z0-9_.-]+$" "pattern": "^[A-Za-z0-9_.-]+$"
}, },
"alias": {
"description": "Optional operator-defined display name for the agent.",
"type": "string"
},
"provider": {
"description": "Optional agent runtime provider identifier such as openai-codex.",
"type": "string"
},
"runtime": { "runtime": {
"type": "string" "type": "string"
}, },

View File

@@ -189,6 +189,36 @@ describe('fleet roster parsing', () => {
expect(getRosterAgent(roster, 'canary-pi').runtime).toBe('pi'); expect(getRosterAgent(roster, 'canary-pi').runtime).toBe('pi');
}); });
it('accepts optional agent alias and provider metadata without requiring them', async () => {
cleanup = await tempDir();
const rosterPath = join(cleanup, 'roster.yaml');
await writeFile(
rosterPath,
[
'version: 1',
'transport: tmux',
'agents:',
' - name: interaction',
' alias: Friday',
' provider: openai-codex',
' runtime: pi',
' - name: worker0',
' runtime: codex',
].join('\n'),
);
const roster = await loadFleetRoster(rosterPath);
expect(getRosterAgent(roster, 'interaction')).toMatchObject({
name: 'interaction',
alias: 'Friday',
provider: 'openai-codex',
});
expect(getRosterAgent(roster, 'worker0')).toMatchObject({ name: 'worker0' });
expect(getRosterAgent(roster, 'worker0').alias).toBeUndefined();
expect(getRosterAgent(roster, 'worker0').provider).toBeUndefined();
});
it('socketArgs: named socket → -L <name>; empty → no -L (default socket)', () => { it('socketArgs: named socket → -L <name>; empty → no -L (default socket)', () => {
expect(socketArgs('mosaic-fleet')).toEqual(['-L', 'mosaic-fleet']); expect(socketArgs('mosaic-fleet')).toEqual(['-L', 'mosaic-fleet']);
expect(socketArgs('')).toEqual([]); expect(socketArgs('')).toEqual([]);
@@ -1872,7 +1902,61 @@ describe('fleet ps — tenant and host', () => {
}); });
}); });
describe('fleet ps — JSON output shape (FR-6)', () => { describe('fleet ps — aliases and JSON output shape (FR-6)', () => {
it('shows an agent alias in table output and falls back to name when absent', async () => {
const home = await mkdtemp(join(tmpdir(), 'mosaic-fleet-'));
const rosterPath = join(home, 'fleet', 'roster.yaml');
await mkdir(join(home, 'fleet'), { recursive: true });
await writeFile(
rosterPath,
[
'version: 1',
'transport: tmux',
'agents:',
' - name: interaction',
' alias: Friday',
' runtime: pi',
' - name: worker0',
' runtime: codex',
].join('\n'),
);
const runner: CommandRunner = async (command, args) => {
const fullArgs = [command, ...args].join(' ');
if (fullArgs.includes('list-sessions')) {
return { stdout: 'interaction\nworker0\n', stderr: '', exitCode: 0 };
}
return {
stdout: fullArgs.includes('systemctl')
? 'ActiveState=active\nSubState=running\nUnitFileState=enabled\n'
: '12345 pi 0 0 0 0\n',
stderr: '',
exitCode: 0,
};
};
const lines: string[] = [];
const origLog = console.log;
console.log = (msg: string) => {
lines.push(msg);
};
const program = new Command();
program.exitOverride();
registerFleetCommand(program, { runner, mosaicHome: home });
try {
await program.parseAsync(['node', 'mosaic', 'fleet', 'ps']);
} finally {
console.log = origLog;
await rm(home, { recursive: true, force: true });
}
const output = lines.join('\n');
expect(output).toContain('Friday');
expect(output).toContain('worker0');
});
it('produces --json records including tenant_id and host for each agent', async () => { it('produces --json records including tenant_id and host for each agent', async () => {
const home = await mkdtemp(join(tmpdir(), 'mosaic-fleet-')); const home = await mkdtemp(join(tmpdir(), 'mosaic-fleet-'));
const rosterPath = join(home, 'fleet', 'roster.yaml'); const rosterPath = join(home, 'fleet', 'roster.yaml');

View File

@@ -81,6 +81,8 @@ interface RawFleetRoster {
runtimes?: Record<string, { reset_command?: unknown; resetCommand?: unknown }>; runtimes?: Record<string, { reset_command?: unknown; resetCommand?: unknown }>;
agents?: Array<{ agents?: Array<{
name?: unknown; name?: unknown;
alias?: unknown;
provider?: unknown;
runtime?: unknown; runtime?: unknown;
class?: unknown; class?: unknown;
working_directory?: unknown; working_directory?: unknown;
@@ -102,6 +104,8 @@ interface RawFleetRoster {
export interface FleetAgent { export interface FleetAgent {
name: string; name: string;
alias?: string;
provider?: string;
runtime: string; runtime: string;
className: string; className: string;
workingDirectory?: string; workingDirectory?: string;
@@ -1074,6 +1078,7 @@ export interface HeartbeatInfo {
export interface AgentPsRow { export interface AgentPsRow {
name: string; name: string;
alias?: string;
tenant_id: string; tenant_id: string;
host: string; host: string;
runtime: string; runtime: string;
@@ -1755,6 +1760,7 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps =
rows.push({ rows.push({
name: agent.name, name: agent.name,
alias: agent.alias,
tenant_id, tenant_id,
host, host,
runtime: agent.runtime, runtime: agent.runtime,
@@ -1888,7 +1894,7 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps =
console.log( console.log(
[ [
row.name.padEnd(18), (row.alias ?? row.name).padEnd(18),
row.tenant_id.padEnd(12), row.tenant_id.padEnd(12),
row.host.padEnd(12), row.host.padEnd(12),
row.runtime.padEnd(10), row.runtime.padEnd(10),
@@ -2494,6 +2500,8 @@ function normalizeAgent(raw: NonNullable<RawFleetRoster['agents']>[number]): Fle
assertObject(raw, 'Fleet roster agent'); assertObject(raw, 'Fleet roster agent');
assertKnownKeys(raw, 'Fleet roster agent', [ assertKnownKeys(raw, 'Fleet roster agent', [
'name', 'name',
'alias',
'provider',
'runtime', 'runtime',
'class', 'class',
'working_directory', 'working_directory',
@@ -2525,6 +2533,8 @@ function normalizeAgent(raw: NonNullable<RawFleetRoster['agents']>[number]): Fle
} }
return { return {
name, name,
alias: optionalString(raw.alias, `Fleet roster agent "${name}" alias`),
provider: optionalString(raw.provider, `Fleet roster agent "${name}" provider`),
runtime, runtime,
className: stringValue(raw.class, 'worker', `Fleet roster agent "${name}" class`), className: stringValue(raw.class, 'worker', `Fleet roster agent "${name}" class`),
workingDirectory: optionalString( workingDirectory: optionalString(
@@ -2827,6 +2837,12 @@ export function serializeRosterToYaml(roster: FleetRoster): string {
runtime: agent.runtime, runtime: agent.runtime,
class: agent.className, class: agent.className,
}; };
if (agent.alias !== undefined) {
raw['alias'] = agent.alias;
}
if (agent.provider !== undefined) {
raw['provider'] = agent.provider;
}
if (agent.workingDirectory !== undefined) { if (agent.workingDirectory !== undefined) {
raw['working_directory'] = agent.workingDirectory; raw['working_directory'] = agent.workingDirectory;
} }

View File

@@ -414,7 +414,7 @@ function readFleetToolPolicyBlock(policy: string | undefined): string {
'', '',
'Permitted: authorized conversation, status, retrieval, and safe diagnostics.', 'Permitted: authorized conversation, status, retrieval, and safe diagnostics.',
'Denied by default: coding/general orchestration claims, direct fleet control, destructive actions, and credential access.', 'Denied by default: coding/general orchestration claims, direct fleet control, destructive actions, and credential access.',
'Delegate Mos-owned work through the authorized handoff boundary.', 'Delegate orchestrator-owned work through the authorized handoff boundary.',
].join('\n'); ].join('\n');
} }