De-hardcode orchestrator and interaction agent names (#748)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful

This commit was merged in pull request #748.
This commit is contained in:
2026-07-13 18:59:27 +00:00
parent 8dd4e9d541
commit 405984af5a
33 changed files with 579 additions and 304 deletions

View File

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

View File

@@ -102,7 +102,7 @@ export interface DurableSessionStore {
export class DurableSessionNotFoundError extends Error {
constructor(sessionId: string) {
super(`Durable Tess session not found: ${sessionId}`);
super(`Durable session not found: ${sessionId}`);
this.name = 'DurableSessionNotFoundError';
}
}
@@ -212,11 +212,11 @@ export class DurableSessionCoordinator {
async resumeHandoff(handoffId: string): Promise<DurableHandoffRecovery> {
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 checkpoint = await this.store.findCheckpoint(handoff.sessionId, handoff.checkpointId);
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 };
}
@@ -257,7 +257,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore {
const existing = this.sessions.get(identity.sessionId);
if (existing) {
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.runtimeSessionId = identity.runtimeSessionId;
@@ -290,7 +290,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore {
const existing = state.inbox.get(input.idempotencyKey);
if (existing) {
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 };
}
@@ -323,7 +323,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore {
const existing = state.outbox.get(input.idempotencyKey);
if (existing) {
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 };
}
@@ -364,7 +364,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore {
const checkpoints = this.require(input.sessionId).checkpoints;
const existing = checkpoints.get(input.checkpointId);
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 });
}
@@ -377,11 +377,11 @@ export class InMemoryDurableSessionStore implements DurableSessionStore {
async handoff(input: DurableHandoffInput): Promise<void> {
const state = this.require(input.sessionId);
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);
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 });
}
@@ -413,7 +413,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore {
kind: string,
): T {
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;
}
}

View File

