Skip to content

Commit cb0ade0

Browse files
Merge pull request #61 from voidstackloop/codex/release-validation-2026-08-31
Add FHIR R4, SMART on FHIR, HL7 v2, eval/drift monitoring, and clinic…
2 parents ff5eb46 + 4ff0b95 commit cb0ade0

121 files changed

Lines changed: 10079 additions & 98 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { modelVisibleClinicalSchema, prepareClinicalMcpArguments } from "./clinical-mcp-broker";
3+
import * as patientCases from "./patient-cases-store";
4+
import * as backend from "./shared-backend-client";
5+
6+
vi.mock("./patient-cases-store");
7+
vi.mock("./shared-backend-client");
8+
9+
const policy = { entryId: "10000000-0000-4000-8000-000000000001", organizationId: "10000000-0000-4000-8000-000000000002", allowedTools: "*" as const, dataEgressPolicy: "unrestricted" as const, integrationProfile: "modelforge-clinical" as const };
10+
11+
describe("clinical MCP broker", () => {
12+
beforeEach(() => vi.clearAllMocks());
13+
14+
it("removes infrastructure-only fields from model-visible tool schemas", () => {
15+
expect(modelVisibleClinicalSchema({ type: "object", properties: { contextGrantId: {}, approvalTicket: {}, idempotencyKey: {}, rationale: {} }, required: ["contextGrantId", "approvalTicket", "idempotencyKey", "rationale"] })).toEqual({ type: "object", properties: { rationale: {} }, required: ["rationale"] });
16+
});
17+
18+
it("hides authoritative medication fields without mutating the wire schema", () => {
19+
const schema = { type: "object", additionalProperties: false, properties: { medications: {}, allergies: {}, contextGrantId: {} }, required: ["medications", "allergies", "contextGrantId"] };
20+
expect(modelVisibleClinicalSchema(schema, "clinical.medication_conflict_check")).toEqual({ type: "object", additionalProperties: false, properties: {}, required: [] });
21+
expect(schema.required).toEqual(["medications", "allergies", "contextGrantId"]);
22+
expect(Object.keys(schema.properties)).toEqual(["medications", "allergies", "contextGrantId"]);
23+
expect(modelVisibleClinicalSchema(undefined, "clinical.medication_conflict_check")).toBeUndefined();
24+
});
25+
26+
it("does not remove similarly named domain fields from other tools", () => {
27+
const schema = { properties: { medications: {} }, required: ["medications"] };
28+
expect(modelVisibleClinicalSchema(schema, "clinical.response_contract_check")).toEqual(schema);
29+
});
30+
31+
it("rejects missing or excluded case data before requesting a grant", async () => {
32+
await expect(prepareClinicalMcpArguments(policy, "clinical.medication_conflict_check", {})).rejects.toThrow(/Attach a patient case/);
33+
vi.mocked(patientCases.getCase).mockResolvedValue(null);
34+
await expect(prepareClinicalMcpArguments(policy, "clinical.medication_conflict_check", {}, { patientCaseId: "case-1" })).rejects.toThrow(/no longer available/);
35+
vi.mocked(patientCases.getCase).mockResolvedValue({ medications: { includeInContext: false, value: [] }, allergies: { includeInContext: true, value: [] } } as never);
36+
await expect(prepareClinicalMcpArguments(policy, "clinical.medication_conflict_check", {}, { patientCaseId: "case-1" })).rejects.toThrow(/Include both/);
37+
expect(backend.createMcpContextGrant).not.toHaveBeenCalled();
38+
});
39+
40+
it("uses medications and allergies from the attached case and injects only the grant handle", async () => {
41+
vi.mocked(patientCases.getCase).mockResolvedValue({ medications: { includeInContext: true, value: ["warfarin"] }, allergies: { includeInContext: true, value: ["aspirin"] } } as never);
42+
vi.mocked(backend.createMcpContextGrant).mockResolvedValue({ id: "grant-1" } as never);
43+
await expect(prepareClinicalMcpArguments(policy, "clinical.medication_conflict_check", { medications: ["untrusted"] }, { patientCaseId: "case-1" })).resolves.toEqual({ medications: ["warfarin"], allergies: ["aspirin"], contextGrantId: "grant-1" });
44+
expect(backend.createMcpContextGrant).toHaveBeenCalledWith(expect.objectContaining({ caseId: "case-1", requestedFields: ["allergies", "medications"] }));
45+
});
46+
47+
it("requires a human-approved review and injects an operation-bound ticket and idempotency key", async () => {
48+
vi.mocked(backend.createMcpContextGrant).mockResolvedValue({ id: "grant-2" } as never);
49+
vi.mocked(backend.prepareMcpApproval).mockResolvedValue({ approvalRequest: { id: "10000000-0000-4000-8000-000000000003" }, challenge: {} } as never);
50+
vi.mocked(backend.confirmMcpApproval).mockResolvedValue({ approvalRequest: {}, approvalTicket: "ticket-1" } as never);
51+
const args = { reviewedOperationId: "10000000-0000-4000-8000-000000000004", decision: "approved", rationale: "Checked." };
52+
await expect(prepareClinicalMcpArguments(policy, "clinical.record_review_decision", args, { patientCaseId: "case-1" })).rejects.toThrow(/explicit approval/);
53+
expect(backend.createMcpContextGrant).not.toHaveBeenCalled();
54+
expect(backend.prepareMcpApproval).not.toHaveBeenCalled();
55+
expect(backend.confirmMcpApproval).not.toHaveBeenCalled();
56+
const result = await prepareClinicalMcpArguments(policy, "clinical.record_review_decision", args, { patientCaseId: "case-1", humanApproved: true });
57+
expect(result).toMatchObject({ ...args, contextGrantId: "grant-2", approvalTicket: "ticket-1" });
58+
expect(result.idempotencyKey).toEqual(expect.any(String));
59+
});
60+
61+
it("strips model-provided infrastructure credentials and leaves generic tools unchanged", async () => {
62+
vi.mocked(backend.createMcpContextGrant).mockResolvedValue({ id: "trusted-grant" } as never);
63+
const args = { assistantResponse: "draft", contextGrantId: "model-grant", approvalTicket: "model-ticket", idempotencyKey: "model-key" };
64+
await expect(prepareClinicalMcpArguments(policy, "clinical.response_contract_check", args, { patientCaseId: "case-1" })).resolves.toEqual({ assistantResponse: "draft", contextGrantId: "trusted-grant" });
65+
await expect(prepareClinicalMcpArguments({ ...policy, integrationProfile: "generic" }, "generic.tool", args)).resolves.toBe(args);
66+
});
67+
});

