99 lines
9.3 KiB
TypeScript
99 lines
9.3 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { spawn } from "node:child_process"
|
|
import { createServer } from "node:net"
|
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
import { resolve, isAbsolute } from "node:path"
|
|
import { SignJWT } from "jose"
|
|
import { writeFile } from "node:fs/promises"
|
|
assert.equal(process.env.OWN_EXTERNAL_PROCESS_PG, "true"); assert.equal(process.env.NODE_ENV, "test")
|
|
assert(process.env.OWN_EXTERNAL_SOURCE_ROOT && isAbsolute(process.env.OWN_EXTERNAL_SOURCE_ROOT))
|
|
assert(process.env.OWN_EXTERNAL_RESULT_FILE && isAbsolute(process.env.OWN_EXTERNAL_RESULT_FILE))
|
|
const source = (path: string) => pathToFileURL(resolve(process.env.OWN_EXTERNAL_SOURCE_ROOT!, "backend/src", path)).href
|
|
const database = new URL(process.env.DATABASE_URL ?? "")
|
|
assert.equal(database.hostname, "127.0.0.1"); assert.equal(database.username, "mcp_test"); assert.equal(database.pathname, "/postgres"); assert.notEqual(database.port, "5432")
|
|
const listener = createServer(); await new Promise<void>(r => listener.listen(0, "127.0.0.1", r)); const port = (listener.address() as { port: number }).port
|
|
await new Promise<void>(r => listener.close(() => r()))
|
|
const origin = `http://127.0.0.1:${port}`, endpoint = origin + "/mcp"
|
|
process.env.MCP_PUBLIC_URL = endpoint; process.env.MCP_UI_URL = origin; process.env.CORS_ORIGIN = origin
|
|
const { pool, closePool } = await import(source("db/index.js"))
|
|
const { applySqlMigrations } = await import(source("db/migrate.js"))
|
|
const { mcpConnectionCreateSchema } = await import("@mmapp/contracts/mcp")
|
|
const { createMcpConnection, rotateConnectionToken, setConnectionStatus, updateMcpSettings } = await import(source("modules/mcp/store.js"))
|
|
const { env } = await import(source("config.js"))
|
|
const { buildApp } = await import(source("index.js"))
|
|
const actor = { id: "owned-external-process-owner", email: "external@example.invalid", name: "Owned external SDK fixture", apps: ["mm"],
|
|
permissions: ["mm:settings:admin", "mm:mcp:admin", "mm:servers:admin"], expiresAt: Date.now() + 3600000, issuedAt: Date.now() - 1000 }
|
|
let currentId: string | undefined, app: Awaited<ReturnType<typeof buildApp>> | undefined, child: ReturnType<typeof spawn> | undefined
|
|
const credentials: string[] = []
|
|
const bridgePids = new Set<number>()
|
|
let childExit: Promise<number> | undefined
|
|
const delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
|
|
function alive(pid: number) { try { process.kill(pid, 0); return true } catch (error) { if ((error as NodeJS.ErrnoException).code === "ESRCH") return false; throw error } }
|
|
async function drainChildAndBridges() {
|
|
if (child && child.exitCode === null && child.signalCode === null) { child.kill(); await Promise.race([childExit, delay(5000).then(() => { throw Error("Owned external child failed to exit") })]) }
|
|
const deadline = Date.now() + 5000
|
|
while ([...bridgePids].some(alive) && Date.now() < deadline) await delay(25)
|
|
const remaining = [...bridgePids].filter(alive)
|
|
for (const pid of remaining) process.kill(pid)
|
|
const killDeadline = Date.now() + 5000
|
|
while (remaining.some(alive) && Date.now() < killDeadline) await delay(25)
|
|
assert(!remaining.some(alive), "Owned SDK bridge grandchild failed to exit")
|
|
console.log("Owned client and all observed production bridge processes exited:", JSON.stringify({ childPid: child?.pid, exitCode: child?.exitCode, signalCode: child?.signalCode, bridgePids: [...bridgePids] }))
|
|
}
|
|
try {
|
|
await applySqlMigrations(pool)
|
|
for (const table of ["traffic_settings", "uptime_settings", "traffic_flow_settings", "servers_api_ping_settings", "certificate_renew_settings", "backup_schedule_settings", "geoip_settings", "internet_path_settings", "router_log_settings"]) await pool.query(`UPDATE ${table} SET enabled=false WHERE id=1`)
|
|
const secret = "OWNED_DEVICE_CREDENTIAL_MUST_NOT_ESCAPE"
|
|
const allowed = (await pool.query("INSERT INTO servers(name,host,enabled,password) VALUES('owned allowed','192.0.2.1',false,$1) RETURNING id", [secret])).rows[0].id
|
|
const forbidden = (await pool.query("INSERT INTO servers(name,host,enabled) VALUES('owned forbidden','192.0.2.2',false) RETURNING id")).rows[0].id
|
|
await updateMcpSettings({ enabled: true }, actor)
|
|
app = await buildApp({ startScheduler: false, logger: false }); await app.listen({ port, host: "127.0.0.1" })
|
|
const jwt = await new SignJWT({ sub: actor.id, apps: actor.apps, permissions: actor.permissions }).setProtectedHeader({ alg: "HS256" }).setIssuer(env.authIssuer).setIssuedAt().setExpirationTime("1h").sign(new TextEncoder().encode(env.jwtSecret))
|
|
const transcript: unknown[] = []; let failure: unknown, result: unknown
|
|
child = spawn(process.execPath, [fileURLToPath(new URL("./client.mjs", import.meta.url))], { cwd: process.cwd(), windowsHide: true, stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
env: { ...Object.fromEntries(["SystemRoot", "WINDIR", "TEMP", "TMP", "PATH", "Path"].filter(k => process.env[k]).map(k => [k, process.env[k]!])),
|
|
OWN_EXTERNAL_CLIENT: "true", OWN_MCP_ENDPOINT: endpoint, OWN_SERVER_PID: String(process.pid), OWN_ALLOWED_SERVER: String(allowed), OWN_FORBIDDEN_SERVER: String(forbidden),
|
|
OWN_BRIDGE_FILE: fileURLToPath(new URL(source("scripts/mcp-stdio.ts"))), OWN_PORTAL_JWT: jwt, OWN_SERVER_SECRET: secret, OWN_EXTERNAL_CLEANUP_FAULT: process.env.OWN_EXTERNAL_CLEANUP_FAULT } })
|
|
childExit = new Promise<number>(resolve => { child!.once("exit", status => resolve(status ?? 1)); child!.once("error", () => resolve(1)) })
|
|
let stderr = "", stdout = ""
|
|
child.stderr?.on("data", chunk => { stderr += String(chunk); if (stderr.length > 1048576) child?.kill() })
|
|
child.stdout?.on("data", chunk => { stdout += String(chunk); if (stdout.length > 1048576) child?.kill() })
|
|
child.on("message", async (raw: unknown) => {
|
|
const message = raw as { kind: string; command?: string; id?: number; pid?: number; result?: unknown; phase?: string; errorType?: string }
|
|
if (message.kind === "bridge-pid") { assert(Number.isSafeInteger(message.pid) && message.pid! > 0 && message.pid !== process.pid && message.pid !== child?.pid); bridgePids.add(message.pid!); return }
|
|
if (message.kind === "arm-cleanup-timeout" && process.env.OWN_EXTERNAL_CLEANUP_FAULT === "timeout") { setTimeout(() => { failure = { phase: "owned-deliberate-cleanup-timeout" }; child?.kill() }, 25); return }
|
|
if (message.kind === "result") { result = message.result; return }
|
|
if (message.kind === "failure") { failure = { phase: message.phase, errorType: message.errorType }; return }
|
|
if (message.kind !== "control" || !Number.isSafeInteger(message.id)) return
|
|
try {
|
|
let value: unknown
|
|
if (message.command === "new-connection") {
|
|
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Owned external SDK process", mode: "observer", expiresAt: new Date(Date.now() + 1200000).toISOString(),
|
|
permissions: ["mm:servers:read"], scope: { serverIds: [Number(allowed)], operationIds: ["servers.list"], allowGlobal: false }, limits: { callsPerMinute: 200 } }), actor)
|
|
currentId = issued.connection.id; credentials.push(issued.token); value = { token: issued.token, connectionId: currentId }
|
|
} else if (message.command === "large-result" || message.command === "reset-result") {
|
|
await pool.query("UPDATE servers SET comment=$2 WHERE id=$1", [allowed, message.command === "large-result" ? "Z".repeat(40000) + "OWNED_TRUNCATION_TAIL" : ""])
|
|
value = { ok: true }
|
|
} else {
|
|
assert(currentId)
|
|
if (message.command === "rotate") { const rotated = await rotateConnectionToken(currentId, actor); credentials.push(rotated.token); value = rotated }
|
|
else if (message.command === "pause" || message.command === "resume" || message.command === "revoke") { await setConnectionStatus(currentId, message.command === "pause" ? "paused" : message.command === "resume" ? "active" : "revoked", actor); value = { ok: true } }
|
|
else throw Error("Unsupported exact fixture control")
|
|
}
|
|
transcript.push({ command: message.command, connectionId: currentId })
|
|
child?.send({ kind: "reply", id: message.id, value })
|
|
} catch { child?.send({ kind: "reply", id: message.id, error: true }) }
|
|
})
|
|
const timer = setTimeout(() => { failure = { phase: "whole-external-client-deadline" }; child?.kill() }, 120000)
|
|
const code = await childExit; clearTimeout(timer)
|
|
if (code !== 0 || !result) { console.log("OWNED_EXTERNAL_PROCESS_FAILURE", JSON.stringify({ code, failure, stdout: stdout.slice(0,1000), stderr: stderr.slice(0,1000) })); throw Error("External SDK process did not complete") }
|
|
const logs = JSON.stringify((await pool.query("SELECT tool,input,result,error_code FROM mcp_requests ORDER BY created_at")).rows)
|
|
for (const token of [...credentials, jwt, secret]) { assert(!logs.includes(token)); assert(!stdout.includes(token)); assert(!stderr.includes(token)) }
|
|
const final = { ...(result as object), actualOwnPg: true, actualProductionHttpServer: true, serverFixturePid: process.pid,
|
|
controlTransitions: transcript, auditCredentialLeakAbsent: true, nativeRouterCalls: 0, productionDatabaseTouched: false }
|
|
await writeFile(process.env.OWN_EXTERNAL_RESULT_FILE!, JSON.stringify(final, null, 2))
|
|
console.log("Owned external process SDK result:", JSON.stringify(final))
|
|
} finally {
|
|
try { await drainChildAndBridges() } finally { try { if (app) await app.close() } finally { await closePool() } }
|
|
}
|