feat(tess): add configurable Pi interaction service (#728)
This commit was merged in pull request #728.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user