app/src/clinical-mcp-broker.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { randomUUID } from "node:crypto";
2+
import type { ManagedMcpPolicy } from "./managed-mcp-policy";
3+
import * as patientCases from "./patient-cases-store";
4+
import { confirmMcpApproval, createMcpContextGrant, prepareMcpApproval } from "./shared-backend-client";
5+
6+
export interface ClinicalMcpExecutionContext {
7+
patientCaseId?: string;
8+
humanApproved?: boolean;
9+
}
10+
11+
const INFRASTRUCTURE_FIELDS = new Set(["contextGrantId", "approvalTicket", "idempotencyKey"]);
12+
const TOOL_FIELDS: Record<string, string[]> = {
13+
"clinical.medication_conflict_check": ["allergies", "medications"],
14+
"clinical.response_contract_check": ["assistantResponse"],
15+
"clinical.response_contract_check_batch": ["items"],
16+
"clinical.record_review_decision": ["rationale"],
17+
};
18+
const PURPOSE = {
19+
"clinical.medication_conflict_check": "medication-review",
20+
"clinical.response_contract_check": "documentation-assist",
21+
"clinical.response_contract_check_batch": "documentation-assist",
22+
"clinical.record_review_decision": "documentation-assist",
23+
} as const;
24+
25+
export function modelVisibleClinicalSchema(schema: Record<string, unknown> | undefined, toolName?: string): Record<string, unknown> | undefined {
26+
if (!schema) return schema;
27+
const brokerFields = new Set(INFRASTRUCTURE_FIELDS);
28+
if (toolName === "clinical.medication_conflict_check") {
29+
brokerFields.add("medications");
30+
brokerFields.add("allergies");
31+
}
32+
const properties = { ...((schema.properties ?? {}) as Record<string, unknown>) };
33+
for (const field of brokerFields) delete properties[field];
34+
const required = Array.isArray(schema.required) ? schema.required.filter((field) => typeof field !== "string" || !brokerFields.has(field)) : undefined;
35+
return { ...schema, properties, ...(required ? { required } : {}) };
36+
}
37+
38+
function stripInfrastructureArgs(args: Record<string, unknown>): Record<string, unknown> {
39+
const clean = { ...args };
40+
for (const field of INFRASTRUCTURE_FIELDS) delete clean[field];
41+
return clean;
42+
}
43+
44+
async function authoritativeArguments(toolName: string, args: Record<string, unknown>, caseId?: string): Promise<Record<string, unknown>> {
45+
const clean = stripInfrastructureArgs(args);
46+
if (toolName !== "clinical.medication_conflict_check") return clean;
47+
if (!caseId) throw new Error("Attach a patient case before running a clinical medication check.");
48+
const patientCase = await patientCases.getCase(caseId);
49+
if (!patientCase) throw new Error("The attached patient case is no longer available.");
50+
if (!patientCase.medications.includeInContext || !patientCase.allergies.includeInContext) {
51+
throw new Error("Include both medications and allergies in the attached case context before running this check.");
52+
}
53+
return { medications: patientCase.medications.value, allergies: patientCase.allergies.value };
54+
}
55+
56+
export async function prepareClinicalMcpArguments(
57+
policy: ManagedMcpPolicy,
58+
toolName: string,
59+
args: Record<string, unknown>,
60+
context: ClinicalMcpExecutionContext = {}
61+
): Promise<Record<string, unknown>> {
62+
if (policy.integrationProfile !== "modelforge-clinical") return args;
63+
if (toolName === "clinical.submit_compute_request") throw new Error("Governed compute submission is not enabled in this clinical-review release.");
64+
if (toolName === "clinical.record_review_decision" && !context.humanApproved) {
65+
throw new Error("This controlled clinical write requires explicit approval for this call.");
66+
}
67+
const domainArguments = await authoritativeArguments(toolName, args, context.patientCaseId);
68+
const fields = TOOL_FIELDS[toolName] ?? [];
69+
let contextGrantId: string | undefined;
70+
if (fields.length > 0) {
71+
if (!context.patientCaseId) throw new Error(`Attach a patient case before running "${toolName}".`);
72+
const purpose = PURPOSE[toolName as keyof typeof PURPOSE] ?? "documentation-assist";
73+
const grant = await createMcpContextGrant({ registryEntryId: policy.entryId, caseId: context.patientCaseId, purpose, toolNames: [toolName], requestedFields: fields });
74+
contextGrantId = grant.id;
75+
}
76+
const injected: Record<string, unknown> = { ...domainArguments, ...(contextGrantId ? { contextGrantId } : {}) };
77+
if (toolName === "clinical.record_review_decision") {
78+
const prepared = await prepareMcpApproval({ registryEntryId: policy.entryId, toolName, arguments: domainArguments, contextGrantId, caseId: context.patientCaseId });
79+
const confirmed = await confirmMcpApproval(prepared.approvalRequest.id);
80+
injected.approvalTicket = confirmed.approvalTicket;
81+
injected.idempotencyKey = randomUUID();
82+
}
83+
return injected;
84+
}

