86 lines
7.0 KiB
TypeScript
86 lines
7.0 KiB
TypeScript
import { loadRoutingMainTransitionPreparation } from "./routing-main-transition.js"
|
|
import { assertScopedCommunityRequest } from "./routing-community-scoped-request.js"
|
|
import { routeAiProfileSchema } from "@mmapp/contracts/mcp-routing"
|
|
import { pool } from "../../db/index.js"
|
|
import { captureVerifiedMcpServiceExecution, inspectVerifiedMcpServiceExecution } from "../../services/config-mutation-policy.js"
|
|
import { effectiveMcpConnection } from "./store.js"
|
|
import { routingProfileAuthorityBindingSchema, assertPersistentScopedProfileAuthority } from "./routing-profile-authority.js"
|
|
import { loadScopedRecoverySource } from "./routing-community-scoped-recovery-source.js"
|
|
import { authenticateRoutingControllerBaseline, routingControllerBaselinePrincipal } from "./routing-controller-baseline.js"
|
|
import { fingerprint, McpError } from "./security.js"
|
|
import { assertPersistentRoutingForwardTransportCharge } from "./routing-reservation-store.js"
|
|
|
|
/** Current native boundary only. Called under existing mm-config server locks.
|
|
* This denies an unrelated REST/MCP native mutation; it creates no debit,
|
|
* dispatch capability, READY, enrollment, inverse or log coverage proof. */
|
|
export async function assertActiveRoutingProfileWriter(serverId:number) {
|
|
const deny=()=>new McpError("STATE_CONFLICT","Активный routing profile требует исходного проверенного шага; приостановите профиль перед ручным изменением",409)
|
|
if(!Number.isSafeInteger(serverId)||serverId<=0)throw deny()
|
|
const rows=(await pool.query(`SELECT id,version,connection_id,grant_expires_at,
|
|
CASE WHEN octet_length(config::text)<=262144 THEN config END AS config,
|
|
CASE WHEN octet_length(state::text)<=131072 THEN state END AS state,
|
|
(config IS NULL OR state IS NULL OR octet_length(config::text)>262144 OR octet_length(state::text)>131072) AS source_bounds_unavailable
|
|
FROM mcp_routing_profiles WHERE enabled AND (config->>'serverId'=$1 OR EXISTS(
|
|
SELECT 1 FROM jsonb_array_elements(config->'candidates') AS candidate
|
|
WHERE candidate->>'jumpHostId'=$1 OR candidate->>'exitNodeId'=$1)) ORDER BY id LIMIT 2`,[String(serverId)])).rows
|
|
// Distinct profile IDs cannot share the one original persistent authority
|
|
// below. Two rows therefore prove ambiguity; never hydrate another999 rows.
|
|
// At most2 * (256KiB config +128KiB state) leave SQL, plus fixed metadata.
|
|
if(rows.length>1||rows.some(row=>row.source_bounds_unavailable===true))throw deny()
|
|
if(!rows.length)return
|
|
let origin
|
|
try { origin=await captureVerifiedMcpServiceExecution() } catch { throw deny() }
|
|
const metadata=await inspectVerifiedMcpServiceExecution(origin)
|
|
if(metadata.phase!=="forward"||metadata.stepIndex!==0)throw deny()
|
|
let input:Record<string,unknown>, recoverySource:Awaited<ReturnType<typeof loadScopedRecoverySource>>|undefined
|
|
if(metadata.operationName==="routing.community.switch"&&metadata.operationVersion===7&&["routing.community.scoped","routing.community"].includes(metadata.serviceName))input=metadata.serviceInput
|
|
else if(metadata.operationName==="operations.recover"&&metadata.serviceName==="routing.community.scoped.recover"){
|
|
const principal=metadata.principal,connection=await effectiveMcpConnection(principal)
|
|
const source=await loadScopedRecoverySource(String(metadata.serviceInput.sourceOperationId),{principal,connection})
|
|
recoverySource=source
|
|
if(metadata.serviceInput.sourceProof!==source.sourceProof || metadata.serviceInput.configurationProof!==source.configuration.checkpointProof ||
|
|
metadata.serviceInput.sourceReceiptId!==source.input.receiptId)throw deny()
|
|
input=source.input
|
|
}else throw deny()
|
|
const authority=routingProfileAuthorityBindingSchema.parse(input.persistentProfileAuthority)
|
|
await assertPersistentScopedProfileAuthority(input)
|
|
for(const row of rows){
|
|
if(row.id!==authority.packet.profileId||row.version!==authority.packet.profileVersion||row.connection_id!==authority.packet.connectionId||!(row.grant_expires_at instanceof Date))throw deny()
|
|
const principal=await routingControllerBaselinePrincipal(row.state?.baseline),connection=await effectiveMcpConnection(principal)
|
|
const baseline=await authenticateRoutingControllerBaseline(row.state?.baseline,row.id,row.version,routeAiProfileSchema.parse(row.config),row.grant_expires_at.toISOString(),{principal,connection})
|
|
const enrolled=baseline.value.packet
|
|
const matches=(identity:{nativeId:unknown;candidateId:unknown;targetProof:unknown;dependencyProof:unknown;sourceProofs:unknown}, targetProof=enrolled.targetProof) =>
|
|
identity.nativeId===enrolled.nativeId && identity.candidateId===enrolled.candidateId && identity.targetProof===targetProof &&
|
|
identity.dependencyProof===enrolled.dependencyProof && fingerprint(identity.sourceProofs)===fingerprint(enrolled.sourceProofs)
|
|
if(metadata.serviceName==="routing.community"){
|
|
const prepared=await loadRoutingMainTransitionPreparation(input)
|
|
if(!matches({nativeId:prepared.nativeId,candidateId:prepared.originalCandidateId,targetProof:prepared.originalTargetProof,
|
|
dependencyProof:prepared.dependencyProof,sourceProofs:prepared.contextSources}))throw deny()
|
|
}else if(!recoverySource){
|
|
await assertScopedCommunityRequest(input)
|
|
if(!matches({nativeId:input.nativeId,candidateId:input.originalCandidateId,targetProof:input.originalTargetProof,
|
|
dependencyProof:input.dependencyProof,sourceProofs:input.contextSources}))throw deny()
|
|
}else{
|
|
// The inverse writes B->A. It is admitted either while the ORIGINAL A
|
|
// baseline awaits its own B transition, or from the EXACT signed B
|
|
// baseline advanced by this very source operation/checkpoint. Never
|
|
// compare inverse before-state B naively to forward original-state A.
|
|
const packet=recoverySource.configuration.packet
|
|
const common={nativeId:packet.nativeId,dependencyProof:packet.dependencyProof,sourceProofs:packet.contextSources}
|
|
const original=matches({...common,candidateId:packet.originalCandidateId,targetProof:packet.originalTargetProof})
|
|
const transition=enrolled.transition
|
|
const ownDesired=!!transition && transition.operationId===recoverySource.row.id && transition.planId===recoverySource.row.source_plan_id &&
|
|
transition.checkpointProof===recoverySource.configuration.checkpointProof &&
|
|
matches({...common,candidateId:packet.desiredCandidateId,targetProof:packet.desiredTargetProof},transition.sourceDesiredTargetProof??enrolled.targetProof)
|
|
if(!original&&!ownDesired)throw deny()
|
|
}
|
|
}
|
|
// The live origin and baseline cannot replace a durable admission charge.
|
|
// Existing reserved forward capability and manual inverse stamp paths remain
|
|
// separate; this check grants nothing and never infers a legacy exemption.
|
|
if (!recoverySource && !routeAiProfileSchema.parse(input.profile).autoRecovery) {
|
|
await assertPersistentRoutingForwardTransportCharge(pool, metadata, metadata.ownerId)
|
|
}
|
|
await inspectVerifiedMcpServiceExecution(origin)
|
|
}
|