mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(identity)!: preserve invocation uncertainty and advertised catalogs
Capture the current entrypoint once without inventing a path. Preserve unknown registration ownership through setup and doctor, refuse unsafe registration plans, and retain exact pinned-launcher readiness checks. Hash the same full or compact schema projection advertised by each MCP server and use that identity in doctor comparisons. BREAKING CHANGE: server.command_path may be null when the invocation path is unavailable. Registration and setup statuses, and doctor classifications, may report unknown. Reviewed-PR: #3230, #3015 Co-authored-by: qorvr <330186599+qorvr@users.noreply.github.com> Co-authored-by: Hemang Varshney <218273197+hemv-857@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { basename, isAbsolute, resolve } from "node:path";
|
||||
import { isAbsolute, resolve } from "node:path";
|
||||
import { valid } from "semver";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
@@ -112,13 +112,56 @@ const isOwnedPackageReference = (packageReference: string): boolean => {
|
||||
};
|
||||
|
||||
/** Check that one parsed command points to REA's MCP entry point. */
|
||||
export type ClientRegistrationCommandIdentity = "owned" | "foreign" | "unknown";
|
||||
|
||||
/** Classify only what the command and current executable identity establish. */
|
||||
export const classifyClientRegistrationCommand = (
|
||||
command: readonly string[],
|
||||
currentCommandPath: string | undefined,
|
||||
): ClientRegistrationCommandIdentity => {
|
||||
const packageReference = clientRegistrationPackageReference(command);
|
||||
if (packageReference !== undefined)
|
||||
return isOwnedClientRegistrationCommand(command, currentCommandPath)
|
||||
? "owned"
|
||||
: "foreign";
|
||||
const currentPathAvailable =
|
||||
currentCommandPath !== undefined && currentCommandPath.length > 0;
|
||||
const nodeScript = command.length === 3 && command[2] === "mcp";
|
||||
if (nodeScript) {
|
||||
if (
|
||||
!isAbsolute(command[0] ?? "") ||
|
||||
!isAbsolute(command[1] ?? "") ||
|
||||
!currentPathAvailable
|
||||
)
|
||||
return "unknown";
|
||||
return isOwnedClientRegistrationCommand(command, currentCommandPath)
|
||||
? "owned"
|
||||
: "foreign";
|
||||
}
|
||||
if (command.length === 2 && command[1] === "mcp") {
|
||||
const executable = command[0] ?? "";
|
||||
if (executable === PRODUCT_IDENTITY.cliBinary) return "owned";
|
||||
const relativePathLike =
|
||||
!isAbsolute(executable) && /[./\\]/u.test(executable);
|
||||
if (relativePathLike || (isAbsolute(executable) && !currentPathAvailable))
|
||||
return "unknown";
|
||||
}
|
||||
if (isOwnedClientRegistrationCommand(command, currentCommandPath))
|
||||
return "owned";
|
||||
return "foreign";
|
||||
};
|
||||
|
||||
export const isOwnedClientRegistrationCommand = (
|
||||
command: readonly string[],
|
||||
currentCommandPath: string = resolve(process.argv[1] ?? "unknown"),
|
||||
currentCommandPath: string | undefined,
|
||||
): boolean => {
|
||||
if (
|
||||
currentCommandPath !== undefined &&
|
||||
currentCommandPath.length > 0 &&
|
||||
command.length === 3 &&
|
||||
command[2] === "mcp" &&
|
||||
isAbsolute(command[0] ?? "") &&
|
||||
isAbsolute(command[1] ?? "") &&
|
||||
resolve(command[0] ?? "") === resolve(process.execPath) &&
|
||||
resolve(command[1] ?? "") === currentCommandPath
|
||||
)
|
||||
@@ -129,8 +172,9 @@ export const isOwnedClientRegistrationCommand = (
|
||||
if (
|
||||
executable === PRODUCT_IDENTITY.cliBinary ||
|
||||
(isAbsolute(executable) &&
|
||||
basename(executable) === PRODUCT_IDENTITY.cliBinary) ||
|
||||
resolve(executable) === currentCommandPath
|
||||
currentCommandPath !== undefined &&
|
||||
currentCommandPath.length > 0 &&
|
||||
resolve(executable) === currentCommandPath)
|
||||
)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,16 +7,17 @@ import {
|
||||
type ClientConfigurationDocument,
|
||||
} from "./ClientConfigurationDocument.js";
|
||||
import { access } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { isAbsolute, resolve } from "node:path";
|
||||
|
||||
import { z } from "zod";
|
||||
import { valid } from "semver";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
|
||||
import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js";
|
||||
import {
|
||||
clientRegistrationPackageReference,
|
||||
isOwnedClientRegistrationCommand,
|
||||
classifyClientRegistrationCommand,
|
||||
packageRunnerRegistrationCommand,
|
||||
} from "./ClientRegistrationIdentity.js";
|
||||
import {
|
||||
@@ -50,6 +51,11 @@ export type ClientRegistrationStatus = ClientRegistrationStatusBase &
|
||||
readonly state: "stale";
|
||||
readonly remediation: string;
|
||||
}
|
||||
| {
|
||||
readonly command: RegistrationCommand;
|
||||
readonly state: "unknown";
|
||||
readonly remediation: string;
|
||||
}
|
||||
| {
|
||||
readonly command: readonly [];
|
||||
readonly state: "missing" | "invalid" | "manual";
|
||||
@@ -93,7 +99,7 @@ export interface ClientRegistrationInspection {
|
||||
/** Project diagnostics from the same inspection used to select maintenance. */
|
||||
export const readClientRegistrationStatuses = async (
|
||||
home: string,
|
||||
currentCommandPath: string = resolve(process.argv[1] ?? "unknown"),
|
||||
currentCommandPath: string | undefined = CURRENT_ENTRYPOINT_PATH,
|
||||
options: ClientRegistrationReadOptions = {},
|
||||
): Promise<readonly ClientRegistrationStatus[]> =>
|
||||
(
|
||||
@@ -103,7 +109,7 @@ export const readClientRegistrationStatuses = async (
|
||||
/** Parse effective registrations and their client-specific activation once. */
|
||||
export const readClientRegistrationInspections = async (
|
||||
home: string,
|
||||
currentCommandPath: string = resolve(process.argv[1] ?? "unknown"),
|
||||
currentCommandPath: string | undefined = CURRENT_ENTRYPOINT_PATH,
|
||||
options: ClientRegistrationReadOptions = {},
|
||||
): Promise<readonly ClientRegistrationInspection[]> => {
|
||||
const inspections: ClientRegistrationInspection[] = [];
|
||||
@@ -219,26 +225,32 @@ export const readClientRegistrationInspections = async (
|
||||
const packageVersionDrift =
|
||||
packageVersion !== undefined &&
|
||||
packageVersion !== PRODUCT_IDENTITY.packageVersion;
|
||||
const alignment = registrationAligned(
|
||||
registration,
|
||||
effectiveClient,
|
||||
currentCommandPath,
|
||||
options.platform ?? process.platform,
|
||||
);
|
||||
record(
|
||||
configuredStatus(
|
||||
effectiveClient,
|
||||
command,
|
||||
activation === "enabled" &&
|
||||
!packageVersionDrift &&
|
||||
registrationAligned(
|
||||
registration,
|
||||
effectiveClient,
|
||||
currentCommandPath,
|
||||
options.platform ?? process.platform,
|
||||
) &&
|
||||
(parsed.dialect !== "dsh" || dshDocument(parsed.document).ready) &&
|
||||
policyBlock === undefined
|
||||
? "aligned"
|
||||
: "stale",
|
||||
alignment === "unknown"
|
||||
? "unknown"
|
||||
: activation === "enabled" &&
|
||||
!packageVersionDrift &&
|
||||
alignment === "aligned" &&
|
||||
(parsed.dialect !== "dsh" ||
|
||||
dshDocument(parsed.document).ready) &&
|
||||
policyBlock === undefined
|
||||
? "aligned"
|
||||
: "stale",
|
||||
policyBlock ??
|
||||
(packageVersionDrift
|
||||
? packageVersionRemediation(packageReference ?? "", client)
|
||||
: undefined),
|
||||
: alignment === "unknown"
|
||||
? unknownEntryPointRemediation()
|
||||
: undefined),
|
||||
packageReference,
|
||||
),
|
||||
activation,
|
||||
@@ -275,25 +287,29 @@ const registrationActivation = (
|
||||
const registrationAligned = (
|
||||
registration: z.output<typeof registrationSchema>,
|
||||
client: SetupClient,
|
||||
currentCommandPath: string,
|
||||
currentCommandPath: string | undefined,
|
||||
platform: NodeJS.Platform,
|
||||
): boolean => {
|
||||
): "aligned" | "stale" | "unknown" => {
|
||||
const command = [registration.command, ...registration.args];
|
||||
if (!isOwnedClientRegistrationCommand(command, currentCommandPath))
|
||||
return false;
|
||||
const identity = classifyClientRegistrationCommand(
|
||||
command,
|
||||
currentCommandPath,
|
||||
);
|
||||
if (identity === "unknown") return "unknown";
|
||||
if (identity === "foreign") return "stale";
|
||||
if (
|
||||
(client.name === "codex" || client.name === "grok_build") &&
|
||||
registration.startup_timeout_sec !==
|
||||
MCP_STARTUP_POLICY.codexStartupTimeoutSeconds
|
||||
)
|
||||
return false;
|
||||
return "stale";
|
||||
if (
|
||||
client.name === "kimi" &&
|
||||
(registration.startupTimeoutMs !==
|
||||
MCP_STARTUP_POLICY.kimiStartupTimeoutMs ||
|
||||
registration.toolTimeoutMs !== MCP_STARTUP_POLICY.kimiToolTimeoutMs)
|
||||
)
|
||||
return false;
|
||||
return "stale";
|
||||
if (
|
||||
(client.format === "kimi" ||
|
||||
client.format === "omp" ||
|
||||
@@ -301,45 +317,55 @@ const registrationAligned = (
|
||||
registration.type !== undefined &&
|
||||
registration.type !== "stdio"
|
||||
)
|
||||
return false;
|
||||
if (client.format === "vscode" && registration.type !== "stdio") return false;
|
||||
return "stale";
|
||||
if (client.format === "vscode" && registration.type !== "stdio")
|
||||
return "stale";
|
||||
// A Harness registration only connects as the rea stdio server; another
|
||||
// server name or transport leaves the session without REA tools.
|
||||
if (
|
||||
client.format === "dsh" &&
|
||||
(registration.serverName !== "rea" || registration.transport !== "stdio")
|
||||
)
|
||||
return false;
|
||||
return "stale";
|
||||
if (
|
||||
client.format === "copilot_cli" &&
|
||||
(registration.type !== "stdio" ||
|
||||
JSON.stringify(registration.tools) !== JSON.stringify(["*"]))
|
||||
)
|
||||
return false;
|
||||
return "stale";
|
||||
if (client.format === "commandcode" && registration.transport !== "stdio")
|
||||
return false;
|
||||
if (
|
||||
command.length === 3 &&
|
||||
command[2] === "mcp" &&
|
||||
resolve(command[0] ?? "") === resolve(process.execPath) &&
|
||||
resolve(command[1] ?? "") === currentCommandPath
|
||||
)
|
||||
return true;
|
||||
return "stale";
|
||||
if (
|
||||
(["npx", "bunx"] as const).some((runner) => {
|
||||
const runnerCommand = packageRunnerRegistrationCommand(runner, platform);
|
||||
const expected = packageRunnerRegistrationCommand(runner, platform);
|
||||
return (
|
||||
command.length === runnerCommand.length &&
|
||||
command.every((argument, index) => argument === runnerCommand[index])
|
||||
command.length === expected.length &&
|
||||
command.every((argument, index) => argument === expected[index])
|
||||
);
|
||||
})
|
||||
)
|
||||
return true;
|
||||
return (
|
||||
return "aligned";
|
||||
if (
|
||||
currentCommandPath !== undefined &&
|
||||
currentCommandPath.length > 0 &&
|
||||
command.length === 3 &&
|
||||
command[2] === "mcp" &&
|
||||
isAbsolute(command[0] ?? "") &&
|
||||
isAbsolute(command[1] ?? "") &&
|
||||
resolve(command[0] ?? "") === resolve(process.execPath) &&
|
||||
resolve(command[1] ?? "") === currentCommandPath
|
||||
)
|
||||
return "aligned";
|
||||
if (
|
||||
currentCommandPath !== undefined &&
|
||||
currentCommandPath.length > 0 &&
|
||||
command.length === 2 &&
|
||||
command[1] === "mcp" &&
|
||||
isAbsolute(command[0] ?? "") &&
|
||||
resolve(command[0] ?? "") === currentCommandPath
|
||||
);
|
||||
)
|
||||
return "aligned";
|
||||
return "stale";
|
||||
};
|
||||
|
||||
const parseRegistration = (
|
||||
@@ -400,10 +426,13 @@ const remediation = (client: SetupClient): string => {
|
||||
return `Quit ${name} fully, run rea setup to refresh this registration, then start ${name} again. A running ${name} can rewrite its configuration from memory and drop the registration.`;
|
||||
};
|
||||
|
||||
const unknownEntryPointRemediation = (): string =>
|
||||
"The existing registration was retained because its executable identity cannot be verified. Run the installed REA CLI or a supported package runner, then rerun rea doctor; no registration was changed.";
|
||||
|
||||
const configuredStatus = (
|
||||
client: SetupClient,
|
||||
command: RegistrationCommand,
|
||||
state: "aligned" | "stale",
|
||||
state: "aligned" | "stale" | "unknown",
|
||||
policyRemediation?: string,
|
||||
packageReference?: string,
|
||||
): ClientRegistrationStatus =>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js
|
||||
import { providerCapabilitySet } from "../contracts/toolOutputSchemaPrimitives.js";
|
||||
import { providerCapabilitySets } from "./binary/BinarySessionStatus.js";
|
||||
import { sqliteDatabaseCapability } from "./sqlite/SqliteDatabaseCapability.js";
|
||||
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
|
||||
|
||||
/** Read target-free provider status and operation availability. */
|
||||
export const runSessionStatus = async (
|
||||
@@ -31,6 +32,7 @@ export const runSessionStatus = async (
|
||||
],
|
||||
server_identity: createServerIdentity({
|
||||
startedAt: new Date().toISOString(),
|
||||
commandPath: CURRENT_ENTRYPOINT_PATH,
|
||||
}),
|
||||
};
|
||||
} finally {
|
||||
|
||||
@@ -330,7 +330,7 @@ describe("doctor scoped readiness", () => {
|
||||
client: "cursor",
|
||||
config_path: "/home/user/.cursor/mcp.json",
|
||||
command: ["/old/rea", "mcp"],
|
||||
state: "stale",
|
||||
state: "unknown",
|
||||
remediation: "Run rea setup to refresh this registration.",
|
||||
},
|
||||
];
|
||||
@@ -349,7 +349,11 @@ describe("doctor scoped readiness", () => {
|
||||
expect.objectContaining({ name: "registration:codex", ok: true }),
|
||||
);
|
||||
expect(scoped.informational_checks).toContainEqual(
|
||||
expect.objectContaining({ name: "registration:cursor", ok: false }),
|
||||
expect.objectContaining({
|
||||
name: "registration:cursor",
|
||||
ok: false,
|
||||
classification: "unknown",
|
||||
}),
|
||||
);
|
||||
|
||||
const staleSelected = await runDoctor(
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface DoctorCheck {
|
||||
| "unsupported_host"
|
||||
| "unsupported_target"
|
||||
| "missing_analysis_engine"
|
||||
| "unknown"
|
||||
| "config_drift";
|
||||
readonly detail?: string;
|
||||
readonly details?: Readonly<Record<string, JsonValue>>;
|
||||
@@ -385,7 +386,7 @@ const registrationCheck = (
|
||||
): DoctorCheck => ({
|
||||
name: `registration:${registration.client}`,
|
||||
ok: false,
|
||||
classification: "config_drift",
|
||||
classification: registration.state === "unknown" ? "unknown" : "config_drift",
|
||||
detail: registration.config_path,
|
||||
remediation: registration.remediation,
|
||||
});
|
||||
|
||||
@@ -174,7 +174,7 @@ const registrationCheck = (
|
||||
): DoctorCheck => ({
|
||||
name: `registration:${status.client}`,
|
||||
ok: false,
|
||||
classification: "config_drift",
|
||||
classification: status.state === "unknown" ? "unknown" : "config_drift",
|
||||
detail: status.config_path,
|
||||
remediation: status.remediation,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { join, resolve } from "node:path";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { FakeSetupHost, options } from "./Setup.fixture.js";
|
||||
import { runSetup } from "./Setup.js";
|
||||
import { setupRegistrationCommand } from "./SetupHost.js";
|
||||
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
|
||||
import { discoverSetupState } from "./SetupPlan.js";
|
||||
import type { MsixPackageView } from "./MsixPackageStorage.js";
|
||||
import type { SetupClient } from "./SupportedClients.js";
|
||||
@@ -333,13 +334,13 @@ describe("setup workflow action selection", () => {
|
||||
|
||||
expect(result.status).toBe("ready");
|
||||
expect(host.configuredCommands).toEqual([
|
||||
setupRegistrationCommand("win32", undefined),
|
||||
]);
|
||||
expect(setupRegistrationCommand("win32", undefined)).toEqual([
|
||||
process.execPath,
|
||||
resolve(process.argv[1] ?? "rea"),
|
||||
"mcp",
|
||||
setupRegistrationCommand("win32", undefined, CURRENT_ENTRYPOINT_PATH),
|
||||
]);
|
||||
expect(
|
||||
setupRegistrationCommand("win32", undefined, CURRENT_ENTRYPOINT_PATH),
|
||||
).toEqual([process.execPath, CURRENT_ENTRYPOINT_PATH, "mcp"]);
|
||||
expect(setupRegistrationCommand("win32", undefined, undefined)).toEqual([]);
|
||||
expect(setupRegistrationCommand("win32", "npx", undefined)).not.toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -16,6 +16,7 @@ import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js";
|
||||
import { CATALOG_IDENTITY } from "../catalogIdentity.js";
|
||||
import type { ClientRegistrationStatus } from "./ClientRegistrationStatus.js";
|
||||
import { setupRegistrationCommand } from "./SetupHost.js";
|
||||
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
|
||||
|
||||
/** Recording setup host for service-level planning and recovery tests. */
|
||||
export class FakeSetupHost implements SetupHost {
|
||||
@@ -24,7 +25,7 @@ export class FakeSetupHost implements SetupHost {
|
||||
skillDestinations(clientIds: readonly string[]) {
|
||||
return skillDestinations(this.homeDirectory, clientIds);
|
||||
}
|
||||
readonly registrationCommand: readonly string[];
|
||||
registrationCommand: readonly string[];
|
||||
registrationCommandBlocker?: string;
|
||||
nodeVersion = "24.18.0";
|
||||
version: string | undefined = "14.5";
|
||||
@@ -57,7 +58,11 @@ export class FakeSetupHost implements SetupHost {
|
||||
|
||||
constructor(platform: NodeJS.Platform = "darwin") {
|
||||
this.platform = platform;
|
||||
this.registrationCommand = setupRegistrationCommand(platform, undefined);
|
||||
this.registrationCommand = setupRegistrationCommand(
|
||||
platform,
|
||||
undefined,
|
||||
CURRENT_ENTRYPOINT_PATH,
|
||||
);
|
||||
}
|
||||
|
||||
macosVersion = (): Promise<string | undefined> =>
|
||||
|
||||
@@ -136,6 +136,55 @@ describe("skill plan destinations", () => {
|
||||
});
|
||||
|
||||
describe("setup workflow", () => {
|
||||
it("blocks client registration when no executable entrypoint is available", async () => {
|
||||
const host = new FakeSetupHost("win32");
|
||||
host.registrationCommand = [];
|
||||
host.registrationCommandBlocker =
|
||||
"REA could not determine the current executable entrypoint.";
|
||||
host.clients = [{ name: "codex", configPath: "C:\\config\\codex.toml" }];
|
||||
|
||||
const result = await runSetup(
|
||||
{ ...options(true), clientIds: ["codex"], installSkill: false },
|
||||
host,
|
||||
);
|
||||
|
||||
expect(result.status).toBe("needs_human");
|
||||
expect(result.plannedActions).toEqual([]);
|
||||
expect(result.remediation).toBe(host.registrationCommandBlocker);
|
||||
expect(host.configurations).toBe(0);
|
||||
});
|
||||
|
||||
it("keeps a relative Node registration out of setup defaults when cwd happens to match", async () => {
|
||||
const host = new FakeSetupHost();
|
||||
host.clients = [{ name: "cursor", configPath: "/cursor.json" }];
|
||||
host.productRegistrations = [
|
||||
{
|
||||
client: "cursor",
|
||||
config_path: "/cursor.json",
|
||||
command: [process.execPath, "./rea.mjs", "mcp"],
|
||||
state: "stale",
|
||||
remediation: "Run rea setup to refresh this registration.",
|
||||
},
|
||||
];
|
||||
|
||||
const result = await runSetup(
|
||||
{ ...options(false), clientIds: [], installSkill: false },
|
||||
host,
|
||||
);
|
||||
|
||||
expect(result.clientStates).toContainEqual(
|
||||
expect.objectContaining({
|
||||
client: expect.objectContaining({ name: "cursor" }),
|
||||
status: "unknown",
|
||||
configured: false,
|
||||
}),
|
||||
);
|
||||
expect(result.plannedActions).toEqual([]);
|
||||
expect(host.configurations).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setup plan application", () => {
|
||||
it("returns a complete plan without mutation", async () => {
|
||||
const host = new FakeSetupHost();
|
||||
host.clients = [{ name: "cursor", configPath: "/cursor.json" }];
|
||||
|
||||
@@ -56,21 +56,21 @@ import type {
|
||||
SetupProviderEnvironment,
|
||||
} from "./SetupTypes.js";
|
||||
import type { DoctorScope } from "./Doctor.js";
|
||||
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
|
||||
|
||||
/** Resolve the executable and arguments used in a managed MCP registration. */
|
||||
export const setupRegistrationCommand = (
|
||||
platform: NodeJS.Platform,
|
||||
packageRunner: PackageRunner | undefined,
|
||||
): readonly string[] =>
|
||||
packageRunner !== undefined
|
||||
? packageRunnerRegistrationCommand(packageRunner, platform)
|
||||
: platform === "win32"
|
||||
? [
|
||||
process.execPath,
|
||||
resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary),
|
||||
"mcp",
|
||||
]
|
||||
: [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
|
||||
entrypointPath: string | undefined,
|
||||
): readonly string[] => {
|
||||
if (packageRunner !== undefined)
|
||||
return packageRunnerRegistrationCommand(packageRunner, platform);
|
||||
if (entrypointPath === undefined || entrypointPath.length === 0) return [];
|
||||
return platform === "win32"
|
||||
? [process.execPath, entrypointPath, "mcp"]
|
||||
: [entrypointPath, "mcp"];
|
||||
};
|
||||
|
||||
/**
|
||||
* The launcher setup registers, and why it must not be registered when it
|
||||
@@ -84,14 +84,17 @@ const setupRegistration = (
|
||||
const registrationCommand = setupRegistrationCommand(
|
||||
platform,
|
||||
invokingPackageRunner(environment, platform),
|
||||
CURRENT_ENTRYPOINT_PATH,
|
||||
);
|
||||
const registrationCommandBlocker =
|
||||
platform === "win32"
|
||||
? packagePrivateLauncherBlocker(
|
||||
registrationCommand,
|
||||
msixPackagesDirectory(homeDirectory, environment),
|
||||
)
|
||||
: undefined;
|
||||
registrationCommand.length === 0
|
||||
? "REA could not determine the current executable entrypoint. Run setup through the installed REA CLI or a supported package runner before configuring clients."
|
||||
: platform === "win32"
|
||||
? packagePrivateLauncherBlocker(
|
||||
registrationCommand,
|
||||
msixPackagesDirectory(homeDirectory, environment),
|
||||
)
|
||||
: undefined;
|
||||
return registrationCommandBlocker === undefined
|
||||
? { registrationCommand }
|
||||
: { registrationCommand, registrationCommandBlocker };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
import { isOwnedClientRegistrationCommand } from "./ClientRegistrationIdentity.js";
|
||||
import { classifyClientRegistrationCommand } from "./ClientRegistrationIdentity.js";
|
||||
import {
|
||||
linuxHopperInstallDisclosure,
|
||||
type LinuxPackageFamily,
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { redirectedWriteBlocker } from "./MsixPackageStorage.js";
|
||||
import type { DoctorReport } from "./Doctor.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
|
||||
|
||||
/** Read-only setup facts shared by client selection and preflight. */
|
||||
export interface SetupDiscovery {
|
||||
@@ -78,20 +79,29 @@ export const discoverSetupState = async (input: {
|
||||
);
|
||||
const clientStates = clients.map((client): SetupClientState => {
|
||||
const registration = registrationByClient.get(client.name);
|
||||
const commandIdentity =
|
||||
registration === undefined
|
||||
? undefined
|
||||
: classifyClientRegistrationCommand(
|
||||
registration.command,
|
||||
CURRENT_ENTRYPOINT_PATH,
|
||||
);
|
||||
const ownedRegistration =
|
||||
(registration?.state === "aligned" || registration?.state === "stale") &&
|
||||
isOwnedClientRegistrationCommand(registration.command);
|
||||
commandIdentity === "owned";
|
||||
const configured = ownedRegistration;
|
||||
const status: SetupClientState["status"] =
|
||||
registration?.state === "invalid"
|
||||
? "invalid"
|
||||
: ownedRegistration && registration.state === "aligned"
|
||||
? "configured"
|
||||
: ownedRegistration && registration.state === "stale"
|
||||
? "needs_configuration"
|
||||
: detectedIds.has(client.name)
|
||||
: registration?.state === "unknown" || commandIdentity === "unknown"
|
||||
? "unknown"
|
||||
: ownedRegistration && registration.state === "aligned"
|
||||
? "configured"
|
||||
: ownedRegistration && registration.state === "stale"
|
||||
? "needs_configuration"
|
||||
: "missing";
|
||||
: detectedIds.has(client.name)
|
||||
? "needs_configuration"
|
||||
: "missing";
|
||||
return {
|
||||
client,
|
||||
detected: detectedIds.has(client.name),
|
||||
@@ -103,7 +113,10 @@ export const discoverSetupState = async (input: {
|
||||
.filter(
|
||||
(registration) =>
|
||||
(registration.state === "aligned" || registration.state === "stale") &&
|
||||
isOwnedClientRegistrationCommand(registration.command),
|
||||
classifyClientRegistrationCommand(
|
||||
registration.command,
|
||||
CURRENT_ENTRYPOINT_PATH,
|
||||
) === "owned",
|
||||
)
|
||||
.map(({ client }) => client);
|
||||
return {
|
||||
@@ -150,6 +163,16 @@ export const planSetupActions = async (input: {
|
||||
const client = byId.get(id);
|
||||
return client === undefined ? [] : [client];
|
||||
});
|
||||
if (
|
||||
selectedClients.length > 0 &&
|
||||
input.host.registrationCommandBlocker !== undefined
|
||||
)
|
||||
return {
|
||||
plannedActions: [],
|
||||
selectedClients,
|
||||
clientsToConfigure: [],
|
||||
blocker: input.host.registrationCommandBlocker,
|
||||
};
|
||||
const inspections = await Promise.all(
|
||||
selectedClients.map(async (client) => ({
|
||||
client,
|
||||
|
||||
@@ -67,7 +67,7 @@ export interface SetupHost {
|
||||
readonly homeDirectory: string;
|
||||
skillDestinations(clientIds: readonly string[]): readonly SkillDestination[];
|
||||
readonly registrationCommand: readonly string[];
|
||||
/** Why no client may be registered with `registrationCommand` from this host. */
|
||||
/** Why client registration cannot proceed from this host. */
|
||||
readonly registrationCommandBlocker?: string;
|
||||
readonly nodeVersion: string;
|
||||
macosVersion(): Promise<string | undefined>;
|
||||
@@ -157,7 +157,12 @@ export interface SetupClientState {
|
||||
readonly client: SetupClient;
|
||||
readonly detected: boolean;
|
||||
readonly configured: boolean;
|
||||
readonly status: "configured" | "missing" | "needs_configuration" | "invalid";
|
||||
readonly status:
|
||||
| "configured"
|
||||
| "missing"
|
||||
| "needs_configuration"
|
||||
| "invalid"
|
||||
| "unknown";
|
||||
}
|
||||
|
||||
/** One concrete setup mutation disclosed before approval. */
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
skillDestinations,
|
||||
} from "./SetupSkill.js";
|
||||
import { isOwnedClientRegistrationCommand } from "./ClientRegistrationIdentity.js";
|
||||
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
|
||||
import { dshDocument } from "./DshCordisPatch.js";
|
||||
import { resolveClientConfigTransactionPath } from "./ClientConfigPath.js";
|
||||
import {
|
||||
@@ -562,12 +563,15 @@ const isOwnedRegistration = (value: unknown, client: SetupClient): boolean => {
|
||||
.safeParse(value);
|
||||
if (!parsed.success) return false;
|
||||
const command = parsed.data.command;
|
||||
return isOwnedClientRegistrationCommand(command);
|
||||
return isOwnedClientRegistrationCommand(command, CURRENT_ENTRYPOINT_PATH);
|
||||
}
|
||||
const parsed = registrationSchema.safeParse(value);
|
||||
if (!parsed.success) return false;
|
||||
const { command, args } = parsed.data;
|
||||
return isOwnedClientRegistrationCommand([command, ...args]);
|
||||
return isOwnedClientRegistrationCommand(
|
||||
[command, ...args],
|
||||
CURRENT_ENTRYPOINT_PATH,
|
||||
);
|
||||
};
|
||||
|
||||
const purgeManagedData = async (
|
||||
|
||||
+38
-14
@@ -10,6 +10,10 @@ import type { ToolContract } from "./contracts/toolContractTypes.js";
|
||||
import { CLI_COMMAND_NAMES } from "./cliCommandNames.js";
|
||||
import { compareUnicodeCodePoints } from "./domain/unicodeCodePointOrder.js";
|
||||
import { advertisedInputJsonSchema } from "./contracts/toolSchemaMetadata.js";
|
||||
import {
|
||||
presentAdvertisedInputSchema,
|
||||
type CompactInputSchemaPresentation,
|
||||
} from "./contracts/compactInputSchema.js";
|
||||
|
||||
const sortByName = (
|
||||
contracts: readonly ToolContract[],
|
||||
@@ -26,7 +30,10 @@ const sortedToolContracts = sortByName(TOOL_CONTRACTS);
|
||||
* that predates the untrusted-content framing must not digest equal to one
|
||||
* that emits `_meta.provenance`, or a stale process would report aligned.
|
||||
*/
|
||||
const createToolCatalog = (contracts: readonly ToolContract[]) =>
|
||||
const createToolCatalog = (
|
||||
contracts: readonly ToolContract[],
|
||||
compactPresentation: CompactInputSchemaPresentation | undefined,
|
||||
) =>
|
||||
contracts.map((contract) => ({
|
||||
name: contract.name,
|
||||
title: contract.title,
|
||||
@@ -35,7 +42,10 @@ const createToolCatalog = (contracts: readonly ToolContract[]) =>
|
||||
effects: { ...contract.effects },
|
||||
returns_target_content: contract.returnsTargetContent,
|
||||
annotations: contract.annotations,
|
||||
input_schema: advertisedInputJsonSchema(contract),
|
||||
input_schema: presentAdvertisedInputSchema(
|
||||
advertisedInputJsonSchema(contract),
|
||||
compactPresentation,
|
||||
),
|
||||
output_schema: {
|
||||
type: "object",
|
||||
...z.toJSONSchema(contract.outputSchema, {
|
||||
@@ -73,8 +83,9 @@ const digest = (value: unknown): string => {
|
||||
export const createCatalogDigests = (
|
||||
contracts: readonly ToolContract[] = TOOL_CONTRACTS,
|
||||
promptContracts: readonly PromptContract[] = PROMPT_CONTRACTS,
|
||||
compactPresentation: CompactInputSchemaPresentation | undefined = undefined,
|
||||
) => {
|
||||
const tools = createToolCatalog(sortByName(contracts));
|
||||
const tools = createToolCatalog(sortByName(contracts), compactPresentation);
|
||||
const prompts = createPromptCatalog(promptContracts);
|
||||
return {
|
||||
tools_sha256: digest(tools),
|
||||
@@ -87,16 +98,29 @@ export const createCatalogDigests = (
|
||||
} as const;
|
||||
};
|
||||
|
||||
let catalogDigests: ReturnType<typeof createCatalogDigests> | undefined;
|
||||
export const createCatalogIdentity = (
|
||||
compactPresentation: CompactInputSchemaPresentation | undefined = undefined,
|
||||
) => {
|
||||
const selectedPresentation =
|
||||
compactPresentation === undefined
|
||||
? undefined
|
||||
: { budgetBytes: compactPresentation.budgetBytes };
|
||||
let catalogDigests: ReturnType<typeof createCatalogDigests> | undefined;
|
||||
return {
|
||||
counts: {
|
||||
cli_commands: CLI_COMMAND_NAMES.length,
|
||||
mcp_tools: sortedToolContracts.length,
|
||||
mcp_prompts: promptCatalog.length,
|
||||
},
|
||||
get digests() {
|
||||
return (catalogDigests ??= createCatalogDigests(
|
||||
TOOL_CONTRACTS,
|
||||
PROMPT_CONTRACTS,
|
||||
selectedPresentation,
|
||||
));
|
||||
},
|
||||
} as const;
|
||||
};
|
||||
|
||||
/** Stable, schema-sensitive identity; schema digests are computed on first use. */
|
||||
export const CATALOG_IDENTITY = {
|
||||
counts: {
|
||||
cli_commands: CLI_COMMAND_NAMES.length,
|
||||
mcp_tools: sortedToolContracts.length,
|
||||
mcp_prompts: promptCatalog.length,
|
||||
},
|
||||
get digests() {
|
||||
return (catalogDigests ??= createCatalogDigests());
|
||||
},
|
||||
} as const;
|
||||
export const CATALOG_IDENTITY = createCatalogIdentity();
|
||||
|
||||
@@ -12,6 +12,14 @@ export const COMPACT_INPUT_SCHEMA_BUDGET_BYTES = 13 * 1024;
|
||||
|
||||
export const mcpInputSchemaProfileSchema = z.enum(["full", "compact"]);
|
||||
|
||||
/** Select the exact compact presentation budget shared by registration and identity. */
|
||||
export const compactPresentationForMcpInputSchemaProfile = (
|
||||
profile: "full" | "compact" | undefined,
|
||||
): { readonly budgetBytes: number } | undefined =>
|
||||
profile === "compact"
|
||||
? { budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES }
|
||||
: undefined;
|
||||
|
||||
const profileConstraint =
|
||||
'REA_MCP_INPUT_SCHEMA_PROFILE must be "full" (default) or "compact". The compact profile advertises input schemas without nested annotation prose for providers that cap each tool schema\'s serialized size.';
|
||||
|
||||
|
||||
@@ -26,6 +26,11 @@ const jsonValueTypes = [
|
||||
|
||||
export type AdvertisedInputSchemaPresentation = "inline" | "reduced";
|
||||
|
||||
/** Budget selected by the caller for compact MCP input-schema presentation. */
|
||||
export interface CompactInputSchemaPresentation {
|
||||
readonly budgetBytes: number;
|
||||
}
|
||||
|
||||
export interface CompactedInputSchema {
|
||||
readonly schema: Record<string, unknown>;
|
||||
readonly presentation: AdvertisedInputSchemaPresentation;
|
||||
@@ -335,3 +340,13 @@ export const compactAdvertisedInputSchema = (
|
||||
presentation: "reduced",
|
||||
};
|
||||
};
|
||||
|
||||
/** Return the JSON schema clients receive for the selected input presentation. */
|
||||
export const presentAdvertisedInputSchema = (
|
||||
schema: Record<string, unknown>,
|
||||
compactPresentation: CompactInputSchemaPresentation | undefined,
|
||||
): Record<string, unknown> =>
|
||||
compactPresentation === undefined
|
||||
? schema
|
||||
: compactAdvertisedInputSchema(schema, compactPresentation.budgetBytes)
|
||||
.schema;
|
||||
|
||||
@@ -505,7 +505,7 @@ export const sessionProvider = z
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
started_at: z.string(),
|
||||
command_path: z.string(),
|
||||
command_path: z.string().nullable(),
|
||||
}),
|
||||
sdk: z.object({
|
||||
server: z.string(),
|
||||
|
||||
+2
-2
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { snapshotEnvironment } from "./process/snapshotEnvironment.js";
|
||||
import { CURRENT_ENTRYPOINT_PATH } from "./process/CurrentEntrypointPath.js";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { realpathSync } from "node:fs";
|
||||
|
||||
@@ -122,7 +123,6 @@ const isDirectEntrypoint = (
|
||||
}
|
||||
};
|
||||
|
||||
const entryPath = process.argv[1];
|
||||
if (isDirectEntrypoint(entryPath, import.meta.url)) {
|
||||
if (isDirectEntrypoint(CURRENT_ENTRYPOINT_PATH, import.meta.url)) {
|
||||
void runEntrypoint();
|
||||
}
|
||||
|
||||
+23
-4
@@ -5,7 +5,11 @@ import { performance } from "node:perf_hooks";
|
||||
import { resolve } from "node:path";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
|
||||
import { CATALOG_IDENTITY } from "./catalogIdentity.js";
|
||||
import { createCatalogIdentity } from "./catalogIdentity.js";
|
||||
import {
|
||||
compactPresentationForMcpInputSchemaProfile,
|
||||
parseMcpInputSchemaProfile,
|
||||
} from "./config/mcpInputSchemaProfile.js";
|
||||
import { PROMPT_CONTRACTS } from "./contracts/promptContracts.js";
|
||||
import { TOOL_CONTRACTS } from "./contracts/toolContracts.js";
|
||||
import { err, ok } from "./domain/result.js";
|
||||
@@ -75,6 +79,7 @@ interface McpDoctorCheck {
|
||||
|
||||
/** Diagnose the actual production stdio adapter with one startup-bounded SDK session. */
|
||||
export const runProductionMcpDoctor = async (options: McpDoctorOptions) => {
|
||||
const catalog = catalogIdentityForEnvironment(options.environment);
|
||||
const deadline =
|
||||
performance.now() +
|
||||
(options.deadlineMs ?? MCP_STARTUP_POLICY.doctorDeadlineMs);
|
||||
@@ -104,6 +109,7 @@ export const runProductionMcpDoctor = async (options: McpDoctorOptions) => {
|
||||
client,
|
||||
deadline,
|
||||
controller.signal,
|
||||
catalog,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
result = {
|
||||
@@ -189,6 +195,7 @@ const inspectProductionMcpSession = async (
|
||||
client: Client,
|
||||
deadline: number,
|
||||
signal: AbortSignal,
|
||||
catalog: ReturnType<typeof createCatalogIdentity>,
|
||||
) => {
|
||||
const request = requestOptions(deadline, signal);
|
||||
const [tools, prompts] = await Promise.all([
|
||||
@@ -238,8 +245,7 @@ const inspectProductionMcpSession = async (
|
||||
name: "server-identity",
|
||||
ok:
|
||||
observedIdentity.packageVersion === PRODUCT_IDENTITY.packageVersion &&
|
||||
observedIdentity.catalogDigest ===
|
||||
CATALOG_IDENTITY.digests.combined_sha256,
|
||||
observedIdentity.catalogDigest === catalog.digests.combined_sha256,
|
||||
detail: `package=${observedIdentity.packageVersion ?? "missing"}; catalog=${observedIdentity.catalogDigest ?? "missing"}`,
|
||||
},
|
||||
{
|
||||
@@ -262,7 +268,7 @@ const inspectProductionMcpSession = async (
|
||||
identity: {
|
||||
expected_package_version: PRODUCT_IDENTITY.packageVersion,
|
||||
observed_package_version: observedIdentity.packageVersion,
|
||||
expected_catalog_digest: CATALOG_IDENTITY.digests.combined_sha256,
|
||||
expected_catalog_digest: catalog.digests.combined_sha256,
|
||||
observed_catalog_digest: observedIdentity.catalogDigest,
|
||||
},
|
||||
request_flow: {
|
||||
@@ -483,6 +489,19 @@ const targetFreeEnvironment = (
|
||||
return result;
|
||||
};
|
||||
|
||||
const catalogIdentityForEnvironment = (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): ReturnType<typeof createCatalogIdentity> => {
|
||||
const parsed = parseMcpInputSchemaProfile(
|
||||
environment.REA_MCP_INPUT_SCHEMA_PROFILE,
|
||||
);
|
||||
return createCatalogIdentity(
|
||||
compactPresentationForMcpInputSchemaProfile(
|
||||
parsed.ok ? parsed.value : undefined,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const captureStderr = () => {
|
||||
const retained = Buffer.allocUnsafe(MCP_STARTUP_POLICY.doctorStderrBytes);
|
||||
let retainedBytes = 0;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { resolve } from "node:path";
|
||||
|
||||
/** Absolute argv entrypoint, or undefined when the process did not supply one. */
|
||||
export const normalizeEntrypointPath = (
|
||||
entrypoint: string | undefined,
|
||||
): string | undefined =>
|
||||
entrypoint === undefined || entrypoint.length === 0
|
||||
? undefined
|
||||
: resolve(entrypoint);
|
||||
|
||||
export const CURRENT_ENTRYPOINT_PATH = normalizeEntrypointPath(process.argv[1]);
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
} from "@modelcontextprotocol/server";
|
||||
|
||||
import { analysisErrorProjectionSchema } from "../contracts/errorSchemas.js";
|
||||
import { compactAdvertisedInputSchema } from "../contracts/compactInputSchema.js";
|
||||
import {
|
||||
presentAdvertisedInputSchema,
|
||||
type CompactInputSchemaPresentation,
|
||||
} from "../contracts/compactInputSchema.js";
|
||||
import { CATALOG_IDENTITY, createCatalogIdentity } from "../catalogIdentity.js";
|
||||
import { transformAdvertisedInputJsonSchema } from "../contracts/toolSchemaMetadata.js";
|
||||
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
|
||||
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
|
||||
@@ -36,10 +40,7 @@ import {
|
||||
TARGET_CONTENT_RESULT_META,
|
||||
} from "./toolResultEncoding.js";
|
||||
|
||||
/** Compact advertised input schemas within this per-schema byte budget. */
|
||||
export interface CompactInputSchemaPresentation {
|
||||
readonly budgetBytes: number;
|
||||
}
|
||||
export type { CompactInputSchemaPresentation } from "../contracts/compactInputSchema.js";
|
||||
|
||||
const currentRequestSignal = new AsyncLocalStorage<AbortSignal>();
|
||||
|
||||
@@ -103,6 +104,11 @@ export const forgetPendingMcpRequest = (
|
||||
|
||||
/** Bind oversized MCP error recovery to this server's Evidence ledger. */
|
||||
export class EvidenceMcpServer extends McpServer {
|
||||
readonly catalogIdentity: ReturnType<typeof createCatalogIdentity>;
|
||||
private readonly compactPresentation:
|
||||
| CompactInputSchemaPresentation
|
||||
| undefined;
|
||||
|
||||
constructor(
|
||||
info: Implementation,
|
||||
options: McpServerOptions,
|
||||
@@ -110,12 +116,18 @@ export class EvidenceMcpServer extends McpServer {
|
||||
| EvidenceWriter["recordEvidence"]
|
||||
| undefined,
|
||||
readonly delivery: ToolResultDelivery,
|
||||
private readonly compactPresentation:
|
||||
| CompactInputSchemaPresentation
|
||||
| undefined = undefined,
|
||||
compactPresentation: CompactInputSchemaPresentation | undefined = undefined,
|
||||
private readonly omitOutputSchemas = false,
|
||||
) {
|
||||
super(info, options);
|
||||
this.compactPresentation =
|
||||
compactPresentation === undefined
|
||||
? undefined
|
||||
: { budgetBytes: compactPresentation.budgetBytes };
|
||||
this.catalogIdentity =
|
||||
this.compactPresentation === undefined
|
||||
? CATALOG_IDENTITY
|
||||
: createCatalogIdentity(this.compactPresentation);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,7 +164,7 @@ export class EvidenceMcpServer extends McpServer {
|
||||
typeof transformAdvertisedInputJsonSchema
|
||||
>[0],
|
||||
(projected) =>
|
||||
compactAdvertisedInputSchema(projected, budget).schema,
|
||||
presentAdvertisedInputSchema(projected, { budgetBytes: budget }),
|
||||
)
|
||||
: undefined;
|
||||
const registeredConfig =
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
import { parseMcpResponseBudget } from "../config/mcpResponseBudget.js";
|
||||
import { parseMcpOutputSchemaProfile } from "../config/mcpOutputSchemaProfile.js";
|
||||
import {
|
||||
COMPACT_INPUT_SCHEMA_BUDGET_BYTES,
|
||||
compactPresentationForMcpInputSchemaProfile,
|
||||
parseMcpInputSchemaProfile,
|
||||
} from "../config/mcpInputSchemaProfile.js";
|
||||
import type { CompactInputSchemaPresentation } from "./EvidenceMcpServer.js";
|
||||
@@ -612,9 +612,7 @@ const selectCompactInputSchemaPresentation = (
|
||||
environment.REA_MCP_INPUT_SCHEMA_PROFILE,
|
||||
);
|
||||
if (!configured.ok) throw configured.error;
|
||||
return configured.value === "compact"
|
||||
? { budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES }
|
||||
: undefined;
|
||||
return compactPresentationForMcpInputSchemaProfile(configured.value);
|
||||
};
|
||||
|
||||
const registerConfiguredAnalysisTools = (
|
||||
|
||||
@@ -18,6 +18,7 @@ import { mcpClientMetadata } from "./mcpClientMetadata.js";
|
||||
import type { AvailabilityPolicy } from "../application/CapabilityInventory.js";
|
||||
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
||||
import { err } from "../domain/result.js";
|
||||
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
|
||||
|
||||
/** Inputs required to register the binary session status tool. */
|
||||
export interface SessionStatusToolOptions {
|
||||
@@ -75,6 +76,8 @@ export const registerSessionStatusTool = (
|
||||
);
|
||||
const serverIdentity = createServerIdentity({
|
||||
startedAt,
|
||||
commandPath: CURRENT_ENTRYPOINT_PATH,
|
||||
catalog: server.catalogIdentity,
|
||||
expected: {
|
||||
...(input.expected_package_version === undefined
|
||||
? {}
|
||||
|
||||
+21
-9
@@ -1,7 +1,10 @@
|
||||
import { dirname, basename, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { CATALOG_IDENTITY } from "./catalogIdentity.js";
|
||||
import {
|
||||
CATALOG_IDENTITY,
|
||||
type createCatalogIdentity,
|
||||
} from "./catalogIdentity.js";
|
||||
import { PRODUCT_IDENTITY, SDK_IDENTITY } from "./identity.js";
|
||||
|
||||
/** Optional observations supplied by a client comparing its expected server. */
|
||||
@@ -27,11 +30,14 @@ const packageRoot = (): string => {
|
||||
/** Build one runtime identity without claiming alignment from absent observations. */
|
||||
export const createServerIdentity = (input: {
|
||||
readonly startedAt: string;
|
||||
readonly commandPath: string | undefined;
|
||||
readonly expected?: ExpectedServerIdentity;
|
||||
readonly client?: ConnectedClientIdentity;
|
||||
readonly protocolVersion?: string | undefined;
|
||||
readonly catalog?: ReturnType<typeof createCatalogIdentity>;
|
||||
}) => {
|
||||
const serverPath = resolve(process.argv[1] ?? "unknown");
|
||||
const catalog = input.catalog ?? CATALOG_IDENTITY;
|
||||
const serverPath = input.commandPath || undefined;
|
||||
const reasons: string[] = [];
|
||||
if (
|
||||
input.expected?.package_version !== undefined &&
|
||||
@@ -40,17 +46,20 @@ export const createServerIdentity = (input: {
|
||||
reasons.push("package_version_mismatch");
|
||||
if (
|
||||
input.expected?.catalog_digest !== undefined &&
|
||||
input.expected.catalog_digest !== CATALOG_IDENTITY.digests.combined_sha256
|
||||
input.expected.catalog_digest !== catalog.digests.combined_sha256
|
||||
)
|
||||
reasons.push("catalog_digest_mismatch");
|
||||
if (
|
||||
input.expected?.server_path !== undefined &&
|
||||
serverPath !== undefined &&
|
||||
resolve(input.expected.server_path) !== serverPath
|
||||
)
|
||||
reasons.push("registration_path_mismatch");
|
||||
const observedExpectation =
|
||||
input.expected !== undefined &&
|
||||
Object.values(input.expected).some((value) => value !== undefined);
|
||||
const pathComparable =
|
||||
input.expected?.server_path === undefined || serverPath !== undefined;
|
||||
return {
|
||||
package: {
|
||||
name: PRODUCT_IDENTITY.packageName,
|
||||
@@ -62,7 +71,7 @@ export const createServerIdentity = (input: {
|
||||
name: PRODUCT_IDENTITY.mcpServerKey,
|
||||
version: PRODUCT_IDENTITY.packageVersion,
|
||||
started_at: input.startedAt,
|
||||
command_path: serverPath,
|
||||
command_path: serverPath ?? null,
|
||||
},
|
||||
sdk: SDK_IDENTITY,
|
||||
negotiated_protocol_version: input.protocolVersion ?? null,
|
||||
@@ -75,8 +84,8 @@ export const createServerIdentity = (input: {
|
||||
expected_version: PRODUCT_IDENTITY.skillVersion,
|
||||
},
|
||||
catalog: {
|
||||
counts: CATALOG_IDENTITY.counts,
|
||||
digests: CATALOG_IDENTITY.digests,
|
||||
counts: catalog.counts,
|
||||
digests: catalog.digests,
|
||||
},
|
||||
protocol_features: {
|
||||
progress: true,
|
||||
@@ -87,16 +96,19 @@ export const createServerIdentity = (input: {
|
||||
state:
|
||||
reasons.length > 0
|
||||
? "mcp_server_restart_required"
|
||||
: observedExpectation
|
||||
: observedExpectation && pathComparable
|
||||
? "aligned"
|
||||
: "unknown",
|
||||
reasons,
|
||||
remediation:
|
||||
reasons.length > 0
|
||||
? "Restart the registered MCP server/client, then compare identity again."
|
||||
: observedExpectation
|
||||
: observedExpectation && pathComparable
|
||||
? null
|
||||
: "Supply expected package, catalog, or registration identity to compare the live server.",
|
||||
: input.expected?.server_path !== undefined &&
|
||||
serverPath === undefined
|
||||
? "The server entrypoint path is unavailable, so registration identity cannot be compared."
|
||||
: "Supply expected package, catalog, or registration identity to compare the live server.",
|
||||
},
|
||||
} as const;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { parse as parseJsonc, type ParseError } from "jsonc-parser";
|
||||
@@ -9,7 +9,14 @@ import { configureClientConfiguration } from "../../../src/application/SetupClie
|
||||
import { systemUninstallHost } from "../../../src/application/Uninstall.js";
|
||||
import { supportedClients } from "../../../src/application/SupportedClients.js";
|
||||
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
|
||||
import { isOwnedClientRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js";
|
||||
import {
|
||||
classifyClientRegistrationCommand,
|
||||
isOwnedClientRegistrationCommand,
|
||||
} from "../../../src/application/ClientRegistrationIdentity.js";
|
||||
import {
|
||||
CURRENT_ENTRYPOINT_PATH,
|
||||
normalizeEntrypointPath,
|
||||
} from "../../../src/process/CurrentEntrypointPath.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import {
|
||||
clearClientLocationEnvironment,
|
||||
@@ -37,6 +44,39 @@ const getClients = (home: string) =>
|
||||
);
|
||||
|
||||
describe("additional client configuration dialects", () => {
|
||||
it("does not uninstall a Node registration whose relative script only matches this process cwd", async () => {
|
||||
if (CURRENT_ENTRYPOINT_PATH === undefined)
|
||||
throw new Error("test runner entrypoint is unavailable");
|
||||
const home = await createTestTempDirectory("rea-relative-uninstall-");
|
||||
const cursor = supportedClients(home).find(({ name }) => name === "cursor");
|
||||
if (cursor === undefined) throw new Error("missing Cursor client");
|
||||
await mkdir(cursor.markerPath ?? home, { recursive: true });
|
||||
const relativeScript = relative(process.cwd(), CURRENT_ENTRYPOINT_PATH);
|
||||
const registration = {
|
||||
command: process.execPath,
|
||||
args: [relativeScript, "mcp"],
|
||||
};
|
||||
await writeFile(
|
||||
cursor.configPath,
|
||||
JSON.stringify({ mcpServers: { rea: registration } }),
|
||||
);
|
||||
|
||||
expect(
|
||||
await readClientRegistrationStatuses(home, CURRENT_ENTRYPOINT_PATH),
|
||||
).toContainEqual(
|
||||
expect.objectContaining({
|
||||
client: "cursor",
|
||||
command: [process.execPath, relativeScript, "mcp"],
|
||||
state: "unknown",
|
||||
}),
|
||||
);
|
||||
await systemUninstallHost(home).removeClient(cursor);
|
||||
const after = parseJsonc(await readFile(cursor.configPath, "utf8")) as {
|
||||
readonly mcpServers: Readonly<Record<string, unknown>>;
|
||||
};
|
||||
expect(after.mcpServers.rea).toEqual(registration);
|
||||
});
|
||||
|
||||
it("treats an empty XDG config home as unset for OpenCode setup", async () => {
|
||||
const home = await createTestTempDirectory("rea-opencode-empty-xdg-");
|
||||
const client = supportedClients(home, "linux", {
|
||||
@@ -130,7 +170,9 @@ describe("additional client configuration dialects", () => {
|
||||
],
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenCode JSONC registration edits", () => {
|
||||
it("preserves OpenCode JSONC comments and sibling registrations during update and removal", async () => {
|
||||
const home = await createTestTempDirectory("rea-opencode-jsonc-");
|
||||
const client = getClients(home).find(({ name }) => name === "opencode");
|
||||
@@ -489,28 +531,43 @@ describe("platform-aware client config paths", () => {
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("client registration command ownership", () => {
|
||||
it("recognizes prior exact REA releases while rejecting foreign package commands", () => {
|
||||
expect(isOwnedClientRegistrationCommand(["rea", "mcp"], undefined)).toBe(
|
||||
true,
|
||||
);
|
||||
const cwdMatchingEntry = resolve("./rea");
|
||||
expect(
|
||||
isOwnedClientRegistrationCommand([
|
||||
"npx",
|
||||
"-y",
|
||||
`${PRODUCT_IDENTITY.packageName}@3.2.0`,
|
||||
"mcp",
|
||||
]),
|
||||
).toBe(true);
|
||||
classifyClientRegistrationCommand(["./rea", "mcp"], cwdMatchingEntry),
|
||||
).toBe("unknown");
|
||||
expect(
|
||||
isOwnedClientRegistrationCommand(["npx", "-y", "another-package", "mcp"]),
|
||||
isOwnedClientRegistrationCommand(["./rea", "mcp"], cwdMatchingEntry),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isOwnedClientRegistrationCommand([
|
||||
"bunx",
|
||||
`${PRODUCT_IDENTITY.packageName}@3.2.0`,
|
||||
"mcp",
|
||||
]),
|
||||
isOwnedClientRegistrationCommand(
|
||||
["npx", "-y", `${PRODUCT_IDENTITY.packageName}@3.2.0`, "mcp"],
|
||||
undefined,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isOwnedClientRegistrationCommand(["bunx", "another-package", "mcp"]),
|
||||
isOwnedClientRegistrationCommand(
|
||||
["npx", "-y", "another-package", "mcp"],
|
||||
undefined,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isOwnedClientRegistrationCommand(
|
||||
["bunx", `${PRODUCT_IDENTITY.packageName}@3.2.0`, "mcp"],
|
||||
undefined,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isOwnedClientRegistrationCommand(
|
||||
["bunx", "another-package", "mcp"],
|
||||
undefined,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isOwnedClientRegistrationCommand(
|
||||
@@ -518,6 +575,12 @@ describe("platform-aware client config paths", () => {
|
||||
"/packaged/rea.mjs",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isOwnedClientRegistrationCommand(
|
||||
[process.execPath, resolve("unknown"), "mcp"],
|
||||
normalizeEntrypointPath(""),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isOwnedClientRegistrationCommand(
|
||||
["node", "/packaged/other-agent.mjs", "mcp"],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -112,7 +112,9 @@ describe("client registration status", () => {
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("client registration launcher identity", () => {
|
||||
it("reports an unversioned npx registration as stale", async () => {
|
||||
const home = await createTestTempDirectory("rea-registrations-");
|
||||
await mkdir(join(home, ".codex"));
|
||||
@@ -132,6 +134,96 @@ describe("client registration status", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports an unpinned bunx registration as stale rather than aligned", async () => {
|
||||
const home = await createTestTempDirectory("rea-bunx-unpinned-");
|
||||
await writeFile(
|
||||
join(home, ".claude.json"),
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
rea: { command: "bunx", args: ["rea-agents", "mcp"] },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const [status] = await readClientRegistrationStatuses(home);
|
||||
|
||||
expect(status).toMatchObject({
|
||||
client: "claude_code",
|
||||
command: ["bunx", "rea-agents", "mcp"],
|
||||
state: "stale",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves direct command identity as unknown when the current entrypoint is absent", async () => {
|
||||
const home = await createTestTempDirectory("rea-registration-unknown-");
|
||||
await mkdir(join(home, ".cursor"));
|
||||
const command = ["/opt/rea/bin/rea", "mcp"];
|
||||
await writeFile(
|
||||
join(home, ".cursor/mcp.json"),
|
||||
JSON.stringify({
|
||||
mcpServers: { rea: { command: command[0], args: [command[1]] } },
|
||||
}),
|
||||
);
|
||||
|
||||
const [status] = await readClientRegistrationStatuses(home, "");
|
||||
|
||||
expect(status).toMatchObject({
|
||||
client: "cursor",
|
||||
command,
|
||||
state: "unknown",
|
||||
remediation: expect.stringContaining("registration was retained"),
|
||||
});
|
||||
});
|
||||
|
||||
it("does not use the REA process cwd to resolve a client's relative launcher path", async () => {
|
||||
const home = await createTestTempDirectory("rea-registration-relative-");
|
||||
await mkdir(join(home, ".cursor"));
|
||||
const currentEntry = resolve("./rea.mjs");
|
||||
const configPath = join(home, ".cursor/mcp.json");
|
||||
const registration = (
|
||||
scriptPath: string,
|
||||
executable = process.execPath,
|
||||
) => ({
|
||||
mcpServers: {
|
||||
rea: { command: executable, args: [scriptPath, "mcp"] },
|
||||
},
|
||||
});
|
||||
await writeFile(configPath, JSON.stringify(registration("./rea.mjs")));
|
||||
|
||||
const [relativeStatus] = await readClientRegistrationStatuses(
|
||||
home,
|
||||
currentEntry,
|
||||
);
|
||||
expect(relativeStatus).toMatchObject({
|
||||
command: [process.execPath, "./rea.mjs", "mcp"],
|
||||
state: "unknown",
|
||||
});
|
||||
|
||||
await writeFile(configPath, JSON.stringify(registration(currentEntry)));
|
||||
const [absoluteStatus] = await readClientRegistrationStatuses(
|
||||
home,
|
||||
currentEntry,
|
||||
);
|
||||
expect(absoluteStatus).toMatchObject({
|
||||
command: [process.execPath, currentEntry, "mcp"],
|
||||
state: "aligned",
|
||||
});
|
||||
|
||||
const relativeInterpreter = relative(process.cwd(), process.execPath);
|
||||
await writeFile(
|
||||
configPath,
|
||||
JSON.stringify(registration(currentEntry, relativeInterpreter)),
|
||||
);
|
||||
const [relativeInterpreterStatus] = await readClientRegistrationStatuses(
|
||||
home,
|
||||
currentEntry,
|
||||
);
|
||||
expect(relativeInterpreterStatus).toMatchObject({
|
||||
command: [relativeInterpreter, currentEntry, "mcp"],
|
||||
state: "unknown",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports the exact requested package version when it differs from this CLI", async () => {
|
||||
const home = await createTestTempDirectory(
|
||||
"rea-registration-version-drift-",
|
||||
|
||||
@@ -178,7 +178,7 @@ describe("Kimi Code package launchers", () => {
|
||||
"keeps the %s launcher aligned on %s",
|
||||
async (runner, platform) => {
|
||||
const { home, client } = await kimiClient();
|
||||
const launcher = setupRegistrationCommand(platform, runner);
|
||||
const launcher = setupRegistrationCommand(platform, runner, undefined);
|
||||
expect(
|
||||
(await configureClientConfiguration(client, {}, launcher)).status,
|
||||
).toBe("configured");
|
||||
|
||||
@@ -56,7 +56,7 @@ it.each([
|
||||
expect.objectContaining({ client: "claude_code", state: "stale" }),
|
||||
);
|
||||
const client = { name: "claude_code", configPath, format: "json" } as const;
|
||||
const command = setupRegistrationCommand("win32", "npx");
|
||||
const command = setupRegistrationCommand("win32", "npx", undefined);
|
||||
expect(
|
||||
await configureClientConfiguration(client, {}, command),
|
||||
).toMatchObject({ status: "configured" });
|
||||
@@ -93,7 +93,7 @@ it.each(["npx", "bunx"] as const)(
|
||||
'\uFEFF# Keep this explanation.\nnotify = ["a", "b"]\nliteral = \'C:\\demo\\path\'\ndisabled_mcp_servers = ["rea"]\n\n[mcp_servers.other]\ncommand = "another-server"\n';
|
||||
await writeFile(configPath, original);
|
||||
const client = { name: "codex", configPath, format: "toml" } as const;
|
||||
const command = setupRegistrationCommand("win32", runner);
|
||||
const command = setupRegistrationCommand("win32", runner, undefined);
|
||||
expect(
|
||||
await configureClientConfiguration(client, {}, command),
|
||||
).toMatchObject({ status: "configured" });
|
||||
@@ -160,7 +160,7 @@ it.skipIf(process.platform !== "win32").each([
|
||||
configPath,
|
||||
format: "json",
|
||||
} as const;
|
||||
const command = setupRegistrationCommand("win32", runner);
|
||||
const command = setupRegistrationCommand("win32", runner, undefined);
|
||||
// The default writer and npm-run setup must produce the same native launcher.
|
||||
expect(
|
||||
await (runner === "npx"
|
||||
|
||||
@@ -4,6 +4,10 @@ import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
|
||||
import { PROMPT_CONTRACTS } from "../../../src/contracts/promptContracts.js";
|
||||
import { advertisedInputJsonSchema } from "../../../src/contracts/toolSchemaMetadata.js";
|
||||
import { presentAdvertisedInputSchema } from "../../../src/contracts/compactInputSchema.js";
|
||||
import { createCatalogDigests } from "../../../src/catalogIdentity.js";
|
||||
import {
|
||||
schemaArrays,
|
||||
schemaChildren,
|
||||
@@ -109,6 +113,68 @@ const fullServer = (): EvidenceMcpServer =>
|
||||
);
|
||||
|
||||
describe("MCP compact input schema profile", () => {
|
||||
it("matches profile digests to tools/list and excludes stripped annotations", async () => {
|
||||
const compactPresentation = {
|
||||
budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES,
|
||||
};
|
||||
const full = fullServer();
|
||||
const compact = compactServer();
|
||||
registerCatalog(full);
|
||||
registerCatalog(compact);
|
||||
const [fullTools, compactTools] = await Promise.all([
|
||||
listToolsThroughTransport(full),
|
||||
listToolsThroughTransport(compact),
|
||||
]);
|
||||
const toolsByName = [
|
||||
new Map(fullTools.map((tool) => [tool.name, tool])),
|
||||
new Map(compactTools.map((tool) => [tool.name, tool])),
|
||||
];
|
||||
for (const [index, presentation] of [
|
||||
undefined,
|
||||
compactPresentation,
|
||||
].entries()) {
|
||||
for (const contract of TOOL_CONTRACTS) {
|
||||
expect(toolsByName[index]?.get(contract.name)?.inputSchema).toEqual(
|
||||
presentAdvertisedInputSchema(
|
||||
advertisedInputJsonSchema(contract),
|
||||
presentation,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fullIdentity = createCatalogDigests();
|
||||
const compactIdentity = createCatalogDigests(
|
||||
TOOL_CONTRACTS,
|
||||
PROMPT_CONTRACTS,
|
||||
compactPresentation,
|
||||
);
|
||||
expect(full.catalogIdentity.digests).toEqual(fullIdentity);
|
||||
expect(compact.catalogIdentity.digests).toEqual(compactIdentity);
|
||||
expect(compactIdentity.tools_sha256).not.toBe(fullIdentity.tools_sha256);
|
||||
expect(
|
||||
createCatalogDigests(TOOL_CONTRACTS, PROMPT_CONTRACTS, {
|
||||
budgetBytes: 1,
|
||||
}).tools_sha256,
|
||||
).not.toBe(compactIdentity.tools_sha256);
|
||||
|
||||
const withoutExamples = TOOL_CONTRACTS.map((contract) =>
|
||||
contract.examples.length === 0 ? contract : { ...contract, examples: [] },
|
||||
);
|
||||
expect(createCatalogDigests(withoutExamples).tools_sha256).not.toBe(
|
||||
fullIdentity.tools_sha256,
|
||||
);
|
||||
expect(
|
||||
createCatalogDigests(
|
||||
withoutExamples,
|
||||
PROMPT_CONTRACTS,
|
||||
compactPresentation,
|
||||
).tools_sha256,
|
||||
).toBe(compactIdentity.tools_sha256);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MCP compact schema constraints", () => {
|
||||
it("advertises every tool input schema within the provider budget", async () => {
|
||||
const server = compactServer();
|
||||
registerCatalog(server);
|
||||
|
||||
@@ -40,34 +40,38 @@ describe("production MCP doctor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("connects to the production stdio child and verifies the canonical catalog", async () => {
|
||||
const result = await runProductionMcpDoctor({
|
||||
command: process.execPath,
|
||||
args: [resolve("scripts/rea.mjs"), "mcp"],
|
||||
cwd: process.cwd(),
|
||||
environment: process.env,
|
||||
});
|
||||
it.each(["full", "compact"] as const)(
|
||||
"connects to the production stdio child and verifies the %s catalog",
|
||||
async (profile) => {
|
||||
const result = await runProductionMcpDoctor({
|
||||
command: process.execPath,
|
||||
args: [resolve("scripts/rea.mjs"), "mcp"],
|
||||
cwd: process.cwd(),
|
||||
environment: { ...process.env, REA_MCP_INPUT_SCHEMA_PROFILE: profile },
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
healthy: true,
|
||||
adapter: "production-stdio",
|
||||
inventory: {
|
||||
tools: {
|
||||
expected: expect.any(Number),
|
||||
observed: expect.any(Number),
|
||||
missing: [],
|
||||
unexpected: [],
|
||||
expect(result).toMatchObject({
|
||||
healthy: true,
|
||||
adapter: "production-stdio",
|
||||
inventory: {
|
||||
tools: {
|
||||
expected: expect.any(Number),
|
||||
observed: expect.any(Number),
|
||||
missing: [],
|
||||
unexpected: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
request_flow: { tool: "binary_session", ok: true },
|
||||
});
|
||||
expect(result.inventory?.tools.expected).toBe(
|
||||
result.inventory?.tools.observed,
|
||||
);
|
||||
expect(result.inventory?.tools.expected).toBe(
|
||||
CATALOG_IDENTITY.counts.mcp_tools,
|
||||
);
|
||||
}, 30_000);
|
||||
request_flow: { tool: "binary_session", ok: true },
|
||||
});
|
||||
expect(result.inventory?.tools.expected).toBe(
|
||||
result.inventory?.tools.observed,
|
||||
);
|
||||
expect(result.inventory?.tools.expected).toBe(
|
||||
CATALOG_IDENTITY.counts.mcp_tools,
|
||||
);
|
||||
},
|
||||
30_000,
|
||||
);
|
||||
});
|
||||
|
||||
describe("production MCP doctor probe validation", () => {
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { PROMPT_CONTRACTS } from "../../../src/contracts/promptContracts.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { createServerIdentity } from "../../../src/serverIdentity.js";
|
||||
import { normalizeEntrypointPath } from "../../../src/process/CurrentEntrypointPath.js";
|
||||
import { observed } from "../../fixtures/analysisExecution.js";
|
||||
import { buildCapabilityInventory } from "../../../src/application/CapabilityInventory.js";
|
||||
import type {
|
||||
@@ -156,19 +157,23 @@ describe("server and catalog identity", () => {
|
||||
it("reports unknown without a live comparison and distinguishes aligned from stale", () => {
|
||||
const unknown = createServerIdentity({
|
||||
startedAt: "2026-07-13T00:00:00.000Z",
|
||||
commandPath: normalizeEntrypointPath(""),
|
||||
});
|
||||
expect(unknown.alignment.state).toBe("unknown");
|
||||
expect(unknown.server.command_path).toBeNull();
|
||||
const aligned = createServerIdentity({
|
||||
startedAt: "2026-07-13T00:00:00.000Z",
|
||||
commandPath: "/packaged/rea.mjs",
|
||||
expected: {
|
||||
package_version: PRODUCT_IDENTITY.packageVersion,
|
||||
catalog_digest: CATALOG_IDENTITY.digests.combined_sha256,
|
||||
server_path: process.argv[1] ?? "unknown",
|
||||
server_path: "/packaged/rea.mjs",
|
||||
},
|
||||
});
|
||||
expect(aligned.alignment).toMatchObject({ state: "aligned", reasons: [] });
|
||||
const stale = createServerIdentity({
|
||||
startedAt: "2026-07-13T00:00:00.000Z",
|
||||
commandPath: "/packaged/rea.mjs",
|
||||
expected: {
|
||||
package_version: "1.2.0",
|
||||
catalog_digest: "0".repeat(64),
|
||||
@@ -180,6 +185,23 @@ describe("server and catalog identity", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps registration alignment unknown when the server entrypoint is absent", () => {
|
||||
const identity = createServerIdentity({
|
||||
startedAt: "2026-07-13T00:00:00.000Z",
|
||||
commandPath: undefined,
|
||||
expected: { server_path: "/registered/rea.mjs" },
|
||||
});
|
||||
|
||||
expect(identity.server.command_path).toBeNull();
|
||||
expect(identity.alignment).toMatchObject({
|
||||
state: "unknown",
|
||||
reasons: [],
|
||||
remediation: expect.stringContaining("entrypoint path is unavailable"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("provider capability inventory", () => {
|
||||
it("reports composed, host, and target-specific availability truthfully", () => {
|
||||
const policy = {
|
||||
processCaptureEnabled: true,
|
||||
@@ -305,6 +327,48 @@ describe("live server identity over MCP", () => {
|
||||
]);
|
||||
}
|
||||
}, 10_000);
|
||||
|
||||
it("compares the caller's digest against the selected compact tools/list catalog", async () => {
|
||||
const session = createTestBinarySession(availabilityProvider());
|
||||
const server = createServer(
|
||||
{ kind: "session", session },
|
||||
{ environment: { REA_MCP_INPUT_SCHEMA_PROFILE: "compact" } },
|
||||
);
|
||||
const client = new Client({ name: "compact-identity-test", version: "1" });
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
try {
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const listed = await client.listTools();
|
||||
expect(listed.tools).toHaveLength(TOOL_CONTRACTS.length);
|
||||
|
||||
const response = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {
|
||||
expected_catalog_digest:
|
||||
server.catalogIdentity.digests.combined_sha256,
|
||||
},
|
||||
});
|
||||
const status = z
|
||||
.object({ result: sessionProvider })
|
||||
.parse(response.structuredContent).result;
|
||||
expect(status.server_identity.catalog).toEqual(server.catalogIdentity);
|
||||
expect(status.server_identity.alignment).toMatchObject({
|
||||
state: "aligned",
|
||||
reasons: [],
|
||||
});
|
||||
expect(server.catalogIdentity.digests.tools_sha256).not.toBe(
|
||||
CATALOG_IDENTITY.digests.tools_sha256,
|
||||
);
|
||||
} finally {
|
||||
await Promise.allSettled([
|
||||
client.close(),
|
||||
server.close(),
|
||||
session.close(),
|
||||
]);
|
||||
}
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
describe("active-target availability over MCP", () => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseMcpInputSchemaProfile } from "../../../src/config/mcpInputSchemaProfile.js";
|
||||
import {
|
||||
compactPresentationForMcpInputSchemaProfile,
|
||||
COMPACT_INPUT_SCHEMA_BUDGET_BYTES,
|
||||
parseMcpInputSchemaProfile,
|
||||
} from "../../../src/config/mcpInputSchemaProfile.js";
|
||||
|
||||
describe("MCP input schema profile configuration", () => {
|
||||
it("accepts the full and compact presentations", () => {
|
||||
@@ -26,4 +30,14 @@ describe("MCP input schema profile configuration", () => {
|
||||
expect(parsed.error.message).toContain("REA_MCP_INPUT_SCHEMA_PROFILE");
|
||||
},
|
||||
);
|
||||
|
||||
it("selects the same result-affecting budget for the compact profile", () => {
|
||||
expect(compactPresentationForMcpInputSchemaProfile(undefined)).toBe(
|
||||
undefined,
|
||||
);
|
||||
expect(compactPresentationForMcpInputSchemaProfile("full")).toBe(undefined);
|
||||
expect(compactPresentationForMcpInputSchemaProfile("compact")).toEqual({
|
||||
budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user