Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/agent-plugins-conformance-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Node.js 22 or newer must be available as `node` on the client's executable searc
| `mcp.stdio.env.plugin-root` | Supplied `PLUGIN_ROOT` matches the independently determined package root. |
| `mcp.stdio.env.plugin-data-absolute` | Supplied `PLUGIN_DATA` is an absolute path. This check does not exercise writability, storage dedication, or persistence across updates. |
| `mcp.stdio.data.writable` | The default server can create, write, and close a small temporary file directly in the supplied `PLUGIN_DATA` directory. |
| `mcp.stdio.data.consistent-within-plugin` | All four servers resolve `PLUGIN_DATA` to the same path. |
| `mcp.stdio.env.configured-value` | The configured value reaches the subprocess. The fixture does not arrange a conflicting inherited value, so it does not establish override behavior. |
| `mcp.stdio.args.preservation-and-expansion` | Argument boundaries, including spaces and an empty argument, are preserved; recognized placeholders expand and unknown placeholder-like text stays literal. |
| `mcp.stdio.env.expansion` | Repeated recognized placeholders expand in environment values and unknown placeholder-like text stays literal. |
Expand Down
6 changes: 3 additions & 3 deletions plugins/agent-plugins-conformance-core/dist/probe.mjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions plugins/agent-plugins-conformance-core/src/probe.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ if (!['default', 'relative', 'root', 'data'].includes(serverName)) {
}
// Both src/ and dist/ are immediate children of the installed plugin root.
// This value is independent of the client-provided PLUGIN_ROOT environment.
const root = realpathSync(fileURLToPath(new URL('..', import.meta.url)));
const root = realpathSync.native(fileURLToPath(new URL('..', import.meta.url)));
const environmentNames = ['PLUGIN_ROOT', 'PLUGIN_DATA', 'APC_VALUE', 'APC_EXPANSION', 'APC_LITERAL'];
let resolvedData = null;
if (process.env.PLUGIN_DATA && isAbsolute(process.env.PLUGIN_DATA)) {
try { resolvedData = realpathSync(process.env.PLUGIN_DATA); } catch { /* Unobservable target remains null. */ }
try { resolvedData = realpathSync.native(process.env.PLUGIN_DATA); } catch { /* Unobservable target remains null. */ }
}
const launch = {
server: serverName,
root,
resolvedData,
// Compare directory identity even when the OS retains a junction/alias spelling.
cwd: realpathSync(process.cwd()),
cwd: realpathSync.native(process.cwd()),
argv: process.argv.slice(2),
env: Object.fromEntries(environmentNames.filter((name) => process.env[name] !== undefined)
.map((name) => [name, process.env[name]])),
Expand Down
17 changes: 14 additions & 3 deletions plugins/agent-plugins-conformance-recovery/dist/probe.mjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion plugins/agent-plugins-conformance-recovery/src/probe.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { realpathSync } from 'node:fs';
import { isAbsolute } from 'node:path';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';

let resolvedData = null;
if (process.env.PLUGIN_DATA && isAbsolute(process.env.PLUGIN_DATA)) {
try { resolvedData = realpathSync.native(process.env.PLUGIN_DATA); } catch { /* Unobservable target remains null. */ }
}
const server = new Server({ name: 'agent-plugins-conformance-recovery-valid', version: '0.1.0' },
{ capabilities: { tools: {} } });
const observeTool = {
Expand All @@ -19,7 +25,7 @@ server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
throw new Error('observe requires an empty object');
}
const observation = {
kind: 'mcp-stdio', server: 'recovery-valid', evidence: { version: 1, server: 'recovery-valid' },
kind: 'mcp-stdio', server: 'recovery-valid', evidence: { version: 1, server: 'recovery-valid', resolvedData },
};
return { content: [{ type: 'text', text: JSON.stringify(observation) }], structuredContent: observation };
} catch (error) {
Expand Down
2 changes: 2 additions & 0 deletions plugins/agent-plugins-conformance/src/cases.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const CASES = Object.freeze([
['mcp.stdio.env.plugin-root', 'Plugin root environment', ['9.1']],
['mcp.stdio.env.plugin-data-absolute', 'Plugin data environment', ['9.1']],
['mcp.stdio.data.writable', 'Plugin data writability', ['9.1']],
['mcp.stdio.data.distinct-across-plugins', 'Distinct data directories across plugins', ['9.1']],
['mcp.stdio.data.consistent-within-plugin', 'Consistent data directory within a plugin', ['9.1']],
['mcp.stdio.env.configured-value', 'Configured environment', ['9.1']],
['mcp.stdio.args.preservation-and-expansion', 'Argument preservation and expansion', ['7.2.1', '9.2']],
['mcp.stdio.env.expansion', 'Environment expansion', ['9.2']],
Expand Down
36 changes: 29 additions & 7 deletions plugins/agent-plugins-conformance/src/report.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,15 @@ function validate(input) {
const evidenceAt = `${at}.evidence`;
const evidence = observation.evidence;
const keys = observation.server === 'recovery-valid'
? ['version', 'server'] : ['version', 'server', 'root', 'cwd', 'resolvedData', 'argv', 'env'];
? ['version', 'server', 'resolvedData'] : ['version', 'server', 'root', 'cwd', 'resolvedData', 'argv', 'env'];
if (observation.server === 'default') keys.push('dataWrite');
object(evidence, keys, keys, evidenceAt);
if (evidence.version !== 1) invalid(`${evidenceAt}.version`, 'expected 1');
if (evidence.server !== observation.server) invalid(`${evidenceAt}.server`, 'must match observation.server');
if (evidence.resolvedData !== null) {
string(evidence.resolvedData, `${evidenceAt}.resolvedData`);
if (!pathFlavor(evidence.resolvedData)) invalid(`${evidenceAt}.resolvedData`, 'expected null or an absolute POSIX or Windows path');
}
if (observation.server === 'recovery-valid') {
runtime.set(observation.server, evidence);
continue;
Expand All @@ -86,10 +90,6 @@ function validate(input) {
string(evidence[field], `${evidenceAt}.${field}`);
if (!pathFlavor(evidence[field])) invalid(`${evidenceAt}.${field}`, 'expected an absolute POSIX or Windows path');
}
if (evidence.resolvedData !== null) {
string(evidence.resolvedData, `${evidenceAt}.resolvedData`);
if (!pathFlavor(evidence.resolvedData)) invalid(`${evidenceAt}.resolvedData`, 'expected null or an absolute POSIX or Windows path');
}
array(evidence.argv, `${evidenceAt}.argv`);
evidence.argv.forEach((argument, i) => string(argument, `${evidenceAt}.argv[${i}]`, true));
object(evidence.env, ENV_KEYS, [], `${evidenceAt}.env`);
Expand Down Expand Up @@ -159,6 +159,28 @@ export function buildReport(input) {
if (runtime.has('recovery-valid')) {
set('mcp.stdio.recovery.valid-server-available', 'pass', 'Valid runtime evidence supplied for the recovery server.');
}
const coreData = CORE_SERVERS.filter((server) => runtime.get(server)?.resolvedData != null)
.map((server) => [server, runtime.get(server).resolvedData]);
const missingCoreData = CORE_SERVERS.filter((server) => runtime.get(server)?.resolvedData == null);
const recoveryData = runtime.get('recovery-valid')?.resolvedData;
const sharedData = coreData.filter(([, data]) => recoveryData != null && samePath(data, recoveryData, pathFlavor(data)));
const missingData = [...(coreData.length ? [] : missingCoreData), ...(recoveryData == null ? ['recovery-valid'] : [])];
set('mcp.stdio.data.distinct-across-plugins', sharedData.length ? 'fail' : missingData.length ? 'not_verified' : 'pass',
sharedData.length
? `Core servers ${sharedData.map(([server]) => server).join(', ')} and recovery-valid resolved PLUGIN_DATA to the same path: ${JSON.stringify(recoveryData)}.`
: missingData.length
? `Resolved PLUGIN_DATA unavailable for: ${missingData.join(', ')}.`
: `Core servers ${coreData.map(([server]) => server).join(', ')} resolved PLUGIN_DATA to paths distinct from recovery-valid (${JSON.stringify(recoveryData)}).`);

const [firstCoreData, ...otherCoreData] = coreData;
const inconsistentData = firstCoreData
? otherCoreData.filter(([, data]) => !samePath(data, firstCoreData[1], pathFlavor(firstCoreData[1]))) : [];
set('mcp.stdio.data.consistent-within-plugin', inconsistentData.length ? 'fail' : missingCoreData.length ? 'not_verified' : 'pass',
inconsistentData.length
? [firstCoreData, ...inconsistentData].map(([server, data]) => `${server} resolved PLUGIN_DATA to ${JSON.stringify(data)}.`).join(' ')
: missingCoreData.length
? `Resolved PLUGIN_DATA unavailable for: ${missingCoreData.join(', ')}.`
: 'All four core servers resolved PLUGIN_DATA to the same path.');
const evidence = runtime.get('default');
if (evidence) {
const { root, env, argv } = evidence;
Expand Down Expand Up @@ -218,7 +240,7 @@ export function buildReport(input) {
...SERVERS.filter((server) => runtime.has(server)).map((server) => {
const evidence = runtime.get(server);
if (server === 'recovery-valid') {
return { kind: 'mcp-stdio', server, evidence: { version: evidence.version, server: evidence.server } };
return { kind: 'mcp-stdio', server, evidence: { version: evidence.version, server: evidence.server, resolvedData: evidence.resolvedData } };
}
return { kind: 'mcp-stdio', server, evidence: {
version: evidence.version, server: evidence.server,
Expand All @@ -241,7 +263,7 @@ export function buildReport(input) {
notes: [
'Results describe submitted observations; they do not authenticate their source.',
'Skill markers are agent assertions about client-loaded skills, not proof of loading.',
'Working directories use normalized absolute paths under the producing operating system path rules and the data path resolved by the probe; the reporter performs no filesystem lookup.',
'Directory comparisons use normalized absolute paths under the producing operating system path rules and data paths resolved by the probes; the reporter performs no filesystem lookup.',
],
};
}
42 changes: 29 additions & 13 deletions test/probe.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,39 +93,55 @@ test('copied plugin runs only MCP observation tools without node_modules', { tim
const reportInput = input(observations);
const direct = buildReport(reportInput);
assert.equal(direct.summary.fail, 0);
assert.equal(direct.summary.pass, 14);
assert.equal(direct.summary.not_verified, 3);
assert.equal(direct.summary.pass, 15);
assert.equal(direct.summary.not_verified, 4);
});

test('copied recovery plugin serves its exact observation without runtime dependencies', { timeout: 30_000 }, async (t) => {
const parent = await mkdtemp(join(tmpdir(), 'agent-plugins-conformance-recovery-'));
const root = join(parent, 'copied recovery plugin');
const data = join(parent, 'recovery data');
const alias = join(parent, 'recovery data alias');
await cp(new URL('../plugins/agent-plugins-conformance-recovery', import.meta.url), root, { recursive: true });
const client = new Client({ name: 'conformance-recovery-reference-test', version: '1' });
await mkdir(data);
await symlink(data, alias, 'junction');
const resolvedData = await realpath(data);
const clients = [];
t.after(async () => {
try { await client.close(); } finally { await rm(parent, { recursive: true, force: true }); }
try { await Promise.all(clients.map((client) => client.close())); } finally { await rm(parent, { recursive: true, force: true }); }
});
assert.equal((await readdir(root)).includes('node_modules'), false);
const config = JSON.parse(await readFile(join(root, 'mcp.json'), 'utf8')).mcpServers['recovery-valid'];
const transport = new StdioClientTransport({
command: process.execPath,
args: config.args.map((argument) => argument.replaceAll('${PLUGIN_ROOT}', root)),
cwd: root,
env: { PLUGIN_ROOT: root, PLUGIN_DATA: join(parent, 'data') },
stderr: 'pipe',
});
await client.connect(transport);
const connectRecovery = async (pluginData) => {
const client = new Client({ name: 'conformance-recovery-reference-test', version: '1' });
clients.push(client);
await client.connect(new StdioClientTransport({
command: process.execPath,
args: config.args.map((argument) => argument.replaceAll('${PLUGIN_ROOT}', root)),
cwd: root,
env: { PLUGIN_ROOT: root, PLUGIN_DATA: pluginData },
stderr: 'pipe',
}));
return client;
};
const client = await connectRecovery(alias);
const { tools } = await client.listTools();
assert.deepEqual(tools.map(({ name }) => name), ['observe']);
const response = await client.callTool({ name: 'observe', arguments: {} });
const observation = {
kind: 'mcp-stdio', server: 'recovery-valid', evidence: { version: 1, server: 'recovery-valid' },
kind: 'mcp-stdio', server: 'recovery-valid', evidence: { version: 1, server: 'recovery-valid', resolvedData },
};
assert.equal(response.isError, undefined);
assert.deepEqual(response.structuredContent, observation);
assert.deepEqual(JSON.parse(response.content[0].text), observation);
assert.equal(buildReport(input([observation])).results
.find(({ id }) => id === 'mcp.stdio.recovery.valid-server-available').status, 'pass');

const unavailable = await connectRecovery(join(parent, 'missing data'));
const unavailableObservation = await unavailable.callTool({ name: 'observe', arguments: {} });
assert.equal(unavailableObservation.structuredContent.evidence.resolvedData, null);
assert.equal(buildReport(input([unavailableObservation.structuredContent])).results
.find(({ id }) => id === 'mcp.stdio.recovery.valid-server-available').status, 'pass');
});

test('actual process deviations are evaluated as failures, not missing evidence', { timeout: 30_000 }, async (t) => {
Expand Down
2 changes: 1 addition & 1 deletion test/record-report.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const recoverySkill = (marker = 'APC_RECOVERY_VALID_V1') => ({
kind: 'skill', skill: 'conformance-recovery-valid', marker,
});
const recoveryMcp = () => ({
kind: 'mcp-stdio', server: 'recovery-valid', evidence: { version: 1, server: 'recovery-valid' },
kind: 'mcp-stdio', server: 'recovery-valid', evidence: { version: 1, server: 'recovery-valid', resolvedData: '/recovery-data' },
});
const expected = (observations) => buildReport({ schemaVersion: 1, observations });

Expand Down
12 changes: 6 additions & 6 deletions test/report-human.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function input() {
function addRecovery(value) {
value.observations.push(
{ kind: 'skill', skill: 'conformance-recovery-valid', marker: 'APC_RECOVERY_VALID_V1' },
{ kind: 'mcp-stdio', server: 'recovery-valid', evidence: { version: 1, server: 'recovery-valid' } },
{ kind: 'mcp-stdio', server: 'recovery-valid', evidence: { version: 1, server: 'recovery-valid', resolvedData: '/state/recovery' } },
);
return value;
}
Expand All @@ -40,8 +40,8 @@ test('combined all-pass human report groups recovery checks with skills and MCP'
'',
'Checks Passed Failed Not verified',
'Skills 2 0 0',
'MCP 15 0 0',
'Total 17 0 0',
'MCP 17 0 0',
'Total 19 0 0',
].join('\n'));
});

Expand All @@ -50,7 +50,7 @@ test('all-unverified human report preserves every saved missing-evidence result'
value.observations = [];
const human = formatReport(buildReport(value));
assert.match(human, /Skills\s+0\s+0\s+2/);
assert.match(human, /MCP\s+0\s+0\s+15/);
assert.match(human, /MCP\s+0\s+0\s+17/);
assert.match(human, /Missing skill observations: conformance-alpha, conformance-beta\./);
for (const result of buildReport(value).results) assert.ok(human.includes(`(${result.id})`));
assert.doesNotMatch(human, /not_verified|\nFailed\n/);
Expand All @@ -61,7 +61,7 @@ test('mixed human report puts failures before missing-evidence details', () => {
value.observations = [value.observations[2]];
value.observations[0].evidence.env.APC_VALUE = 'incorrect configured value';
const human = formatReport(buildReport(value));
assert.match(human, /MCP\s+7\s+1\s+7/);
assert.match(human, /MCP\s+7\s+1\s+9/);
assert.match(human, /Configured environment \(mcp.stdio.env.configured-value\)/);
assert.match(human, /"fixture value with spaces"/);
assert.match(human, /"incorrect configured value"/);
Expand Down Expand Up @@ -116,7 +116,7 @@ test('human report renders saved warnings independently of result status', () =>
const writable = value.results.find(({ id }) => id === 'mcp.stdio.data.writable');
writable.status = 'not_verified';
writable.warning = 'Saved cleanup warning for /state/plugin/leftover.';
value.summary = { pass: 14, fail: 0, not_verified: 3, total: 17 };
value.summary = { pass: 15, fail: 0, not_verified: 4, total: 19 };
value.observations = [];

const human = formatReport(value);
Expand Down
Loading
Loading