187 lines
21 KiB
TypeScript
187 lines
21 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { createHash, randomUUID } from "node:crypto"
|
|
import { createServerProbeMcpAdapter, serverProbeActionSchema, serverProbeSafeMcpError, type ServerProbeMcpDependencies } from "./server-probe-actions.js"
|
|
import { ServerProbeExecutionError, type PreparedServerProbe, type ServerProbeAuthority, type ServerProbeReceipt, type ServerProbeJournal } from "../../services/server-probe-execution.js"
|
|
import type { ServerProbeJournalContext } from "../../services/server-probe-journal.js"
|
|
import { createServerProbeJournalHistoryReader } from "../../services/server-probe-journal-history.js"
|
|
import type { ActionContext, ChangeEvidence } from "./action-types.js"
|
|
import { fingerprint, redact } from "./security.js"
|
|
|
|
const now = 1800000000000, operationId = randomUUID(), connectionId = randomUUID(), credentialId = randomUUID(), receiptId = randomUUID()
|
|
const principal = { connectionId, credentialId, credentialExpiresAt: new Date(now + 600000).toISOString() }
|
|
const proof = async (value: unknown) => createHash("sha256").update(JSON.stringify(value)).digest("hex")
|
|
const context = { principal, connection: { id: connectionId, owner_id: "owner", owner_claims_issued_at: new Date(now - 1000) } } as ActionContext
|
|
let assertions = 0
|
|
function fixture(retiredSource?: ServerProbeMcpDependencies["retiredSource"]) {
|
|
const events: string[] = [], receipts = new Map<string, ServerProbeReceipt>(), contexts: ServerProbeJournalContext[] = [], sealed = new Map<string, string>()
|
|
let evidence: ChangeEvidence, result: unknown = null, stepState = "intent", operationState = "running", scope = [2, 9], revoked = false, sqlTamper: string | undefined, dispatchFailure: unknown, preparedTool: PreparedServerProbe["tool"] = "ping", mismatchDestination = false, nextCipher = 0, revokeOnQuery = false, expiredPrepared = false, clock = now, liveHistory = true
|
|
const access: ServerProbeAuthority = { transport: "mcp", ownerId: "owner", connectionId, credentialId, expiresAt: now + 600000, generation: "generation", policyProof: "1".repeat(64), ownerProof: "2".repeat(64), active: true, operationAllowed: true, scopeServerIds: scope, ownerPermissions: ["mm:servers:write"], policyPermissions: ["mm:servers:write"] }
|
|
const readAuthority = async () => { events.push("authority"); if (revoked) throw new ServerProbeExecutionError("PERMISSION_DENIED", "PRIVATE_PROVIDER_SECRET", 403); return { ...access, expiresAt: clock + 600000, scopeServerIds: [...scope] } }
|
|
const assertProtections = async (ids: number[]) => { events.push("protections"); if (ids.some(id => !scope.includes(id))) throw new Error("out of scope") }
|
|
const seal = async (value: string) => { const id = `opaque-${++nextCipher}`; sealed.set(id, value); return id }
|
|
const open = async (value: string) => { const clear = sealed.get(value); if (!clear) throw new Error("bad cipher"); return clear }
|
|
const prepared = (): PreparedServerProbe => ({ receiptId, serverId: 9, tool: preparedTool, expiresAt: new Date(now + 300000).toISOString(), bindingCiphertext: "inner-private", ...(preparedTool === "bandwidth" ? { destinationServerId: mismatchDestination ? 3 : 2 } : {}) })
|
|
const preflight = async (value: PreparedServerProbe, signal?: AbortSignal) => { events.push("preflight"); if (signal?.aborted || expiredPrepared) throw new ServerProbeExecutionError("CANCELLED", "cancelled", 502); await readAuthority(); await assertProtections([value.serverId, ...(value.destinationServerId ? [value.destinationServerId] : [])]); return { receiptId: value.receiptId, bindingProof: "3".repeat(64), tool: value.tool, serverIds: [value.serverId, ...(value.destinationServerId ? [value.destinationServerId] : [])], expiresAt: value.expiresAt } }
|
|
const journal: ServerProbeJournal = {
|
|
claim: async () => { events.push("claim"); return "claimed" }, intent: async () => { events.push("intent") }, received: async () => {}, quarantine: async () => {},
|
|
finish: async value => { receipts.set(value.receiptId, value) },
|
|
read: async id => { events.push("journal.read"); return receipts.get(id) },
|
|
}
|
|
const deps: ServerProbeMcpDependencies = {
|
|
retiredSource,
|
|
authority: (() => ({ readAuthority, assertProtections })) as ServerProbeMcpDependencies["authority"],
|
|
historicalAuthority: ((currentPrincipal: typeof principal) => ({ readAuthority: async () => { events.push("read-authority"); return { ...await readAuthority(), connectionId: currentPrincipal.connectionId, credentialId: currentPrincipal.credentialId } }, assertProtections })) as ServerProbeMcpDependencies["historicalAuthority"],
|
|
history: dependencies => createServerProbeJournalHistoryReader({ ...dependencies, loadArchive: async binding => {
|
|
events.push("history.read"); const receipt = receipts.get(binding.receiptId); if (!receipt) return []
|
|
const identity = binding.context.identity; assert.equal(identity.kind, "mcp"); if (identity.kind !== "mcp") return []
|
|
assert.equal(identity.credentialId, credentialId)
|
|
const plaintext = JSON.stringify({ version: 1, context: binding.context, bindingProof: binding.bindingProof, receipt })
|
|
const ciphertext = "archive-" + await proof(plaintext); sealed.set(ciphertext, plaintext)
|
|
return [{ receipt_id: binding.receiptId, binding_proof: binding.bindingProof, identity_kind: "mcp", execution_id: operationId, actor_id: "owner", authentication: "mcp", mcp_connection_id: connectionId, mcp_credential_id: credentialId, request_id: null, authority_issued_at: now - 1000, authority_expires_at: new Date(now + 600000), source_server_id: receipt.serverId, destination_server_id: receipt.destinationServerId ?? null, tool: receipt.tool, state: receipt.state, attempted_stages: receipt.attemptedStages, completed_stages: receipt.completedStages, receipt_ciphertext: ciphertext, receipt_proof: await proof(plaintext), quarantine_count: "0" }]
|
|
}, crypto: { decrypt: open, proof: (_domain, value) => proof(value) } }),
|
|
executor: ((options: { journal?: ServerProbeJournal }) => ({
|
|
prepare: async (_id: number, request: { tool: PreparedServerProbe["tool"] }) => { events.push("prepare"); preparedTool = request.tool; return prepared() }, preflight,
|
|
execute: async (value: PreparedServerProbe, signal?: AbortSignal) => {
|
|
if (signal) events.push("trusted-signal")
|
|
events.push("execute"); if (dispatchFailure) throw dispatchFailure
|
|
assert.ok(options.journal); await options.journal.claim({ receiptId, bindingProof: "3".repeat(64), transport: "mcp", ownerId: "owner", connectionId, serverIds: [9, ...(value.destinationServerId ? [value.destinationServerId] : [])], tool: value.tool })
|
|
const receipt: ServerProbeReceipt = { receiptId, serverId: value.serverId, ...(value.destinationServerId ? { destinationServerId: value.destinationServerId } : {}), tool: value.tool, state: "completed", httpStatus: 200, output: "PUBLIC_MEASUREMENT".repeat(900), data: { kind: value.tool, reply: true }, attemptedStages: 1, completedStages: 1, automaticReplay: false, forwardingVerified: false }
|
|
await options.journal.finish(receipt); return receipt
|
|
},
|
|
})) as ServerProbeMcpDependencies["executor"],
|
|
journal: (ctx, dependencies, execution) => { assert.equal(execution?.deadlineAt, Date.parse(prepared().expiresAt)); contexts.push(ctx); return { ...journal, read: async (...args) => { await dependencies.assertAuthority(ctx, args[1]); return journal.read(...args) }, claim: async binding => { await dependencies.assertAuthority(ctx, binding.bindingProof); return journal.claim(binding) } } },
|
|
query: async (sql, values) => {
|
|
events.push("sql"); if (revokeOnQuery) revoked = true
|
|
if (sql.includes("probe-history:scope")) { assert.equal(values[2], connectionId); assert.equal(values[3], "owner"); return liveHistory ? [{ source_server_id: 9, destination_server_id: preparedTool === "bandwidth" ? 2 : null }] : [] }
|
|
if (sql.includes("FROM servers")) { assert.ok(sql.includes("ANY($1::int[])")); assert.deepEqual(values[0], scope); return sql.includes("AND id=$2") ? [] : [{ id: 2, host: "peer", enabled: true }] }
|
|
if (!sql.includes("mcp_operation_steps")) return [{ evidence: { sealed: await seal(JSON.stringify(evidence)) } }]
|
|
assert.equal(values[0], connectionId); assert.equal(values[1], "owner"); assert.equal(values[2], "servers.probes.run")
|
|
const intended = redact(evidence.steps[0])
|
|
assert.equal(fingerprint(JSON.parse(String(values[4]))), fingerprint(intended))
|
|
if (sqlTamper === "none") return []
|
|
const row = { operation_id: operationId, operation_state: operationState, plan_state: operationState === "running" ? "executing" : "completed", approved_by: "approver", step_state: stepState, step_index: 0, intended_state: intended, result, evidence: { sealed: await seal(JSON.stringify(evidence)) } }
|
|
if (sqlTamper === "digest") row.intended_state = { mismatch: true }
|
|
if (sqlTamper === "unapproved") row.approved_by = ""
|
|
if (sqlTamper === "step") row.step_index = 1
|
|
if (sqlTamper === "operation") row.operation_id = randomUUID()
|
|
if (sqlTamper === "evidence") row.evidence.sealed = await seal(JSON.stringify({ ...evidence, requiresHuman: false }))
|
|
return sqlTamper === "duplicate" ? [row, row] : [row]
|
|
}, seal, open, proof, now: () => clock,
|
|
}
|
|
const adapter = createServerProbeMcpAdapter(deps)
|
|
const retireHistory = () => { liveHistory = false }
|
|
const prepare = async (tool: PreparedServerProbe["tool"] = "ping") => { evidence = await adapter.prepare({ serverId: 9, request: tool === "bandwidth" ? { tool, bwRemoteAddress: "peer", dstServerId: 8 } : { tool, target: "192.0.2.1" } }, context); const step = evidence.steps[0]; assert.equal(step.kind, "service"); return step.kind === "service" ? step.input : assert.fail() }
|
|
return { adapter, events, receipts, contexts, prepare, retireHistory, setResult: (value: unknown) => { result = value; stepState = "applied" }, finishOperation: () => { operationState = "applied"; stepState = "acknowledged" }, setTamper: (value?: string) => { sqlTamper = value }, revoke: () => { revoked = true }, setScope: (value: number[]) => { scope = value }, fail: (value: unknown) => { dispatchFailure = value }, revokeOnQuery: () => { revokeOnQuery = true }, evidence: () => evidence, mismatchDestination: () => { mismatchDestination = true }, expirePrepared: () => { expiredPrepared = true; clock = now + 3600000 } }
|
|
}
|
|
|
|
for (const tool of ["ping", "traceroute", "bandwidth", "dns", "route", "mtu"] as const) {
|
|
const f = fixture(), input = await f.prepare(tool), evidence = f.evidence()
|
|
assert.equal(evidence.requiresHuman, true); assert.equal(evidence.verificationScope, "acknowledgement"); assert.equal(evidence.mutationCost, tool === "mtu" ? 11 : tool === "ping" || tool === "traceroute" ? 2 : 1)
|
|
assert.deepEqual(evidence.target, { scope: "servers", serverIds: tool === "bandwidth" ? [2, 9] : [9] }); assert.ok(!f.events.includes("sql") && !f.events.includes("claim"))
|
|
if (tool === "bandwidth") assert.ok(evidence.warnings.some(text => text.includes("2 (запрошен 8)")))
|
|
const before = await f.adapter.service.current(input); assert.deepEqual(before, evidence.before); assert.equal(f.contexts.length, 0)
|
|
const output = await f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0 }) as Record<string, unknown>
|
|
assert.equal(output.operationId, operationId); assert.equal(output.status, "completed"); assert.ok(!JSON.stringify(output).includes("PUBLIC_MEASUREMENT")); assert.ok(!("confirmedWrites" in output)); assert.ok(Number(output.totalPages) > 1)
|
|
assert.equal(f.contexts[0].identity.executionId, operationId); assert.equal(f.contexts[0].identity.issuedAt, now - 1000)
|
|
f.setResult(output); assert.equal(await f.adapter.service.verify!(input, output), true)
|
|
assert.equal(await f.adapter.service.verify!(input, { ...output, receiptId: randomUUID() }), false)
|
|
f.finishOperation(); const executions = f.events.filter(event => event === "execute").length
|
|
assert.deepEqual(await f.adapter.service.reconcile!(input, async () => {}), { matchesDesiredState: true, matchesOriginalState: false })
|
|
const page = await f.adapter.readPage({ operationId, receiptId, page: 0 }, context)
|
|
assert.equal(page.sha256, output.sha256); assert.ok(Buffer.from(page.contentB64, "base64").toString().includes("PUBLIC_MEASUREMENT")); assert.equal(f.events.filter(event => event === "execute").length, executions)
|
|
await assert.rejects(f.adapter.readPage({ operationId, receiptId: randomUUID(), page: 0 }, context)); assertions += 18
|
|
}
|
|
for (const tamper of ["none", "duplicate", "digest", "step", "unapproved", "operation", "evidence"]) {
|
|
const f = fixture(), input = await f.prepare(); f.setTamper(tamper)
|
|
await assert.rejects(f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0 })); assert.equal(f.events.includes("execute"), false); assertions++
|
|
}
|
|
for (const context of [undefined, { operationId, phase: "recovery" as const }, { operationId, phase: "forward" as const, stepIndex: 1 }]) {
|
|
const f = fixture(), input = await f.prepare(); await assert.rejects(f.adapter.service.execute(input, context)); assert.equal(f.events.includes("execute"), false); assertions++
|
|
}
|
|
{
|
|
const f = fixture(), input = await f.prepare()
|
|
for (const changed of [{ ...input, operationId }, { ...input, inputDigest: "0".repeat(64) }, { ...input, receiptId: randomUUID() }, { ...input, bindingCiphertext: "foreign" }]) { await assert.rejects(f.adapter.service.current(changed)); assertions++ }
|
|
f.revoke(); await assert.rejects(f.adapter.service.current(input)); assert.equal(f.events.includes("journal.read"), false); assertions++
|
|
}
|
|
{
|
|
const f = fixture(); assert.deepEqual(await f.adapter.target({ serverId: 9, request: { tool: "bandwidth", bwRemoteAddress: "peer", dstServerId: 8 } }, context), { scope: "servers", serverIds: [2, 9] }); assertions++
|
|
f.setScope([2]); await assert.rejects(f.adapter.target({ serverId: 9, request: { tool: "ping", target: "192.0.2.1" } }, context)); assertions++
|
|
}
|
|
{
|
|
const f = fixture(); f.revokeOnQuery(); await assert.rejects(f.adapter.target({ serverId: 9, request: { tool: "bandwidth", bwRemoteAddress: "peer", dstServerId: 8 } }, context)); assert.equal(f.events.filter(event => event === "sql").length, 1); assertions++
|
|
}
|
|
{
|
|
const f = fixture(), input = await f.prepare(); f.fail(new ServerProbeExecutionError("JOURNAL_UNKNOWN", "PRIVATE_PROVIDER_SECRET", 503, "unknown", 1, 0))
|
|
const error = await f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0 }).catch(error => error)
|
|
assert.ok(error && typeof error === "object" && "code" in error && "diagnosticProgress" in error)
|
|
const progress = error.diagnosticProgress; assert.ok(progress && typeof progress === "object" && "attemptedStages" in progress)
|
|
assert.equal(error.code, "JOURNAL_UNKNOWN"); assert.equal(progress.attemptedStages, 1); assert.ok(!JSON.stringify(error).includes("PRIVATE_PROVIDER_SECRET")); assert.ok(!("nativeProgress" in error)); assertions += 3
|
|
}
|
|
{
|
|
const f = fixture(), input = await f.prepare(), output = await f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0 }); f.setResult(output)
|
|
f.receipts.set(receiptId, { ...f.receipts.get(receiptId)!, serverId: 2 }); assert.equal(await f.adapter.service.verify!(input, output), false); await assert.rejects(f.adapter.readPage({ operationId, receiptId, page: 0 }, context)); assertions += 2
|
|
}
|
|
for (const input of [{ serverId: 9, request: { tool: "ping", target: "x", credential: "secret" } }, { serverId: 9, operationId, request: { tool: "route", target: "192.0.2.1" } }, { serverId: 9, request: { tool: "route", target: "host" } }]) { assert.equal(serverProbeActionSchema.safeParse(input).success, false); assertions++ }
|
|
{
|
|
const f = fixture(), input = await f.prepare(), controller = new AbortController()
|
|
assert.equal(f.adapter.service.cancellableDiagnostic, true)
|
|
await f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0, signal: controller.signal }); assert.ok(f.events.includes("trusted-signal"))
|
|
controller.abort(); const attempted = f.events.filter(event => event === "execute").length
|
|
await assert.rejects(f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0, signal: controller.signal })); assert.equal(f.events.filter(event => event === "execute").length, attempted); assertions += 3
|
|
}
|
|
assert.ok(!("diagnosticProgress" in serverProbeSafeMcpError(new ServerProbeExecutionError("X", "secret", 500, "unknown", 99), { receiptId, operationId, stageBudget: 1 }))); assertions++
|
|
{
|
|
const hash = "3".repeat(64), date = (value: number) => new Date(value).toISOString().replace("Z", "000Z")
|
|
const retired = { source: { version: 1 as const, operation: "servers.probes.run" as const, operationVersion: 1 as const, tool: "ping" as const,
|
|
receiptId, executionId: operationId, connectionId, ownerId: "owner", planId: randomUUID(), originalCredentialId: credentialId,
|
|
sourceServerId: 9, serverIds: [9], issuedAt: now - 1000, authorityExpiresAt: date(now + 600000), terminalAt: date(now + 600000),
|
|
activityAt: date(now + 600000), gcNotBefore: date(now + 87060000), retentionDays: 1, safetyMs: 60000,
|
|
bindingProof: hash, sourceDigest: hash, terminalDigest: hash, stageSnapshotDigest: hash, receiptProof: hash, ciphertextSha256: hash,
|
|
}, payloadPurged: false }
|
|
let lookups = 0, changeOnRecheck = false
|
|
const f = fixture(async lookup => {
|
|
lookups++
|
|
assert.deepEqual(lookup, { receiptId, operationId, connectionId, ownerId: "owner", scopeServerIds: [2, 9] })
|
|
return changeOnRecheck && lookups % 2 === 0 ? undefined : retired
|
|
})
|
|
const input = await f.prepare(), output = await f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0 })
|
|
f.setResult(output); f.finishOperation()
|
|
const request = { operationId, receiptId, page: 0 }
|
|
const live = await f.adapter.readPage(request, context)
|
|
assert.equal(lookups, 0)
|
|
f.retireHistory()
|
|
const rotated = { ...context, principal: { ...principal, credentialId: randomUUID() } }
|
|
assert.deepEqual(await f.adapter.readPage(request, rotated), live)
|
|
assert.deepEqual(await f.adapter.receiptTarget(request, rotated), { scope: "servers", serverIds: [9] })
|
|
const beforeForeign = lookups
|
|
await assert.rejects(f.adapter.readPage(request, { ...rotated, connection: { ...context.connection, owner_id: "foreign" } }))
|
|
assert.equal(lookups, beforeForeign)
|
|
lookups = 0; changeOnRecheck = true
|
|
const privateReads = f.events.filter(event => event === "history.read").length
|
|
await assert.rejects(f.adapter.readPage(request, rotated))
|
|
assert.equal(f.events.filter(event => event === "history.read").length, privateReads)
|
|
assertions += 7
|
|
}
|
|
{
|
|
const f = fixture(), input = await f.prepare("bandwidth"), output = await f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0 }); f.setResult(output); f.finishOperation(); f.expirePrepared()
|
|
await assert.rejects(f.adapter.service.current(input))
|
|
const rotatedContext = { ...context, principal: { ...principal, credentialId: randomUUID(), credentialExpiresAt: new Date(now + 7200000).toISOString() } }
|
|
const preflights = f.events.filter(event => event === "preflight").length, claims = f.events.filter(event => event === "claim").length
|
|
const page = await f.adapter.readPage({ operationId, receiptId, page: 0 }, rotatedContext)
|
|
assert.equal(page.receiptId, receiptId); assert.deepEqual(await f.adapter.receiptTarget({ operationId, receiptId, page: 0 }, rotatedContext), { scope: "servers", serverIds: [2, 9] })
|
|
assert.equal(f.events.filter(event => event === "preflight").length, preflights); assert.equal(f.events.filter(event => event === "claim").length, claims)
|
|
f.setScope([9]); await assert.rejects(f.adapter.readPage({ operationId, receiptId, page: 0 }, rotatedContext)); f.setScope([2, 9])
|
|
await assert.rejects(f.adapter.readPage({ operationId, receiptId, page: 0 }, { ...rotatedContext, principal: { ...rotatedContext.principal, connectionId: randomUUID() } }))
|
|
await assert.rejects(f.adapter.readPage({ operationId, receiptId, page: 0 }, { ...rotatedContext, connection: { ...context.connection, owner_id: "foreign" } }))
|
|
f.revoke(); await assert.rejects(f.adapter.readPage({ operationId, receiptId, page: 0 }, rotatedContext)); assertions += 8
|
|
}
|
|
{
|
|
const f = fixture(), input = await f.prepare("bandwidth")
|
|
assert.deepEqual(await f.adapter.service.diagnosticIdentity!(input, { operationId, phase: "forward", stepIndex: 0 }), { receiptId, stageBudget: 1 })
|
|
assert.equal(f.events.includes("execute"), false); assert.equal(f.events.includes("claim"), false)
|
|
await assert.rejects(f.adapter.service.diagnosticIdentity!(input, { operationId, phase: "recovery" }))
|
|
await assert.rejects(f.adapter.service.diagnosticIdentity!({ ...input, inputDigest: "0".repeat(64) }, { operationId, phase: "forward", stepIndex: 0 }))
|
|
f.revoke(); await assert.rejects(f.adapter.service.diagnosticIdentity!(input, { operationId, phase: "forward", stepIndex: 0 })); assertions += 6
|
|
}
|
|
console.log(`server-probe MCP adapter: ${assertions} inert assertions passed`)
|