TASK-33d49b5b: compose bound owner reply MCP scenario

This commit is contained in:
thePR0M3TH3AN committed 2026-10-03 18:33:10 -04:00
1 parent 6c217fcc91
commit 4214217840
3 files changed
+190 -3

No files matched your search

+27
View File
@@ -3100,3 +3100,30 @@ test_integrity_note:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
## 2026-10-03 — Public MCP owner-reply wrapper
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-mcp-owner-reply-public-boundary
given: "A registered agent has an owner-originated request addressed to it and a second registered identity exists"
when: "Each identity invokes the public stdio MCP owner-reply tool"
then: "Only the bound addressee can persist a reply with replyTo and audit attribution; selector fields and another valid identity refuse before a write"
observable_outcomes:
- "The MCP schema exposes only request_id and body with no additional properties or owner-history tool"
- "The persisted reply is linked to the owner request and audited as the bound actor"
- "Selector-field and wrong-addressee refusals leave owner conversation and audit counts unchanged"
determinism_controls:
- "Hermetic Git/SQLite fixture, fixed topology, local stdio MCP process, no network, provider, sleep, or retry"
anti_cheat_rationale:
prevents:
- "Selecting a sender, recipient, owner, or history through MCP input"
- "Replying from a different valid Fleet identity"
- "Exposing an agent-facing owner-history reader"
- "Claiming an in-process helper as public MCP qualification"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
+26 -3
View File
@@ -19,6 +19,7 @@ export const TORCH_CORE_MCP_TOOL_NAMES = Object.freeze([
'torch_who_owns',
'torch_neighbours',
'torch_send_message',
'torch_reply_to_owner',
'torch_read_messages',
'torch_ack_message',
'torch_report_status',
@@ -100,6 +101,19 @@ function claimedIdentity(controlPlane, actorId, claim, field) {
});
}
function boundActorIdentity(controlPlane, actorId) {
if (typeof actorId !== 'string' || actorId.trim().length === 0) {
throw new TorchError('torch_reply_to_owner requires an actual bound Fleet identity', {
code: 'FLEET_IDENTITY_REQUIRED', details: { field: 'bound_actor' },
});
}
return controlPlane.assertIdentity(actorId);
}
function inputSchema(tool) {
return typeof tool.schema?.safeParse === 'function' ? tool.schema : z.object(tool.schema);
}
export function createTorchToolset(controlPlane, {
actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService,
evolutionService, hierarchyService, repositoryRoot,
@@ -169,6 +183,15 @@ export function createTorchToolset(controlPlane, {
sender: claimedIdentity(controlPlane, actorId, sender, 'sender'), recipient, body, references,
}),
}],
['torch_reply_to_owner', {
description: 'Reply to one owner-originated request as the bound Fleet identity. This tool cannot select a sender, recipient, owner, or conversation history.',
schema: z.object({
request_id: z.string().min(1), body: z.string().min(1),
}).strict(),
invoke: ({ request_id: requestId, body }) => controlPlane.sendOwnerReply({
sender: boundActorIdentity(controlPlane, actorId), requestId, body,
}),
}],
['torch_read_messages', {
description: 'Read durable direct and group messages visible to one Fleet identity.',
schema: {
@@ -736,14 +759,14 @@ export function describeTorchTools() {
}).entries()].map(([name, tool]) => ({
name,
description: tool.description,
inputSchema: z.toJSONSchema(z.object(tool.schema)),
inputSchema: z.toJSONSchema(inputSchema(tool)),
}));
}
export function callTorchTool(controlPlane, name, input = {}, options = {}) {
const tool = createTorchToolset(controlPlane, options).get(name);
if (!tool) throw new Error(`Unknown TORCH MCP tool: ${name}`);
const parsed = z.object(tool.schema).parse(input);
const parsed = inputSchema(tool).parse(input);
return tool.invoke(parsed);
}
@@ -762,7 +785,7 @@ export function createTorchMcpServer(controlPlane, {
actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService,
evolutionService, hierarchyService, repositoryRoot, artifactService,
})) {
server.registerTool(name, { description: tool.description, inputSchema: tool.schema }, async (input) => {
server.registerTool(name, { description: tool.description, inputSchema: inputSchema(tool) }, async (input) => {
try {
const result = await tool.invoke(input);
return {
+137
View File
@@ -0,0 +1,137 @@
import assert from 'node:assert/strict';
import { execFileSync, spawn } from 'node:child_process';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { openControlPlane } from '../../src/control-plane/service.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { installProject } from '../../src/kernel/install.mjs';
const MCP_SERVER = new URL('../../src/mcp/server.mjs', import.meta.url).pathname;
function installedFixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-mcp-owner-reply-'));
execFileSync('git', ['init', '-b', 'main', root]);
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
writeFileSync(join(root, 'index.js'), 'export const fixture = true;\n');
execFileSync('git', ['-C', root, 'add', '.']);
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']);
const repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
proposal.review = { status: 'approved', reviewedAt: '2026-10-03T00:00:00Z', reviewedBy: 'fixture-owner', notes: [] };
const env = { ...process.env, XDG_DATA_HOME: join(root, '.data') };
installProject({ repository, proposal, env, projectId: 'mcp-owner-reply-fixture' });
const control = openControlPlane({ repositoryRoot: root, env });
const identities = control.listAgents().map((agent) => agent.areaId);
control.close();
const worker = identities.find((areaId) => areaId !== 'qa' && areaId !== 'session-manager')
?? identities.find((areaId) => areaId !== 'qa')
?? identities[0];
const other = identities.find((areaId) => areaId !== worker);
assert.ok(other, 'fixture requires a second registered Fleet identity');
return { root, env, worker, other };
}
async function connectMcp({ root, env, actorId }) {
const child = spawn(process.execPath, [MCP_SERVER, '--root', root, '--area', actorId], {
cwd: root, env, stdio: ['pipe', 'pipe', 'pipe'],
});
const responses = new Map();
let stdout = '';
let stderr = '';
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.stdout.on('data', (chunk) => {
stdout += chunk;
const lines = stdout.split('\n');
stdout = lines.pop();
for (const line of lines.filter(Boolean)) {
const message = JSON.parse(line);
if (message.id !== undefined) responses.get(message.id)?.(message);
}
});
let nextId = 0;
const request = (method, params) => new Promise((resolve, reject) => {
const id = ++nextId;
const timeout = setTimeout(() => reject(new Error(`MCP response timeout. stderr: ${stderr}`)), 5000);
responses.set(id, (response) => { clearTimeout(timeout); resolve(response); });
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
});
const initialized = await request('initialize', {
protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'torch-test', version: '1.0.0' },
});
assert.equal(initialized.result.serverInfo.name, 'torch-agent-fleet');
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\n`);
return {
request,
async close() {
child.stdin.end();
await new Promise((resolve) => {
const timeout = setTimeout(() => { child.kill('SIGTERM'); resolve(); }, 1000);
child.once('exit', () => { clearTimeout(timeout); resolve(); });
});
},
};
}
test('SCN-mcp-owner-reply-public-boundary: a bound MCP identity can reply only to its own owner request', async () => {
const fixture = installedFixture();
let control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
const request = control.sendOwnerRequest({ actorId: 'owner', recipient: fixture.worker, body: 'Report the durable outcome.' });
control.close();
const workerMcp = await connectMcp({ ...fixture, actorId: fixture.worker });
try {
const listed = await workerMcp.request('tools/list', {});
const replyTool = listed.result.tools.find((tool) => tool.name === 'torch_reply_to_owner');
assert.ok(replyTool);
assert.deepEqual(Object.keys(replyTool.inputSchema.properties).sort(), ['body', 'request_id']);
assert.equal(replyTool.inputSchema.additionalProperties, false);
assert.equal(listed.result.tools.some((tool) => /owner.*conversation|history/i.test(tool.name)), false);
const replied = await workerMcp.request('tools/call', {
name: 'torch_reply_to_owner', arguments: { request_id: request.id, body: 'The durable outcome is ready.' },
});
assert.equal(replied.result.isError, undefined);
const reply = JSON.parse(replied.result.content[0].text);
control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
const conversation = control.readOwnerConversation({ actorId: 'owner', requestId: request.id, limit: 10 });
assert.deepEqual(conversation.replies.map((message) => message.id), [reply.id]);
assert.equal(conversation.replies[0].references.replyTo, request.id);
const audit = control.readAudit().find((event) => event.operation === 'message.reply-owner' && event.entityId === reply.id);
assert.deepEqual(audit && { actorId: audit.actorId, details: audit.details }, { actorId: fixture.worker, details: { requestId: request.id } });
const before = { audit: control.readAudit().length, replies: conversation.replies.length };
control.close();
const schemaRefusal = await workerMcp.request('tools/call', {
name: 'torch_reply_to_owner',
arguments: { request_id: request.id, body: 'Attempt to select a sender.', sender: fixture.other },
});
assert.equal(schemaRefusal.result.isError, true);
control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
assert.equal(control.readAudit().length, before.audit);
assert.equal(control.readOwnerConversation({ actorId: 'owner', requestId: request.id, limit: 10 }).replies.length, before.replies);
control.close();
} finally {
await workerMcp.close();
}
const otherMcp = await connectMcp({ ...fixture, actorId: fixture.other });
try {
const wrongActor = await otherMcp.request('tools/call', {
name: 'torch_reply_to_owner', arguments: { request_id: request.id, body: 'A valid identity cannot reply for another agent.' },
});
assert.equal(wrongActor.result.isError, true);
assert.match(wrongActor.result.content[0].text, /owner request addressed to the sender/i);
} finally {
await otherMcp.close();
}
control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
assert.equal(control.readOwnerConversation({ actorId: 'owner', requestId: request.id, limit: 10 }).replies.length, 1);
control.close();
});