app/src/hl7-client.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { hl7IngestionJobSchema, type Hl7IngestionJob } from "@modelforge/contracts";
2+
import { z } from "zod";
3+
import { authorizedRequest, SharedBackendClientError } from "./shared-backend-client";
4+
import { getSharedBackendConfig } from "./shared-backend-config-store";
5+
6+
// REST glue for server/src/routes/hl7.ts's inbound-ingestion review queue —
7+
// GET .../inbound/jobs and POST .../inbound/jobs/:jobId/resolve. Outbound
8+
// ORU^R01 generation and the raw /parse endpoint have no UI need yet (no
9+
// clinician-facing use for either) so aren't wired here.
10+
11+
export type Hl7ResolveDecision = { action: "apply"; caseId: string } | { action: "reject"; reason: string };
12+
13+
function organizationId(): string {
14+
const id = getSharedBackendConfig()?.organizationId;
15+
if (!id) throw new SharedBackendClientError("Select a shared-backend organization before using HL7 ingestion review.");
16+
return id;
17+
}
18+
19+
async function expectJson<T>(response: Response, action: string): Promise<T> {
20+
if (!response.ok) {
21+
let detail = `HTTP ${response.status}`;
22+
try {
23+
const body = (await response.json()) as { message?: string; error?: string };
24+
detail = body.message ?? body.error ?? detail;
25+
} catch { /* response was not JSON */ }
26+
throw new SharedBackendClientError(`${action} failed: ${detail}`);
27+
}
28+
return response.json() as Promise<T>;
29+
}
30+
31+
export async function listHl7IngestionJobs(status?: Hl7IngestionJob["status"]): Promise<Hl7IngestionJob[]> {
32+
const org = organizationId();
33+
const query = status ? `?status=${encodeURIComponent(status)}` : "";
34+
const body = await expectJson<{ jobs: unknown[] }>(
35+
await authorizedRequest(`/organizations/${encodeURIComponent(org)}/hl7/v2/inbound/jobs${query}`),
36+
"Loading HL7 ingestion queue"
37+
);
38+
return z.array(hl7IngestionJobSchema).parse(body.jobs);
39+
}
40+
41+
export async function resolveHl7IngestionJob(jobId: string, decision: Hl7ResolveDecision): Promise<Hl7IngestionJob> {
42+
const org = organizationId();
43+
return hl7IngestionJobSchema.parse(
44+
await expectJson(
45+
await authorizedRequest(`/organizations/${encodeURIComponent(org)}/hl7/v2/inbound/jobs/${encodeURIComponent(jobId)}/resolve`, { method: "POST", body: JSON.stringify(decision) }),
46+
"Resolving HL7 ingestion job"
47+
)
48+
);
49+
}

