feat(tess): add configurable pi interaction service
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
Jarvis
2026-07-12 21:37:53 -05:00
parent 86a50138a9
commit c58e86e2b9
20 changed files with 550 additions and 7 deletions

View File

@@ -165,6 +165,19 @@ describe('composeContract — overlay composer', () => {
expect(composeContract('codex', fixture.home)).not.toContain('# pi runtime contract');
});
it('injects the configured operator-interaction tool policy into the runtime contract', () => {
const previous = process.env['MOSAIC_AGENT_TOOL_POLICY'];
try {
process.env['MOSAIC_AGENT_TOOL_POLICY'] = 'operator-interaction';
const out = composeContract('pi', fixture.home);
expect(out).toContain('# Fleet Tool Policy (operator-interaction)');
expect(out).toContain('Denied by default: coding/general orchestration claims');
} finally {
if (previous === undefined) delete process.env['MOSAIC_AGENT_TOOL_POLICY'];
else process.env['MOSAIC_AGENT_TOOL_POLICY'] = previous;
}
});
// ── Persona contract injection (A3b) ──────────────────────────────────────
// composeContract reads MOSAIC_AGENT_CLASS and injects the resolved persona
// (override-aware). Save/restore the env so these tests don't leak state.

View File

@@ -87,6 +87,10 @@ interface RawFleetRoster {
workingDirectory?: unknown;
model_hint?: unknown;
modelHint?: unknown;
reasoning_level?: unknown;
reasoningLevel?: unknown;
tool_policy?: unknown;
toolPolicy?: unknown;
persistent_persona?: unknown;
persistentPersona?: unknown;
reset_between_tasks?: unknown;
@@ -102,6 +106,8 @@ export interface FleetAgent {
className: string;
workingDirectory?: string;
modelHint?: string;
reasoningLevel?: string;
toolPolicy?: string;
persistentPersona?: boolean | string;
resetBetweenTasks?: boolean;
kickstartTemplate?: string;
@@ -510,6 +516,12 @@ export function generateAgentEnv(roster: FleetRoster, agent: FleetAgent): string
// the `mosaic yolo` launch so workers run on the roster's model (e.g. pi on
// openai-codex/gpt-5.5:high). Empty when the agent declares no model_hint.
`MOSAIC_AGENT_MODEL=${shellEnvValue(agent.modelHint ?? '')}`,
...(agent.reasoningLevel !== undefined
? [`MOSAIC_AGENT_REASONING=${shellEnvValue(agent.reasoningLevel)}`]
: []),
...(agent.toolPolicy !== undefined
? [`MOSAIC_AGENT_TOOL_POLICY=${shellEnvValue(agent.toolPolicy)}`]
: []),
`MOSAIC_AGENT_WORKDIR=${shellEnvValue(expandHome(workingDirectory))}`,
`MOSAIC_TMUX_SOCKET=${shellEnvValue(roster.tmux.socketName)}`,
'',
@@ -2316,13 +2328,35 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise<void>
await mkdir(activePaths.agentEnvDir, { recursive: true });
const startAgentSessionPath = join(activePaths.fleetToolsDir, 'start-agent-session.sh');
const startInteractionServicePath = join(
activePaths.fleetToolsDir,
'start-interaction-service.sh',
);
const printInteractionPolicyPath = join(
activePaths.fleetToolsDir,
'print-interaction-effective-policy.sh',
);
const sendMessagePath = join(activePaths.tmuxToolsDir, 'send-message.sh');
const agentSendPath = join(activePaths.tmuxToolsDir, 'agent-send.sh');
const executableToolPaths = [startAgentSessionPath, sendMessagePath, agentSendPath];
const executableToolPaths = [
startAgentSessionPath,
startInteractionServicePath,
printInteractionPolicyPath,
sendMessagePath,
agentSendPath,
];
await copyFile(
join(frameworkRoot, 'tools', 'fleet', 'start-agent-session.sh'),
startAgentSessionPath,
);
await copyFile(
join(frameworkRoot, 'tools', 'fleet', 'start-interaction-service.sh'),
startInteractionServicePath,
);
await copyFile(
join(frameworkRoot, 'tools', 'fleet', 'print-interaction-effective-policy.sh'),
printInteractionPolicyPath,
);
await copyFile(join(frameworkRoot, 'tools', 'tmux', 'send-message.sh'), sendMessagePath);
await copyFile(join(frameworkRoot, 'tools', 'tmux', 'agent-send.sh'), agentSendPath);
for (const toolPath of executableToolPaths) {
@@ -2336,6 +2370,10 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise<void>
join(frameworkRoot, 'systemd', 'user', 'mosaic-agent@.service'),
join(activePaths.systemdUserDir, 'mosaic-agent@.service'),
);
await copyFile(
join(frameworkRoot, 'systemd', 'user', 'mosaic-interaction-agent@.service'),
join(activePaths.systemdUserDir, 'mosaic-interaction-agent@.service'),
);
for (const agent of roster.agents) {
const envPath = join(activePaths.agentEnvDir, `${agent.name}.env`);
@@ -2462,6 +2500,10 @@ function normalizeAgent(raw: NonNullable<RawFleetRoster['agents']>[number]): Fle
'workingDirectory',
'model_hint',
'modelHint',
'reasoning_level',
'reasoningLevel',
'tool_policy',
'toolPolicy',
'persistent_persona',
'persistentPersona',
'reset_between_tasks',
@@ -2493,6 +2535,14 @@ function normalizeAgent(raw: NonNullable<RawFleetRoster['agents']>[number]): Fle
raw.model_hint ?? raw.modelHint,
`Fleet roster agent "${name}" model_hint`,
),
reasoningLevel: optionalString(
raw.reasoning_level ?? raw.reasoningLevel,
`Fleet roster agent "${name}" reasoning_level`,
),
toolPolicy: optionalString(
raw.tool_policy ?? raw.toolPolicy,
`Fleet roster agent "${name}" tool_policy`,
),
persistentPersona: optionalBooleanOrString(
raw.persistent_persona ?? raw.persistentPersona,
`Fleet roster agent "${name}" persistent_persona`,
@@ -2783,6 +2833,12 @@ export function serializeRosterToYaml(roster: FleetRoster): string {
if (agent.modelHint !== undefined) {
raw['model_hint'] = agent.modelHint;
}
if (agent.reasoningLevel !== undefined) {
raw['reasoning_level'] = agent.reasoningLevel;
}
if (agent.toolPolicy !== undefined) {
raw['tool_policy'] = agent.toolPolicy;
}
if (agent.persistentPersona !== undefined) {
raw['persistent_persona'] = agent.persistentPersona;
}

View File

@@ -395,6 +395,9 @@ For required push/merge/issue-close/release actions, execute without routine con
const persona = readPersonaContractBlock(mosaicHome, process.env['MOSAIC_AGENT_CLASS']);
if (persona) parts.push('\n\n' + persona);
const toolPolicy = readFleetToolPolicyBlock(process.env['MOSAIC_AGENT_TOOL_POLICY']);
if (toolPolicy) parts.push('\n\n' + toolPolicy);
// Fleet onboarding: when this is a spawned fleet agent (MOSAIC_AGENT_NAME set
// and present in the roster), inject a comms cheat-sheet + peer roster so it
// knows how to reach the orchestrator and its peers from its first turn.
@@ -404,6 +407,17 @@ For required push/merge/issue-close/release actions, execute without routine con
return parts.join('\n');
}
function readFleetToolPolicyBlock(policy: string | undefined): string {
if (policy !== 'operator-interaction') return '';
return [
'# Fleet Tool Policy (operator-interaction)',
'',
'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.',
].join('\n');
}
/** @deprecated internal alias — use composeContract. Retained for call-site clarity. */
function buildRuntimePrompt(runtime: RuntimeName): string {
return composeContract(runtime);

View 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);
}

View 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>);
});
});

View File

@@ -1,5 +1,6 @@
export const VERSION = '0.0.0';
export * from './fleet/interaction-service-profile.js';
export * from './fleet/tmux-runtime-transport.js';
export {