feat(tess): add configurable Pi interaction service (#728)
This commit was merged in pull request #728.
This commit is contained in:
110
packages/mosaic/src/fleet/interaction-service-profile.ts
Normal file
110
packages/mosaic/src/fleet/interaction-service-profile.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import YAML from 'yaml';
|
||||
import type { FleetAgent } from '../commands/fleet.js';
|
||||
|
||||
const REQUIRED_RUNTIME = 'pi';
|
||||
const REQUIRED_MODEL = 'openai/gpt-5.6-sol';
|
||||
const REQUIRED_REASONING = 'high';
|
||||
const REQUIRED_TOOL_POLICY = 'operator-interaction';
|
||||
const AGENT_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
||||
|
||||
export interface InteractionServiceProfile {
|
||||
runtime: string;
|
||||
model: string;
|
||||
reasoning: string;
|
||||
toolPolicy: string;
|
||||
}
|
||||
|
||||
export interface EffectiveInteractionPolicy {
|
||||
agentName: string;
|
||||
runtime: string;
|
||||
model: string;
|
||||
reasoning: string;
|
||||
toolPolicy: string;
|
||||
}
|
||||
|
||||
export class InteractionServiceProfileError extends Error {
|
||||
constructor(
|
||||
readonly code: 'invalid_profile' | 'invalid_request',
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = InteractionServiceProfileError.name;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readInteractionServiceProfile(
|
||||
path: string,
|
||||
overrides: Partial<InteractionServiceProfile> = {},
|
||||
): Promise<InteractionServiceProfile> {
|
||||
const parsed: unknown = YAML.parse(await readFile(path, 'utf8'));
|
||||
if (!isRecord(parsed)) {
|
||||
throw new InteractionServiceProfileError(
|
||||
'invalid_profile',
|
||||
'Service profile must be an object',
|
||||
);
|
||||
}
|
||||
const profile: InteractionServiceProfile = {
|
||||
runtime: valueOrUndefined(overrides.runtime, parsed['runtime']),
|
||||
model: valueOrUndefined(overrides.model, parsed['model']),
|
||||
reasoning: valueOrUndefined(overrides.reasoning, parsed['reasoning']),
|
||||
toolPolicy: valueOrUndefined(overrides.toolPolicy, parsed['tool_policy']),
|
||||
};
|
||||
assertPinnedPolicy(profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
export function provisionInteractionService(
|
||||
profile: InteractionServiceProfile,
|
||||
input: { agentName: string },
|
||||
): { rosterAgent: FleetAgent; effectivePolicy: EffectiveInteractionPolicy } {
|
||||
assertPinnedPolicy(profile);
|
||||
if (!AGENT_NAME_PATTERN.test(input.agentName)) {
|
||||
throw new InteractionServiceProfileError(
|
||||
'invalid_request',
|
||||
'Agent name must contain only letters, numbers, dots, underscores, or hyphens',
|
||||
);
|
||||
}
|
||||
const effectivePolicy: EffectiveInteractionPolicy = {
|
||||
agentName: input.agentName,
|
||||
runtime: profile.runtime,
|
||||
model: profile.model,
|
||||
reasoning: profile.reasoning,
|
||||
toolPolicy: profile.toolPolicy,
|
||||
};
|
||||
return {
|
||||
rosterAgent: {
|
||||
name: input.agentName,
|
||||
runtime: profile.runtime,
|
||||
className: profile.toolPolicy,
|
||||
modelHint: profile.model,
|
||||
reasoningLevel: profile.reasoning,
|
||||
toolPolicy: profile.toolPolicy,
|
||||
persistentPersona: true,
|
||||
},
|
||||
effectivePolicy,
|
||||
};
|
||||
}
|
||||
|
||||
function assertPinnedPolicy(profile: InteractionServiceProfile): void {
|
||||
const invalid =
|
||||
profile.runtime !== REQUIRED_RUNTIME ||
|
||||
profile.model !== REQUIRED_MODEL ||
|
||||
profile.reasoning !== REQUIRED_REASONING ||
|
||||
profile.toolPolicy !== REQUIRED_TOOL_POLICY;
|
||||
if (invalid) {
|
||||
throw new InteractionServiceProfileError(
|
||||
'invalid_profile',
|
||||
`Service profile must pin ${REQUIRED_RUNTIME}, ${REQUIRED_MODEL}, ${REQUIRED_REASONING}, and ${REQUIRED_TOOL_POLICY}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function valueOrUndefined(override: string | undefined, value: unknown): string {
|
||||
if (override !== undefined) return override;
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
161
packages/mosaic/src/fleet/tess-service-profile.test.ts
Normal file
161
packages/mosaic/src/fleet/tess-service-profile.test.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { execFile } from 'node:child_process';
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { generateAgentEnv, loadFleetRoster } from '../commands/fleet.js';
|
||||
import {
|
||||
provisionInteractionService,
|
||||
readInteractionServiceProfile,
|
||||
} from './interaction-service-profile.js';
|
||||
import type { InteractionServiceProfileError } from './interaction-service-profile.js';
|
||||
|
||||
const frameworkFleet = resolve(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
'..',
|
||||
'framework',
|
||||
'fleet',
|
||||
);
|
||||
const profilePath = join(frameworkFleet, 'services', 'operator-interaction.yaml');
|
||||
const examplePath = join(frameworkFleet, 'examples', 'operator-interaction.yaml');
|
||||
const policyScript = join(
|
||||
frameworkFleet,
|
||||
'..',
|
||||
'tools',
|
||||
'fleet',
|
||||
'print-interaction-effective-policy.sh',
|
||||
);
|
||||
const interactionStartScript = join(
|
||||
frameworkFleet,
|
||||
'..',
|
||||
'tools',
|
||||
'fleet',
|
||||
'start-interaction-service.sh',
|
||||
);
|
||||
const agentStartScript = join(frameworkFleet, '..', 'tools', 'fleet', 'start-agent-session.sh');
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
describe('operator interaction service profile', (): void => {
|
||||
it('provisions a user-supplied Nova identity as data with the required effective policy', async (): Promise<void> => {
|
||||
const profile = await readInteractionServiceProfile(profilePath);
|
||||
const provisioned = provisionInteractionService(profile, { agentName: 'Nova' });
|
||||
|
||||
expect(provisioned.rosterAgent).toEqual({
|
||||
name: 'Nova',
|
||||
runtime: 'pi',
|
||||
className: 'operator-interaction',
|
||||
modelHint: 'openai/gpt-5.6-sol',
|
||||
reasoningLevel: 'high',
|
||||
toolPolicy: 'operator-interaction',
|
||||
persistentPersona: true,
|
||||
});
|
||||
expect(provisioned.effectivePolicy).toEqual({
|
||||
agentName: 'Nova',
|
||||
runtime: 'pi',
|
||||
model: 'openai/gpt-5.6-sol',
|
||||
reasoning: 'high',
|
||||
toolPolicy: 'operator-interaction',
|
||||
});
|
||||
expect(JSON.stringify(provisioned.effectivePolicy)).not.toMatch(
|
||||
/token|secret|credential|api.?key/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts a renamed example roster and surfaces only its effective policy', async (): Promise<void> => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'mosaic-interaction-profile-'));
|
||||
const rosterPath = join(directory, 'roster.yaml');
|
||||
try {
|
||||
const example = await readFile(examplePath, 'utf8');
|
||||
await writeFile(rosterPath, example.replace('name: Tess', 'name: Nova'), 'utf8');
|
||||
const roster = await loadFleetRoster(rosterPath);
|
||||
const agent = roster.agents[0]!;
|
||||
|
||||
expect(agent.name).toBe('Nova');
|
||||
expect(agent.reasoningLevel).toBe('high');
|
||||
expect(agent.toolPolicy).toBe('operator-interaction');
|
||||
expect(generateAgentEnv(roster, agent)).toContain('MOSAIC_AGENT_NAME=Nova');
|
||||
expect(generateAgentEnv(roster, agent)).toContain('MOSAIC_AGENT_REASONING=high');
|
||||
|
||||
const { stdout } = await execFileAsync(policyScript, [], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSAIC_AGENT_NAME: agent.name,
|
||||
MOSAIC_AGENT_RUNTIME: agent.runtime,
|
||||
MOSAIC_AGENT_MODEL: agent.modelHint,
|
||||
MOSAIC_AGENT_REASONING: agent.reasoningLevel,
|
||||
MOSAIC_AGENT_TOOL_POLICY: agent.toolPolicy,
|
||||
},
|
||||
});
|
||||
expect(JSON.parse(stdout)).toEqual({
|
||||
agentName: 'Nova',
|
||||
runtime: 'pi',
|
||||
model: 'openai/gpt-5.6-sol',
|
||||
reasoning: 'high',
|
||||
toolPolicy: 'operator-interaction',
|
||||
});
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('fails before launch when service environment drifts from the pinned policy', async (): Promise<void> => {
|
||||
await expect(
|
||||
execFileAsync(interactionStartScript, ['Nova'], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSAIC_AGENT_NAME: 'Nova',
|
||||
MOSAIC_AGENT_RUNTIME: 'pi',
|
||||
MOSAIC_AGENT_MODEL: 'other/model',
|
||||
MOSAIC_AGENT_REASONING: 'high',
|
||||
MOSAIC_AGENT_TOOL_POLICY: 'operator-interaction',
|
||||
},
|
||||
}),
|
||||
).rejects.toMatchObject({ code: 64 });
|
||||
});
|
||||
|
||||
it('rejects unsafe names from the policy printer and reasoning before tmux launch', async (): Promise<void> => {
|
||||
await expect(
|
||||
execFileAsync(policyScript, [], {
|
||||
env: {
|
||||
...process.env,
|
||||
MOSAIC_AGENT_NAME: 'Nova"bad',
|
||||
MOSAIC_AGENT_RUNTIME: 'pi',
|
||||
MOSAIC_AGENT_MODEL: 'openai/gpt-5.6-sol',
|
||||
MOSAIC_AGENT_REASONING: 'high',
|
||||
MOSAIC_AGENT_TOOL_POLICY: 'operator-interaction',
|
||||
},
|
||||
}),
|
||||
).rejects.toMatchObject({ code: 64 });
|
||||
await expect(
|
||||
execFileAsync(agentStartScript, ['Nova'], {
|
||||
env: { ...process.env, MOSAIC_AGENT_REASONING: 'high; id' },
|
||||
}),
|
||||
).rejects.toMatchObject({ code: 64 });
|
||||
});
|
||||
|
||||
it('keeps the product name confined to an example instance, not service source or defaults', async (): Promise<void> => {
|
||||
const [profile, source, example] = await Promise.all([
|
||||
readFile(profilePath, 'utf8'),
|
||||
readFile(new URL('./interaction-service-profile.ts', import.meta.url), 'utf8'),
|
||||
readFile(examplePath, 'utf8'),
|
||||
]);
|
||||
|
||||
expect(profile).not.toMatch(/tess/i);
|
||||
expect(source).not.toMatch(/tess/i);
|
||||
expect(example).toMatch(/name: Tess/);
|
||||
});
|
||||
|
||||
it('fails fast when a required policy field is absent or changed from the pinned policy', async (): Promise<void> => {
|
||||
await expect(readInteractionServiceProfile(profilePath, { model: '' })).rejects.toMatchObject({
|
||||
code: 'invalid_profile',
|
||||
} satisfies Partial<InteractionServiceProfileError>);
|
||||
await expect(
|
||||
readInteractionServiceProfile(profilePath, { reasoning: 'medium' }),
|
||||
).rejects.toMatchObject({
|
||||
code: 'invalid_profile',
|
||||
} satisfies Partial<InteractionServiceProfileError>);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user