app/src/ipc/agent-handlers.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ export function registerAgentIpc(): void {
2020
"tools:execute",
2121
async (
2222
event: IpcMainInvokeEvent,
23-
{ workspaceRoot, name, args, requestId }: { workspaceRoot: string; name: string; args: unknown; requestId?: string }
23+
{ workspaceRoot, name, args, requestId, clinicalContext }: {
24+
workspaceRoot: string;
25+
name: string;
26+
args: unknown;
27+
requestId?: string;
28+
clinicalContext?: { patientCaseId?: string; humanApproved?: boolean };
29+
}
2430
) => {
2531
requireString(workspaceRoot, "workspace root");
2632
requireString(name, "tool name");
@@ -46,9 +52,10 @@ export function registerAgentIpc(): void {
4652
const controller = requestId ? new AbortController() : undefined;
4753
if (requestId && controller) activeMcpToolRequests.set(requestId, controller);
4854
try {
49-
result = await mcpClient.callMcpTool(name, validatedArgs, {
55+
result = await mcpClient.callMcpToolStructured(name, validatedArgs, {
5056
signal: controller?.signal,
5157
onProgress: requestId ? (p) => event.sender.send(`mcp:toolProgress:${requestId}`, p) : undefined,
58+
clinicalContext,
5259
});
5360
} finally {
5461
if (requestId) activeMcpToolRequests.delete(requestId);

app/src/ipc/mcp-handlers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ import * as mcpOAuth from "../mcp-oauth";
55
import { mcpServerConfigSchema, parseOrThrow } from "../schemas";
66
import { requireString, getMainWindow, activeMcpToolRequests } from "../app-state";
77
import { buildMastervaultServerConfig, isMastervaultBuiltinAvailable } from "../mastervault-builtin";
8+
import { listManagedClinicalMcpServers } from "../managed-mcp-policy";
89

910
export function registerMcpIpc(): void {
11+
ipcMain.handle("mcp:listManagedClinicalServers", async () => {
12+
try { return { servers: await listManagedClinicalMcpServers() }; }
13+
catch (error) { return { error: (error as Error).message }; }
14+
});
1015
ipcMain.handle("mcp:isMastervaultBuiltinAvailable", () => isMastervaultBuiltinAvailable());
1116

1217
// Convenience one-click add for the built-in MasterVault server: prompts

app/src/ipc/shared-backend-handlers.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { requireString } from "../app-state";
88
import * as caseMigration from "../case-migration";
99
import * as imagingClient from "../imaging-client";
1010
import * as clinicalAiClient from "../clinical-ai-client";
11+
import * as hl7Client from "../hl7-client";
12+
import * as smartLaunchClient from "../smart-launch-client";
13+
import { runSmartLaunch } from "../smart-launch-flow";
1114
import { closeOhifLaunch, createOhifLaunch } from "../ohif-viewer";
1215

1316
// IPC surface for enterprise-mode shared-backend connection management
@@ -142,4 +145,47 @@ export function registerSharedBackendIpc(): void {
142145
ipcMain.handle("clinicalAi:submit", (_event, input: { caseId: string; request: clinicalAiClient.ClinicalAiSubmitInput }) => { requireString(input?.caseId,"case id");return clinicalAiClient.submitClinicalAiRequest(input.caseId,input.request); });
143146
ipcMain.handle("clinicalAi:listActivity", (_event, caseId: string) => { requireString(caseId,"case id");return clinicalAiClient.listClinicalAiActivity(caseId); });
144147
ipcMain.handle("clinicalAi:review", (_event, input: { outputId: string; review: { decision: "accepted"|"rejected"|"corrected"|"escalated"; correctedText?: string; escalationReason?: string } }) => { requireString(input?.outputId,"output id");return clinicalAiClient.reviewClinicalAiOutput(input.outputId,input.review); });
148+
149+
ipcMain.handle("hl7:listJobs", (_event: IpcMainInvokeEvent, status?: "pending-review" | "applied" | "rejected") => hl7Client.listHl7IngestionJobs(status));
150+
ipcMain.handle("hl7:resolveJob", (_event: IpcMainInvokeEvent, input: { jobId: string; decision: hl7Client.Hl7ResolveDecision }) => {
151+
requireString(input?.jobId, "ingestion job id");
152+
if (input?.decision?.action !== "apply" && input?.decision?.action !== "reject") throw new Error('Resolution decision must have action "apply" or "reject".');
153+
if (input.decision.action === "apply") requireString(input.decision.caseId, "case id");
154+
else requireString(input.decision.reason, "rejection reason");
155+
return hl7Client.resolveHl7IngestionJob(input.jobId, input.decision);
156+
});
157+
158+
ipcMain.handle("smartLaunch:listTrustedIssuers", () => smartLaunchClient.listTrustedIssuers());
159+
ipcMain.handle("smartLaunch:upsertTrustedIssuer", (_event: IpcMainInvokeEvent, input: { issuer: string; clientId: string; redirectUris: string[] }) => {
160+
requireString(input?.issuer, "issuer");
161+
requireString(input?.clientId, "client id");
162+
if (!Array.isArray(input?.redirectUris) || input.redirectUris.length === 0) throw new Error("At least one redirect URI is required.");
163+
return smartLaunchClient.upsertTrustedIssuer(input);
164+
});
165+
ipcMain.handle("smartLaunch:deleteTrustedIssuer", (_event: IpcMainInvokeEvent, issuer: string) => {
166+
requireString(issuer, "issuer");
167+
return smartLaunchClient.deleteTrustedIssuer(issuer);
168+
});
169+
ipcMain.handle("smartLaunch:listSessions", () => smartLaunchClient.listLaunchSessions());
170+
ipcMain.handle("smartLaunch:revokeSession", (_event: IpcMainInvokeEvent, sessionId: string) => {
171+
requireString(sessionId, "session id");
172+
return smartLaunchClient.revokeLaunchSession(sessionId);
173+
});
174+
// Opens the system browser and waits on user interaction at the EHR —
175+
// same long-running-external-flow shape as sharedBackend:connect/
176+
// mcp:startOAuthFlow above, so it catches and returns {error} rather
177+
// than rejecting, letting the renderer show an inline error instead of
178+
// an unhandled-IPC-rejection for an ordinary "user closed the tab" or
179+
// "5-minute timeout" outcome.
180+
ipcMain.handle("smartLaunch:start", async (_event: IpcMainInvokeEvent, issuer: string) => {
181+
try {
182+
requireString(issuer, "issuer");
183+
const token = await runSmartLaunch(issuer);
184+
return { token };
185+
} catch (err) {
186+
const error = err as Error;
187+
logger.error(`SMART launch failed: ${error.message}`);
188+
return { error: error.message };
189+
}
190+
});
145191
}

0 commit comments

Comments
 (0)