@@ -4,4 +4,4 @@ export * from './runtime-provider-registry.js';
export * from './tmux-fleet-runtime-provider.js';
export * from './hermes-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> {
throw new MatrixRuntimeProviderError(
'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();
});
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();
fleet.verifySession = vi.fn(async (): Promise<FleetRuntimeTarget> => {
throw new Error('target identity mismatch');
@@ -220,7 +220,20 @@ describe('TmuxFleetRuntimeProvider security policy', (): void => {
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 writeAuthority: FleetWriteAuthority = {
canWrite: vi.fn(async (): Promise<boolean> => true),
@@ -228,7 +241,7 @@ describe('TmuxFleetRuntimeProvider security policy', (): void => {
};
const provider = new TmuxFleetRuntimeProvider({
transport: fleet,
sourceLabel: 'tess',
sourceLabel: 'operator',
writeAuthority,
});
@@ -246,7 +259,7 @@ describe('TmuxFleetRuntimeProvider security policy', (): void => {
scope,
approvalRef: 'approval-1',
});
expect(fleet.sendMessage).toHaveBeenCalledWith('coder0', 'hello', 'tess');
expect(fleet.sendMessage).toHaveBeenCalledWith('coder0', 'hello', 'operator');
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
* invocation; the default authority prevents direct Tess writes by design.
* invocation; the default authority prevents direct interaction-plane writes by design.
*/
export interface FleetWriteAuthority {
/** Non-consuming preflight used before probing the fleet transport. */
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>;
}
@@ -116,7 +116,7 @@ class DenyFleetWriteAuthority implements FleetWriteAuthority {
async assertAuthorized(_authorization: FleetWriteAuthorization): Promise<void> {
throw new FleetRuntimeProviderError(
'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
* 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 {
readonly id = FLEET_PROVIDER_ID;
@@ -139,7 +139,7 @@ export class TmuxFleetRuntimeProvider implements AgentRuntimeProvider {
constructor(private readonly options: TmuxFleetRuntimeProviderOptions) {
this.readAuthority = options.readAuthority ?? new DenyFleetReadAuthority();
this.writeAuthority = options.writeAuthority ?? new DenyFleetWriteAuthority();
this.sourceLabel = options.sourceLabel ?? 'tess';
this.sourceLabel = options.sourceLabel ?? 'interaction';
this.attachmentIdFactory = options.attachmentIdFactory ?? randomUUID;
this.now = options.now ?? (() => new Date());
this.attachmentTtlMs = options.attachmentTtlMs ?? ATTACHMENT_TTL_MS;

View File

@@ -1,10 +1,10 @@
import { describe, expect, it, vi } from 'vitest';
import {
InMemoryMosCoordinationPort,
MosCoordinationClient,
InMemoryInteractionCoordinationPort,
InteractionCoordinationClient,
type CoordinationScope,
type MosCoordinationAuthorityError,
type MosCoordinationPort,
type InteractionCoordinationAuthorityError,
type InteractionCoordinationPort,
} from '../index.js';
const scope: CoordinationScope = {
@@ -15,19 +15,19 @@ const scope: CoordinationScope = {
};
function client(
port: MosCoordinationPort,
port: InteractionCoordinationPort,
handoffIdFactory: () => string = (): string => 'handoff-1',
): MosCoordinationClient {
return new MosCoordinationClient(
): InteractionCoordinationClient {
return new InteractionCoordinationClient(
{ interactionAgentId: 'Nova', orchestrationAgentId: 'Conductor' },
port,
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> => {
const adapter = new InMemoryMosCoordinationPort();
const adapter = new InMemoryInteractionCoordinationPort();
const coordination = client(adapter);
await expect(
@@ -42,8 +42,8 @@ describe('MosCoordinationClient', (): void => {
correlationId: 'corr-1',
});
adapter.recordActivity('handoff-1', 'running', 'Mos accepted the request');
adapter.recordResult('handoff-1', 'completed', 'Merged by Mos');
adapter.recordActivity('handoff-1', 'running', 'Orchestrator accepted the request');
adapter.recordResult('handoff-1', 'completed', 'Merged by orchestrator');
await expect(coordination.observe('handoff-1', scope)).resolves.toMatchObject({
status: 'completed',
@@ -59,7 +59,7 @@ describe('MosCoordinationClient', (): void => {
targetAgentId: 'Conductor',
status: 'completed',
correlationId: 'corr-1',
summary: 'Merged by Mos',
summary: 'Merged by orchestrator',
});
expect(coordination).not.toHaveProperty('dispatch');
@@ -69,8 +69,8 @@ describe('MosCoordinationClient', (): void => {
expect(coordination).not.toHaveProperty('cancel');
});
it('fails closed before delivery when an unconfigured agent requests Mos work', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort();
it('fails closed before delivery when an unconfigured agent requests orchestrator work', async (): Promise<void> => {
const adapter = new InMemoryInteractionCoordinationPort();
await expect(
client(adapter).handoff(
@@ -79,31 +79,31 @@ describe('MosCoordinationClient', (): void => {
),
).rejects.toMatchObject({
code: 'requester_forbidden',
} satisfies Partial<MosCoordinationAuthorityError>);
} satisfies Partial<InteractionCoordinationAuthorityError>);
});
it('rejects self-delegation configuration before constructing a client', (): void => {
expect(
(): MosCoordinationClient =>
new MosCoordinationClient(
(): InteractionCoordinationClient =>
new InteractionCoordinationClient(
{ interactionAgentId: 'Nova', orchestrationAgentId: 'Nova' },
new InMemoryMosCoordinationPort(),
new InMemoryInteractionCoordinationPort(),
),
).toThrow('Interaction and orchestration identities must differ');
});
it('rejects whitespace-equivalent self-delegation identities', (): void => {
expect(
(): MosCoordinationClient =>
new MosCoordinationClient(
(): InteractionCoordinationClient =>
new InteractionCoordinationClient(
{ interactionAgentId: 'Nova ', orchestrationAgentId: 'Nova' },
new InMemoryMosCoordinationPort(),
new InMemoryInteractionCoordinationPort(),
),
).toThrow('Interaction and orchestration identities must differ');
});
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);
await coordination.handoff(
{ 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> => {
const adapter = new InMemoryMosCoordinationPort({ maxHandoffs: 1 });
const adapter = new InMemoryInteractionCoordinationPort({ maxHandoffs: 1 });
const first = client(adapter, (): string => 'handoff-1');
const second = client(adapter, (): string => 'handoff-2');
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> => {
const adapter: MosCoordinationPort = {
const adapter: InteractionCoordinationPort = {
handoff: vi.fn(async () => ({
handoffId: 'handoff-1',
targetAgentId: 'Unexpected',
@@ -149,6 +149,6 @@ describe('MosCoordinationClient', (): void => {
),
).rejects.toMatchObject({
code: 'target_drift',
} satisfies Partial<MosCoordinationAuthorityError>);
} satisfies Partial<InteractionCoordinationAuthorityError>);
});
});

View File

@@ -2,25 +2,25 @@ import {
type CoordinationObservation,
type CoordinationResult,
type CoordinationScope,
type MosCoordinationActivity,
type MosCoordinationPort,
type MosHandoff,
type MosHandoffReceipt,
type MosHandoffStatus,
} from './mos-coordination.js';
type InteractionCoordinationActivity,
type InteractionCoordinationPort,
type Handoff,
type HandoffReceipt,
type HandoffStatus,
} from './interaction-coordination.js';
const DEFAULT_HANDOFF_TTL_MS = 60 * 60 * 1_000;
const DEFAULT_MAX_HANDOFFS = 1_000;
interface StoredHandoff {
readonly handoff: MosHandoff;
status: MosHandoffStatus;
readonly activity: MosCoordinationActivity[];
readonly handoff: Handoff;
status: HandoffStatus;
readonly activity: InteractionCoordinationActivity[];
readonly expiresAt: number;
result?: CoordinationResult;
}
export interface InMemoryMosCoordinationPortOptions {
export interface InMemoryInteractionCoordinationPortOptions {
now?: () => Date;
handoffTtlMs?: number;
maxHandoffs?: number;
@@ -29,21 +29,21 @@ export interface InMemoryMosCoordinationPortOptions {
/**
* Native deterministic queue/port adapter for the coordination boundary.
* 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 now: () => Date;
private readonly handoffTtlMs: number;
private readonly maxHandoffs: number;
constructor(options: InMemoryMosCoordinationPortOptions = {}) {
constructor(options: InMemoryInteractionCoordinationPortOptions = {}) {
this.now = options.now ?? (() => new Date());
this.handoffTtlMs = options.handoffTtlMs ?? DEFAULT_HANDOFF_TTL_MS;
this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS;
}
async handoff(handoff: MosHandoff): Promise<MosHandoffReceipt> {
async handoff(handoff: Handoff): Promise<HandoffReceipt> {
this.pruneExpiredHandoffs();
const existing = this.handoffs.get(handoff.handoffId);
if (existing !== undefined) {
@@ -88,14 +88,14 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
}
/** 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();
const stored = this.requireHandoff(handoffId);
stored.status = status;
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 {
this.pruneExpiredHandoffs();
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 {
handoffId: handoff.handoffId,
targetAgentId: handoff.targetAgentId,
@@ -141,7 +141,7 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
stored.handoff.scope.actorId !== scope.actorId ||
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;
}
@@ -149,12 +149,12 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
private requireHandoff(handoffId: string): StoredHandoff {
const stored = this.handoffs.get(handoffId);
if (stored === undefined) {
throw new InMemoryMosCoordinationError('not_found', 'Handoff was not found');
throw new InMemoryInteractionCoordinationError('not_found', 'Handoff was not found');
}
return stored;
}
private assertSameHandoff(existing: MosHandoff, incoming: MosHandoff): void {
private assertSameHandoff(existing: Handoff, incoming: Handoff): void {
if (
existing.targetAgentId !== incoming.targetAgentId ||
existing.request.idempotencyKey !== incoming.request.idempotencyKey ||
@@ -166,7 +166,7 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
existing.scope.correlationId !== incoming.scope.correlationId ||
existing.scope.requesterAgentId !== incoming.scope.requesterAgentId
) {
throw new InMemoryMosCoordinationError(
throw new InMemoryInteractionCoordinationError(
'conflict',
'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(
readonly code: InMemoryMosCoordinationErrorCode,
readonly code: InMemoryInteractionCoordinationErrorCode,
message: string,
) {
super(message);
this.name = InMemoryMosCoordinationError.name;
this.name = InMemoryInteractionCoordinationError.name;
}
}
function activity(
status: MosHandoffStatus,
status: HandoffStatus,
summary: string,
now: () => Date,
): MosCoordinationActivity {
): InteractionCoordinationActivity {
return { occurredAt: now().toISOString(), status, summary };
}
function copyActivity(entry: MosCoordinationActivity): MosCoordinationActivity {
function copyActivity(entry: InteractionCoordinationActivity): InteractionCoordinationActivity {
return { ...entry };
}
function snapshotHandoff(handoff: MosHandoff): MosHandoff {
function snapshotHandoff(handoff: Handoff): Handoff {
return Object.freeze({
handoffId: handoff.handoffId,
targetAgentId: handoff.targetAgentId,

View File

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

View File

@@ -2,10 +2,10 @@
The **operator-interaction** role is the authorized human interaction plane for
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
- 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 does not receive or surface credentials in its effective policy.

View File

@@ -75,6 +75,14 @@
"type": "string",
"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": {
"type": "string"
},

View File

@@ -189,6 +189,36 @@ describe('fleet roster parsing', () => {
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)', () => {
expect(socketArgs('mosaic-fleet')).toEqual(['-L', 'mosaic-fleet']);
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 () => {
const home = await mkdtemp(join(tmpdir(), 'mosaic-fleet-'));
const rosterPath = join(home, 'fleet', 'roster.yaml');

View File

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