feat(mcp): расширить управление маршрутизацией, доступом и обслуживанием базы

This commit is contained in:
Codex committed 2026-11-13 13:37:55 +07:00
1 parent 9240e8be11
commit cffcfada88
125 files changed
+6021 -602

No files matched your search

+4 -2
View File
@@ -74,6 +74,7 @@ function UsersContent() {
const isLive = mode === "live"
const canRead = can("mm:users:read")
const canWrite = can("mm:users:write")
const canManagePermissions = !isLive || can("mm:settings:admin")
const canReadServers = can("mm:servers:read")
const [users, setUsers] = useState<AppUser[]>(() => isLive ? [] : INIT_USERS)
@@ -184,8 +185,8 @@ function UsersContent() {
setSaving(true); setSaveError(null); setOperationNotice(null)
try {
const saved = await saveUserSteps(form, checkpoint, {
create: f => createAppUser(backendUrl, userProfileRequest(f), init()),
update: (id, f) => updateAppUser(backendUrl, id, userProfileRequest(f), init()),
create: f => createAppUser(backendUrl, userProfileRequest(f, canManagePermissions), init()),
update: (id, f) => updateAppUser(backendUrl, id, userProfileRequest(f, canManagePermissions), init()),
read: id => getAppUser(backendUrl, id, init()),
removeBinding: (userId, id) => deleteUserBinding(backendUrl, userId, id, init()),
addBinding: (userId, b) => createUserBinding(backendUrl, userId, {
@@ -328,6 +329,7 @@ function UsersContent() {
users={users}
servers={serverOptions}
isLive={isLive}
canManagePermissions={canManagePermissions}
backendUrl={backendUrl}
saving={saving}
saveError={saveError}
+6 -3
View File
@@ -7,7 +7,10 @@
"test:mcp:module-completion": "tsx src/services/ipsec-server-init-model.test.ts && tsx src/modules/mcp/ipsec-server-init-actions.test.ts && tsx src/services/wireguard-revision-restore.test.ts && tsx src/modules/mcp/vrf-ticket-runtime.test.ts && tsx src/services/ipsec-restore.test.ts && tsx src/services/ipsec-pool-dependencies.test.ts && tsx src/services/vrf-production-transition.test.ts && tsx src/services/vrf-production-action-binding.test.ts && tsx src/services/ipsec-revision-restore-model.test.ts && tsx src/modules/mcp/ipsec-revision-actions.test.ts && tsx src/modules/mcp/service-native-progress.test.ts && tsx src/services/ipsec-revision-restore.test.ts && tsx src/services/ipsec-revision-secret.test.ts",
"test:mcp:module-completion:pg": "node scripts/test-mcp-postgres.mjs --module-completion --restore",
"test:mcp:pg": "node scripts/test-mcp-postgres.mjs",
"test:mcp:users-bindings:pg": "node scripts/test-mcp-postgres.mjs --users-bindings",
"test:mcp:bridge-vlan:pg": "node scripts/test-mcp-postgres.mjs --bridge-vlan-configuration",
"test:mcp:external-process:pg": "node scripts/mcp-external-process/run-owned.mjs",
"test:mcp:bridge-vlan:sdk": "node scripts/mcp-external-process/run-owned.mjs .. bridge-vlan",
"test:traffic-flow:ownership": "tsx src/services/traffic-flow-batch-proof.test.ts && tsx src/services/traffic-flow-owned-batch.test.ts && tsx src/services/traffic-flow-owned-writer.test.ts && tsx src/services/traffic-flow-write-transaction.test.ts && tsx src/services/traffic-flow-writer-exclusion.test.ts && tsx src/services/traffic-flow-drain-protocol.test.ts && tsx src/services/traffic-flow-facts-rebuild-execution.test.ts && tsx src/services/traffic-flow-facts-rebuild-source.test.ts && tsx src/services/traffic-flow-facts-rebuild-classifier.test.ts && tsx src/services/traffic-flow-facts-rebuild-service.test.ts && tsx src/services/traffic-flow-ripe-prune-owned.test.ts && tsx src/services/traffic-flow-engine-owned.test.ts && tsx src/routes/traffic-flow-rebuild.test.ts && tsx src/services/traffic-flow-settings-execution.test.ts",
"test:traffic-flow:rebuild:pg": "node scripts/test-mcp-postgres.mjs --flow-rebuild",
"test:traffic-flow:pg": "node scripts/test-mcp-postgres.mjs --flow-writes",
@@ -34,11 +37,11 @@
"test:probes": "npm run test:speed-workflow && tsx src/services/manual-data-collection-execution.test.ts && tsx src/routes/manual-data-collection.test.ts && tsx src/services/network-host-resolution.test.ts && tsx src/routes/probes.test.ts && tsx src/routes/probes.cancel.test.ts && tsx src/services/ping-measurement.test.ts && tsx src/services/uptime-probe-observations.test.ts && tsx src/services/ping-measurement-sql.test.ts",
"test:exec": "tsx src/services/mikrotik-logs.test.ts && tsx src/services/terminal-diagnostics.test.ts && tsx src/routes/exec.test.ts && tsx src/plugins/auth.exec.test.ts",
"test:auth": "tsx --test src/modules/mcp/routing-manual-inverse-history.test.ts && tsx src/db/application-writer-profile-registry-structure.test.ts && tsx src/db/application-writer-profile-registry-read.test.ts && tsx src/db/application-writer-profile-registry-read-parity.test.ts && tsx src/db/application-writer-profile-format.test.ts && tsx src/plugins/auth-path.test.ts && tsx src/lib/permissions.test.ts && tsx src/plugins/auth-provenance.test.ts && tsx src/services/bandwidth-rest-speed-original.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts && npm run test:mcp",
"test:mcp": "tsx --test src/services/server-poll-error-provenance.test.ts && tsx --test src/modules/mcp/plan-expiry.test.ts src/modules/mcp/result-budget.test.ts src/modules/mcp/result-delivery.test.ts src/modules/mcp/wire-admission.test.ts src/modules/mcp/flow-envelope-allowance.test.ts src/modules/mcp/sdk-terminal-budget.test.ts src/modules/mcp/inflight-owner.test.ts && npm run test:database-source-binding && tsx --test src/modules/mcp/integration-fixture-expiry.test.ts && npm run test:mcp:operation-origin && npm run test:mcp:service-context && npm run test:mcp:operation-locks && tsx --test src/modules/mcp/routing-community-public.test.ts src/modules/mcp/management-transport.test.ts src/modules/mcp/routing-community-context-model.test.ts src/modules/mcp/routing-community-context-native.test.ts ../packages/contracts/src/mcp-routing.test.ts && tsx src/modules/mcp/human-artifact-receipts.test.ts && tsx src/modules/mcp/chr-overlay-readiness.test.ts && tsx src/modules/mcp/chr-recovery-source.test.ts && tsx src/modules/mcp/chr-native-failure.test.ts && tsx src/modules/mcp/ospf-owner-graph.test.ts && tsx src/modules/mcp/routing-measurement-address.test.ts && tsx src/modules/mcp/routing-observation-source.test.ts && tsx src/services/traffic-flow-talkers-model.test.ts && tsx src/services/bgp-prefix-model.test.ts && tsx src/services/vrf-members.test.ts && tsx src/services/routing-gateway-reference.test.ts && tsx src/services/bgp-export-model.test.ts && tsx src/services/traffic-history-model.test.ts && tsx src/services/internet-path-snapshot.test.ts && tsx src/services/internet-path-observation.test.ts && tsx src/modules/mcp/coverage.test.ts && tsx src/modules/mcp/policy.test.ts && tsx src/modules/mcp/events-batch-model.test.ts && tsx src/modules/mcp/operation-revisions.test.ts && tsx src/modules/mcp/native-create-identity.test.ts && tsx src/modules/mcp/ros-step-verification.test.ts && tsx src/modules/mcp/ros-prefix.test.ts && tsx src/modules/mcp/native-conservation-http.test.ts && tsx src/modules/mcp/security.test.ts && tsx src/modules/mcp/state-file-publish.test.ts && tsx src/modules/mcp/routing-model.test.ts && tsx src/modules/mcp/routing-observations-model.test.ts && tsx src/modules/mcp/ospf-health.test.ts && tsx src/modules/mcp/ecmp-preflight.test.ts && tsx src/modules/mcp/recursive-recovery.test.ts && tsx src/modules/mcp/control-path-model.test.ts && tsx src/modules/mcp/routing-community-scoped-model.test.ts && tsx src/modules/mcp/routing-community-scoped-native.test.ts && tsx src/modules/mcp/path-verification.test.ts && tsx src/modules/mcp/database-maintenance.test.ts && tsx src/routes/mcp.test.ts && tsx ../lib/auth-mcp.test.ts && tsx --test src/modules/mcp/routing-controller-fence.test.ts && tsx src/test/chr-vrf-companion-proof.test.ts && tsx src/test/chr-vrf-companion-telemetry.test.ts && tsx src/test/chr-vrf-companion-address-space.test.ts && tsx src/modules/mcp/flow-facts-rebuild-actions.test.ts && tsx src/modules/mcp/ipsec-peer-create-dependencies.test.ts && tsx src/services/ipsec-certificate-update-model.test.ts && tsx src/modules/mcp/ipsec-native-dispatch.test.ts && tsx src/modules/mcp/ipsec-user-update-actions.test.ts && tsx src/modules/mcp/ipsec-server-delete-actions.test.ts && tsx src/services/ipsec-proof-telemetry.test.ts && tsx src/services/vrf-production-admission.test.ts && tsx src/modules/mcp/flow-host-keys-actions.test.ts && tsx src/services/vrf-production-read.test.ts && tsx src/modules/mcp/ipsec-user-create-actions.test.ts && tsx src/modules/mcp/flow-settings-actions.test.ts && tsx src/services/vrf-production-ticket-store.test.ts && npm run test:mcp:routing-baseline",
"test:mcp": "node scripts/vrf-production-profile.test.mjs && node src/services/vrf-production-peer-telemetry.test.mjs && node src/services/vrf-execution-diagnostic.test.mjs && node src/services/vrf-transition-refusal.test.mjs && node src/services/vrf-production-gre-cycle.test.mjs && tsx src/services/vrf-production-gre-link-local.test.ts && tsx src/services/vrf-production-bridge-delete.test.ts && tsx src/services/vrf-production-transition.test.ts && tsx --test src/services/routeros-snapshot-read.test.ts src/services/bridge-vlan-configuration.test.ts src/modules/mcp/bridge-vlan-projection.test.ts src/modules/mcp/vrf-preparation-diagnostic.test.ts && tsx --test src/services/server-poll-error-provenance.test.ts && tsx --test src/modules/mcp/plan-expiry.test.ts src/modules/mcp/result-budget.test.ts src/modules/mcp/result-delivery.test.ts src/modules/mcp/wire-admission.test.ts src/modules/mcp/flow-envelope-allowance.test.ts src/modules/mcp/sdk-terminal-budget.test.ts src/modules/mcp/inflight-owner.test.ts && npm run test:database-source-binding && tsx --test src/modules/mcp/integration-fixture-expiry.test.ts && npm run test:mcp:operation-origin && npm run test:mcp:service-context && npm run test:mcp:operation-locks && tsx --test src/modules/mcp/routing-community-public.test.ts src/modules/mcp/management-transport.test.ts src/modules/mcp/routing-community-context-model.test.ts src/modules/mcp/routing-community-context-native.test.ts ../packages/contracts/src/mcp-routing.test.ts && tsx src/modules/mcp/human-artifact-receipts.test.ts && tsx src/modules/mcp/chr-overlay-readiness.test.ts && tsx src/modules/mcp/chr-recovery-source.test.ts && tsx src/modules/mcp/chr-native-failure.test.ts && tsx src/modules/mcp/ospf-owner-graph.test.ts && tsx src/modules/mcp/routing-measurement-address.test.ts && tsx src/modules/mcp/routing-observation-source.test.ts && tsx src/services/traffic-flow-talkers-model.test.ts && tsx src/services/bgp-prefix-model.test.ts && tsx src/services/vrf-members.test.ts && tsx src/services/routing-gateway-reference.test.ts && tsx src/services/bgp-export-model.test.ts && tsx src/services/traffic-history-model.test.ts && tsx src/services/internet-path-snapshot.test.ts && tsx src/services/internet-path-observation.test.ts && tsx src/modules/mcp/coverage.test.ts && tsx src/modules/mcp/policy.test.ts && tsx src/modules/mcp/events-batch-model.test.ts && tsx src/modules/mcp/operation-revisions.test.ts && tsx src/modules/mcp/native-create-identity.test.ts && tsx src/modules/mcp/ros-step-verification.test.ts && tsx src/modules/mcp/ros-prefix.test.ts && tsx src/modules/mcp/native-conservation-http.test.ts && tsx src/modules/mcp/security.test.ts && tsx src/modules/mcp/state-file-publish.test.ts && tsx src/modules/mcp/routing-model.test.ts && tsx src/modules/mcp/routing-observations-model.test.ts && tsx src/modules/mcp/ospf-health.test.ts && tsx src/modules/mcp/ecmp-preflight.test.ts && tsx src/modules/mcp/recursive-recovery.test.ts && tsx src/modules/mcp/control-path-model.test.ts && tsx src/modules/mcp/routing-community-scoped-model.test.ts && tsx src/modules/mcp/routing-community-scoped-native.test.ts && tsx src/modules/mcp/path-verification.test.ts && tsx src/modules/mcp/database-maintenance.test.ts && tsx src/routes/mcp.test.ts && tsx ../lib/auth-mcp.test.ts && tsx --test src/modules/mcp/routing-controller-fence.test.ts && tsx src/test/chr-vrf-companion-proof.test.ts && tsx src/test/chr-vrf-companion-telemetry.test.ts && tsx src/test/chr-vrf-companion-address-space.test.ts && tsx src/modules/mcp/flow-facts-rebuild-actions.test.ts && tsx src/modules/mcp/ipsec-peer-create-dependencies.test.ts && tsx src/services/ipsec-certificate-update-model.test.ts && tsx src/modules/mcp/ipsec-native-dispatch.test.ts && tsx src/modules/mcp/ipsec-user-update-actions.test.ts && tsx src/modules/mcp/ipsec-server-delete-actions.test.ts && tsx src/services/ipsec-proof-telemetry.test.ts && tsx src/services/vrf-production-admission.test.ts && tsx src/modules/mcp/flow-host-keys-actions.test.ts && tsx src/services/vrf-production-read.test.ts && tsx --test src/services/vrf-production-wireguard-alias.test.ts && tsx src/modules/mcp/ipsec-user-create-actions.test.ts && tsx src/modules/mcp/flow-settings-actions.test.ts && tsx src/services/vrf-production-ticket-store.test.ts && npm run test:mcp:routing-baseline",
"test:wireguard": "tsx src/services/mikrotik-response-bounds.test.ts && tsx --test src/services/wireguard-dependency-read.test.ts && tsx src/services/wireguard-interface-update.test.ts && tsx src/services/wireguard-peer-update.test.ts && npx tsx src/services/wireguard-config.test.ts && npx tsx src/services/wireguard-ipv6.test.ts && npx tsx --test src/services/wireguard-import.test.ts src/services/wireguard-import-apply.test.ts src/services/wireguard-import-revision.test.ts src/services/wireguard-delete.test.ts && npx tsx src/routes/wireguard-import.test.ts && tsx src/services/wireguard-interface-create.test.ts && tsx src/services/wireguard-peer-create.test.ts && tsx src/services/wireguard-import-plan.test.ts && tsx src/services/wireguard-import-execution.test.ts && tsx src/modules/mcp/wireguard-import-actions.test.ts && tsx --test src/services/wireguard-export.test.ts",
"test:traffic-rate": "tsx src/services/traffic-rate.test.ts && tsx src/services/traffic-observation.test.ts && tsx src/services/traffic-freshness.test.ts && tsx src/services/traffic-live.test.ts && tsx src/services/traffic-observation-sql.test.ts && tsx src/services/traffic-history-sql.test.ts && tsx src/services/traffic-live-sql.test.ts && tsx src/services/traffic-volume-sql.test.ts && tsx src/services/traffic-binding-status.test.ts && tsx src/services/traffic-status-sql.test.ts && tsx src/services/traffic-gap.test.ts && tsx src/services/traffic-identity.test.ts && tsx src/services/traffic-membership.test.ts && tsx src/services/traffic-aggregate-peak.test.ts && tsx src/services/traffic-users-cache.test.ts && tsx src/services/traffic-collection-lock.test.ts && tsx src/services/traffic-concurrency-sql.test.ts",
"test:traffic-flow": "tsx --test src/services/traffic-flow-heartbeat.test.ts src/services/traffic-flow-heartbeat-wiring.test.ts src/services/traffic-flow-heartbeat-receiver.test.ts src/services/traffic-flow-ingest-quality.test.ts src/services/traffic-flow-quality-contract.test.ts && tsx src/services/traffic-flow-catalog-read.test.ts && tsx src/routes/traffic-flow-query.test.ts && tsx src/services/traffic-flow-month.test.ts && tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-ipv6-pipeline.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/geoip-download.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-fact-accumulator.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts && tsx src/services/geoip-manual-update.test.ts && tsx src/services/traffic-flow-overlay-model.test.ts && npm run test:traffic-flow:ownership && tsx src/services/traffic-flow-worker-platform.test.ts && tsx src/services/traffic-flow-template-cache.test.ts && tsx --test src/services/traffic-flow-template-lifetime.test.ts && tsx src/services/traffic-flow-protocol-scope.test.ts && npm run test:traffic-flow:read-budget && npm run test:traffic-flow:resource-profile",
"test:users": "tsx ../packages/contracts/src/users.test.ts && tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts",
"test:users": "tsx ../packages/contracts/src/users.test.ts && tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/binding-target.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts",
"test:pg": "tsx src/db/pg-pool.test.ts && tsx src/db/sql-bind.test.ts && tsx src/db/sqlite-json.test.ts && tsx src/db/traffic-flags.test.ts && tsx src/db/pg-schema.test.ts && tsx src/db/scheduler-retention-index.test.ts && tsx src/services/config-revisions.test.ts",
"test:recursive-routes": "tsx src/services/recursive-route-identity.test.ts && tsx src/routes/recursive-routes.test.ts && tsx src/services/recursive-route-catalog.test.ts && tsx src/services/recursive-effective-properties.test.ts && tsx src/services/recursive-apply.test.ts",
"test:config-sync": "tsx src/services/owned-routing-policy.test.ts && tsx src/services/routing-filter-body.test.ts && tsx src/services/bgp-community-policy-model.test.ts && tsx src/services/bgp-connection-read.test.ts && tsx src/services/bgp-filter-binding-model.test.ts && tsx src/services/filter-rules-write-model.test.ts && tsx src/services/filter-rules-mutation.test.ts && tsx src/utils/server-id.test.ts && tsx src/services/config-apply-plan.test.ts && tsx src/services/entity-snapshots.test.ts && tsx src/services/gre-native-identity.test.ts && tsx src/services/gre-ipv6.test.ts && tsx src/routes/gre-ipv6-validation.test.ts && tsx src/services/network-disabled-server.test.ts",
@@ -168,7 +171,7 @@
"test:traffic-flow:read-budget": "tsx --test src/services/traffic-flow-read-budget.test.ts src/modules/mcp/flow-read-quality.test.ts && node scripts/test-flow-read-adapters.mjs",
"test:traffic-flow:read-http:pg": "node scripts/test-flow-read-http-owned.mjs",
"test:traffic-flow:read-source:pg": "tsx scripts/test-flow-read-sql-owned.mts",
"test:mcp:routing-baseline": "tsx --test src/modules/mcp/routing-controller-active-writer.test.ts src/modules/mcp/routing-controller-fence.test.ts src/modules/mcp/routing-community-public.test.ts src/modules/mcp/routing-controller-overlap.test.ts",
"test:mcp:routing-baseline": "tsx --test src/modules/mcp/routing-controller-active-writer.test.ts src/modules/mcp/routing-controller-baseline.test.ts src/modules/mcp/routing-controller-fence.test.ts src/modules/mcp/routing-community-public.test.ts src/modules/mcp/routing-controller-overlap.test.ts",
"test:mcp:routing-baseline:pg": "node scripts/test-routing-controller-baseline.mjs",
"test:mcp:routing-baseline-inverse:pg": "node scripts/test-routing-controller-baseline.mjs --default-scoped",
"test:sqlite-import-control:pg": "node scripts/test-sqlite-import-control-postgres.mjs",
@@ -20,7 +20,7 @@ test('fixed artifact is reproducible, resolves the compiled backend graph, and r
const packageJson=JSON.parse(await readFile(path.join(backend,'package.json'),'utf8'))
await mkdir(projectBackend,{recursive:true});await writeFile(path.join(projectBackend,'package.json'),JSON.stringify({type:'module',dependencies:packageJson.dependencies}))
const first=await buildDatabaseOperator({backendDirectory:backend,outputDirectory:output}),second=await buildDatabaseOperator({backendDirectory:backend,outputDirectory:output})
assert.deepEqual(first,second);assert.equal(first.modules.length,OPERATOR_SOURCES.length);assert.equal(first.modules.length,19);assert.deepEqual(first.runtimeDependencies,['dotenv','pg','zod']);assert.equal(first.backendDistModules.length,12)
assert.deepEqual(first,second);assert.equal(first.modules.length,OPERATOR_SOURCES.length);assert.equal(first.modules.length,20);assert.deepEqual(first.runtimeDependencies,['dotenv','pg','zod']);assert.equal(first.backendDistModules.length,12)
const copied=new Set()
async function copyDist(relative){
if(copied.has(relative))return;copied.add(relative)
@@ -36,10 +36,10 @@ test('fixed artifact is reproducible, resolves the compiled backend graph, and r
assert.deepEqual([...copied].sort(),first.backendDistModules.map(item=>item.file).sort())
// Exercise the real build entry point with its default backend/output paths.
const scripts=path.join(projectBackend,'scripts')
assert.equal(await stageDatabaseOperatorScripts(scripts,path.join(backend,'scripts')),21)
assert.equal(await stageDatabaseOperatorScripts(scripts,path.join(backend,'scripts')),OPERATOR_SOURCES.length+2)
const buildScript=path.join(scripts,'build-database-operator.mjs')
const built=spawnSync(process.execPath,[buildScript],{cwd:temporary,env:{...process.env,NODE_OPTIONS:''},encoding:'utf8',timeout:30000})
assert.equal(built.status,0,built.stderr);assert.equal(JSON.parse(built.stdout.trim()).modules,19)
assert.equal(built.status,0,built.stderr);assert.equal(JSON.parse(built.stdout.trim()).modules,20)
assert.deepEqual(JSON.parse(await readFile(path.join(output,'build-manifest.json'),'utf8')),first)
const configFile=path.join(projectBackend,'dist','config.js'),originalConfig=await readFile(configFile,'utf8')
for(const injected of ["import 'tsx';","import '../src/config.js';","import '../outside.js';"]){
@@ -61,9 +61,9 @@ test('fixed artifact is reproducible, resolves the compiled backend graph, and r
const modulesToImport=first.modules.map(item=>path.join(output,item.file))
const importer=path.join(temporary,'import-all.mjs');await writeFile(importer,`import {pathToFileURL} from 'node:url';for(const file of ${JSON.stringify(modulesToImport)})await import(pathToFileURL(file).href);console.log(JSON.stringify({imported:${modulesToImport.length},readyAuthorized:false}));`)
const imported=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,importer],{cwd:temporary,env,encoding:'utf8',timeout:30000})
assert.equal(imported.status,0,imported.stderr);assert.equal(JSON.parse(imported.stdout.trim()).imported,19);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0)
assert.equal(imported.status,0,imported.stderr);assert.equal(JSON.parse(imported.stdout.trim()).imported,20);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0)
const refusals=[]
for(const [entry,event,args] of [['database-writer-bootstrap-operator.mjs','application-writer-bootstrap-refused',['invalid']],['database-writer-profile-operator.mjs','application-writer-profile-publication-refused',['invalid']],['database-writer-profile-operator.mjs','database-maintenance-command-refused',['maintenance-inspect']],['database-writer-profile-operator.mjs','database-maintenance-command-refused',['maintenance-resolve']]]){
for(const [entry,event,args] of [['database-writer-bootstrap-operator.mjs','application-writer-bootstrap-refused',['invalid']],['database-writer-profile-operator.mjs','application-writer-profile-publication-refused',['invalid']],['database-writer-profile-operator.mjs','database-maintenance-command-refused',['maintenance-inspect']],['database-writer-profile-operator.mjs','database-maintenance-command-refused',['maintenance-resolve']],['database-writer-profile-operator.mjs','database-source-activation-refused',['source-activate']]]){
const child=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,path.join(output,entry),...args],{cwd:temporary,env,encoding:'utf8',timeout:30000})
assert.equal(child.status,1,child.stderr);assert.equal(child.stdout,'');assert.equal(JSON.parse(child.stderr.trim()).event,event);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0);refusals.push(event)
}
@@ -2,7 +2,7 @@
export const OPERATOR_SOURCES=Object.freeze([
'database-writer-bootstrap-operator.mjs','database-writer-profile-operator.mts',
'writer-profile/operator-files.mjs','writer-profile/operator-contract.mjs','writer-profile/publisher.mjs','writer-profile/context.mjs',
'writer-profile/protected-source-registry.mts','writer-profile/maintenance-resolution.mts',
'writer-profile/protected-source-registry.mts','writer-profile/maintenance-resolution.mts','writer-profile/source-activation.mts',
'writer-profile/catalogue.mts','writer-profile/catalogue-value-bridge.ts','writer-profile/catalogue-projection.ts','writer-profile/catalogue-leaves.ts',
'writer-profile/application-writer-profile-catalogue.ts','writer-profile/application-writer-profile-catalogue-values.ts',
'writer-profile/application-writer-profile-catalogue-parent-policy.ts','writer-profile/application-writer-profile-canonical.ts',
@@ -8,10 +8,14 @@ import {approval,reconciliationApproval,poolOptions,roleKeys,assertJsDriverEnvir
import {deploymentRequest} from './database-writer-bootstrap-operator.mjs'
const hex=(v:unknown)=>typeof v==='string'&&/^[a-f0-9]{64}$/.test(v)
const parse=(b:Buffer)=>JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(b))
const sourceCommands=['source-activate','source-revoke','source-status']
export async function main(args:string[]){
if(sourceCommands.includes(args[0]))return (await import('./writer-profile/source-activation.mjs')).sourceActivationMain(args)
if(['maintenance-inspect','maintenance-resolve'].includes(args[0]))return (await import('./writer-profile/maintenance-resolution.mjs')).maintenanceMain(args)
const activePreflight=args[0]==='source-preflight',preflight=activePreflight||args[0]==='preflight'
const flags=['bootstrap','bootstrap-sha256','approval','approval-sha256','definition','definition-sha256','transport','transport-sha256','ca','source-registry','source-spki-sha256','journal']
assert(['publish','reconcile','preflight'].includes(args[0])&&args.length===1+2*flags.length)
if(activePreflight)flags.push('activation-directory','activation-sha256')
assert((preflight||['publish','reconcile'].includes(args[0]))&&args.length===1+2*flags.length)
assertJsDriverEnvironment(process.env)
const values=new Map<string,string>()
for(let i=1;i<args.length;i+=2){assert(args[i].startsWith('--')&&flags.includes(args[i].slice(2))&&!values.has(args[i].slice(2))&&args[i+1]);values.set(args[i].slice(2),args[i+1])}
@@ -32,34 +36,49 @@ export async function main(args:string[]){
// All explicit trust files are checked before app helper/PG imports. No .env URL is used.
// Existing source helper loads config/dotenv. Complete it BEFORE pg's first
// import so a newly loaded NODE_PG_FORCE_NATIVE cannot select another driver.
const {createDatabaseSourceBindingVerifier}=await import('../src/db/database-source-binding.js')
const {createProtectedDatabaseSourceBindingVerifier}=await import('./writer-profile/protected-source-registry.mjs')
assertJsDriverEnvironment(process.env)
const [{default:pg},{createConnectedPublisher},{collectSource44Catalogue,approvedDefinition}]=await Promise.all([
const [{default:pg},{createConnectedPublisher,requireCurrentCatalogueDefinition},{collectSource44Catalogue,approvedDefinition}]=await Promise.all([
import('pg'),import('./writer-profile/publisher.mjs'),import('./writer-profile/catalogue.mjs')])
assertJsDriverEnvironment(process.env)
assert.equal(approvedDefinition(definition),pinned.definitionSha256)
const installation=Object.freeze({...subject,roles:Object.fromEntries(roleKeys.map(k=>[k,request.roles[k].signedOid])),roleBindings:request.roles,expectedSource:request.source})
const reviewedDefinition=approvedDefinition(definition)
assert.equal(reviewedDefinition.definitionSha256,pinned.definitionSha256)
if(args[0]!=='reconcile')requireCurrentCatalogueDefinition(pinned,reviewedDefinition)
const installation=Object.freeze({...subject,roles:Object.fromEntries(roleKeys.map(k=>[k,request.roles[k].signedOid])),roleBindings:request.roles,expectedSource:request.source,tls:Object.freeze({...request.tls})})
const sourcePin={registryDirectory:get('source-registry'),installationId:subject.installationId,publicKeySpkiSha256:get('source-spki-sha256'),currentHeadSha256:request.sourceHeadSha256}
// Preflight is the read-only lifecycle entry for protected registry evidence.
// A refused protection check never falls back to the signature-only verifier.
const verifier=args[0]==='preflight'
?(await import('./writer-profile/protected-source-registry.mjs')).createProtectedDatabaseSourceBindingVerifier(sourcePin)
:createDatabaseSourceBindingVerifier(sourcePin)
const publisher=createConnectedPublisher(installation,(s:Parameters<typeof collectSource44Catalogue>[0])=>collectSource44Catalogue(s,installation),verifier,pinned)
const pool=new pg.Pool(options)
// A pool-level async error never becomes raw stdout or a retry signal.
let asyncFailure=false;pool.on('error',()=>{asyncFailure=true})
let result,primary:unknown,failed=false
try{result=args[0]==='preflight'?await publisher.preflightCurrentProfile(pool,{deadlineAt:Date.now()+120000}):args[0]==='reconcile'?await publisher.reconcilePendingPublication(pool,{journalDirectory:get('journal'),deadlineAt:Date.now()+120000}):await publisher.publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256:pinned.expectedHeadSha256,journalDirectory:get('journal'),validityMs:pinned.validityMs,deadlineAt:Date.now()+120000})}
catch(error){primary=error;failed=true}
try{await pool.end()}catch(error){primary??=error;failed=true}
if(args[0]==='preflight'&&result?.state==='matched'&&result.platformProtectionVerified===true&&!failed&&!asyncFailure)return {event:'application-writer-profile-preflight',outcome:'matched',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,sourceMatched:true,rolesMatched:true,catalogueMatched:true,platformProtectionVerified:true,writerAuthorized:false,cleanupUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
if(result?.outcome==='committed')return {event:'application-writer-profile-publication',outcome:'committed',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,
cleanupUnconfirmed:result.cleanupUnconfirmed||asyncFailure||failed,journalUnconfirmed:!!result.journalUnconfirmed,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
if(args[0]==='reconcile'&&result&&!failed&&!asyncFailure&&['idle','confirmed','not-committed'].includes(result.state))return {event:'application-writer-profile-reconciliation',outcome:result.state,headSha256:result.headSha256??null,cleanupUnconfirmed:false,journalUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
throw primary??Error('Publisher terminal unconfirmed')
// Use the same protected registry evidence for inspection and publication.
const verifier=createProtectedDatabaseSourceBindingVerifier(sourcePin)
async function connected(sourceVerifier:typeof verifier,deadlineAt:number){
const publisher=createConnectedPublisher(installation,(s:Parameters<typeof collectSource44Catalogue>[0])=>collectSource44Catalogue(s,installation),sourceVerifier,pinned,reviewedDefinition)
const pool=new pg.Pool(options)
// A pool-level async error never becomes raw stdout or a retry signal.
let asyncFailure=false;pool.on('error',()=>{asyncFailure=true})
let result,primary:unknown,failed=false
try{result=preflight?await publisher.preflightCurrentProfile(pool,{deadlineAt}):args[0]==='reconcile'?await publisher.reconcilePendingPublication(pool,{journalDirectory:get('journal'),deadlineAt}):await publisher.publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256:pinned.expectedHeadSha256,journalDirectory:get('journal'),validityMs:pinned.validityMs,deadlineAt})}
catch(error){primary=error;failed=true}
try{await pool.end()}catch(error){primary??=error;failed=true}
if(preflight&&result?.state==='matched'&&result.platformProtectionVerified===true&&!failed&&!asyncFailure)return {event:'application-writer-profile-preflight',outcome:'matched',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,sourceMatched:true,rolesMatched:true,catalogueMatched:true,platformProtectionVerified:true,writerAuthorized:false,cleanupUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
if(result?.outcome==='committed')return {event:'application-writer-profile-publication',outcome:'committed',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,
cleanupUnconfirmed:result.cleanupUnconfirmed||asyncFailure||failed,journalUnconfirmed:!!result.journalUnconfirmed,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
if(args[0]==='reconcile'&&result&&!failed&&!asyncFailure&&['idle','confirmed','not-committed'].includes(result.state))return {event:'application-writer-profile-reconciliation',outcome:result.state,headSha256:result.headSha256??null,cleanupUnconfirmed:false,journalUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
throw primary??Error('Publisher terminal unconfirmed')
}
const deadlineAt=Date.now()+120000
if(!activePreflight)return connected(verifier,deadlineAt)
const {withActiveSourcePreflight}=await import('./writer-profile/source-activation.mjs')
const publisherRole=request.roles.publisher.name
const expectedSource={...request.source,databaseOid:Number(request.source.databaseOid),sessionUser:publisherRole,currentUser:publisherRole}
const result=await withActiveSourcePreflight({directory:get('activation-directory'),expectedSha256:get('activation-sha256'),sourcePin,expectedSource,tlsSpkiSha256:request.tls.spkiSha256,deadlineAt},verifier,
guardedVerifier=>connected(guardedVerifier,deadlineAt))
assert.equal(result.event,'application-writer-profile-preflight')
return {...result,event:'application-writer-profile-source-preflight',sourceActivationMatched:true}
}
if(process.argv[1]&&import.meta.url===pathToFileURL(path.resolve(process.argv[1])).href){
try{const result=await main(process.argv.slice(2));console.log(JSON.stringify(result));if(result.cleanupUnconfirmed||result.journalUnconfirmed)process.exitCode=2}
catch{console.error(JSON.stringify({event:['maintenance-inspect','maintenance-resolve'].includes(process.argv[2])?'database-maintenance-command-refused':'application-writer-profile-publication-refused',automaticReplay:false,inspectPendingBeforeRetry:true,readyAuthorized:false}));process.exitCode=1}
catch(error){
const sourceCommand=sourceCommands.includes(process.argv[2])
const unknown=sourceCommand&&(error as NodeJS.ErrnoException|null)?.code==='SOURCE_ACTIVATION_TERMINAL_UNKNOWN'
console.error(JSON.stringify({event:sourceCommand?(unknown?'database-source-activation-unknown':'database-source-activation-refused'):
['maintenance-inspect','maintenance-resolve'].includes(process.argv[2])?'database-maintenance-command-refused':process.argv[2]==='source-preflight'?'application-writer-profile-source-preflight-refused':'application-writer-profile-publication-refused',automaticReplay:false,inspectPendingBeforeRetry:true,readyAuthorized:false}));process.exitCode=unknown?2:1
}
}
@@ -6,22 +6,38 @@ import fs from 'node:fs/promises'
import {createRequire} from 'node:module'
import {runInNewContext} from 'node:vm'
import {EventEmitter} from 'node:events'
import {generateKeyPairSync} from 'node:crypto'
import path from 'node:path'
import * as format from '../src/db/application-writer-profile-format.ts'
import {MIGRATION_MANIFEST,migrationManifestDigest} from '../src/db/migration-manifest.ts'
import {readCurrentApplicationWriterProfile} from '../src/db/application-writer-profile-registry-read.ts'
import {syntheticFixture,stagedModelClient} from '../src/db/application-writer-profile-registry-read-model.test.ts'
import {approvedDefinition} from './writer-profile/catalogue.mts'
import {BUILD44_PROVENANCE,FIXED44_ROLE_TEMPLATE} from './writer-profile/fixed44-build-definition.ts'
const require=createRequire(new URL('../package.json',import.meta.url)),ts=require('typescript'),pg=require('pg')
const candidate=await fs.readFile(new URL('./writer-profile/publisher.mjs',import.meta.url),'utf8')
interface PreflightDiagnostics {state:string;headSha256:string;sourceMatched:boolean;rolesMatched:boolean;catalogueMatched:boolean;platformProtectionVerified:boolean;writerAuthorized:boolean;generationAuthenticated:boolean;readyAuthorized:boolean}
interface PublisherModule {createConnectedPublisher(installation:unknown,collect:()=>Promise<unknown>,sourceVerifier:{verify:()=>Promise<{platformProtectionVerified:boolean}>},approval:unknown):{preflightCurrentProfile(pool:unknown,options:{deadlineAt:number}):Promise<PreflightDiagnostics>}}
interface OperatorModule {main(args:string[]):Promise<{event:string;outcome:string;platformProtectionVerified:boolean;writerAuthorized:boolean;readyAuthorized:boolean}>}
interface PublisherModule {
requireCurrentCatalogueDefinition(approval:unknown,definition:unknown):string
createConnectedPublisher(installation:unknown,collect:()=>Promise<unknown>,sourceVerifier:{verify:()=>Promise<{platformProtectionVerified:boolean}>},approval:unknown,definition:unknown):{
preflightCurrentProfile(pool:unknown,options:{deadlineAt:number}):Promise<PreflightDiagnostics>
publishCurrentProfile(pool:unknown,options:{privateKeyFile:string;expectedHeadSha256:null;journalDirectory:string;deadlineAt:number}):Promise<{outcome:string}>
}
}
interface OperatorModule {main(args:string[]):Promise<{event:string;outcome:string;platformProtectionVerified:boolean;writerAuthorized:boolean;readyAuthorized:boolean;sourceActivationMatched?:boolean}>}
function compile<T>(source:string,modules:Record<string,unknown>):T{
const exports:Record<string,unknown>={};const compiled=ts.transpileModule(source.split('\nif(process.argv[1]')[0].replaceAll('import.meta.url',JSON.stringify('file:///candidate-not-cli.mts')),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText
runInNewContext(compiled,{exports,require:(name:string)=>{assert(name in modules,'Unapproved import '+name);return modules[name]},Buffer,URL,TextDecoder,Date,setTimeout,clearTimeout,Object,Error,AggregateError,Promise,Map,Set,process:{env:{},argv:[]}}, {filename:'candidate.mjs'})
// The isolated VM loads the exact source above; tests exercise this narrow export contract.
return exports as T
}
function fixture(fault=''){
const f=syntheticFixture('1'),catalogue={catalogSha256:'f'.repeat(64),definitionSha256:f.policy.definitionSha256,leafCount:117},security={sha256:'e'.repeat(64),roles:{...f.policy.roles}}
const frozenDefinition=approvedDefinition({format:'application-writer-source44-definition-approval-v1',provenance:BUILD44_PROVENANCE,template:FIXED44_ROLE_TEMPLATE})
const frozenMigrations=MIGRATION_MANIFEST.slice(0,44),markerIds=frozenMigrations.map(pin=>pin.file.slice(0,-4))
const markerSql='SELECT id FROM public.schema_migrations ORDER BY id COLLATE "C" LIMIT $1'
function fixture(fault='',forPublication=false){
const f=syntheticFixture('1'),catalogue={...frozenDefinition,catalogSha256:'f'.repeat(64),leafCount:117},security={sha256:'e'.repeat(64),roles:{...f.policy.roles}}
const currentBuild=fault==='current-build'||fault==='current-approved-frozen-definition'
f.policy.manifestSha256=frozenDefinition.manifestSha256;f.policy.definitionSha256=frozenDefinition.definitionSha256
f.policy.catalogSha256=format.sha(format.canonical({base:catalogue.catalogSha256,security:security.sha256}))
if(fault==='signed-source')f.policy.source.databaseOid='999'
if(fault==='signed-role')f.policy.roles.operator='99'
@@ -34,30 +50,49 @@ function fixture(fault=''){
f.state.current[0].head_sha256=top.head_sha256;f.state.policy=[{policy_sha256:hash,policy_bytes:raw}]
if(fault==='tamper')f.state.policy[0].policy_bytes=Buffer.from(raw.toString().replace('synthetic','tampered'))
if(fault==='empty'){f.state.current=[];f.state.latest=[];f.state.policy=[]}
const model=stagedModelClient(f.state),queries:string[]=[],events:string[]=[];let clocks=0
const model=stagedModelClient(f.state),queries:string[]=[],events:string[]=[],markerLimits:unknown[][]=[]
let clocks=0,checkouts=0
const client=Object.assign(new EventEmitter(),{query:async(sql:string,values?:unknown[])=>{
queries.push(sql)
if(sql.includes('clock_timestamp()')&&++clocks===2&&fault==='late-clock')return {rows:[{now:'1'}]}
if(sql===markerSql){
markerLimits.push(values??[])
const ids=fault==='marker-missing'?markerIds.slice(0,-1):fault==='marker-extra'||fault==='marker-drift'&&markerLimits.length===2?[...markerIds,'9999_foreign']:markerIds
return {rows:ids.map(id=>({id}))}
}
if(sql.startsWith('WITH')||sql.includes('clock_timestamp()'))return model.client.query(sql,values)
if(sql.startsWith('SELECT pg_catalog.pg_advisory_unlock'))return {rows:[{released:true}]}
if(sql==='ROLLBACK')return {rows:[],command:fault==='rollback-ack'?'':'ROLLBACK'}
if(sql.startsWith('INSERT INTO mmapp_control.current'))return {rows:[{id:1}]}
return {rows:[],command:sql.split(' ')[0]}
},release:(bad:boolean)=>{events.push('release:'+bad)}})
const pool=new pg.Pool();pool.connect=async()=>client
const pool=new pg.Pool();pool.connect=async()=>{checkouts++;return client}
const source={systemIdentifier:'123',databaseOid:'456',databaseName:'synthetic',serverVersionNum:180004,inRecovery:false}
let contexts=0,verifications=0
const installation={...f.subject,expectedSource:source,roleBindings:{publisher:{name:'publisher',signedOid:'14'}}}
const approval={expiresAt:Date.now()+60000,manifestSha256:'b'.repeat(64),definitionSha256:catalogue.definitionSha256,catalogSha256:format.sha(format.canonical({base:catalogue.catalogSha256,security:security.sha256}))}
const fixtureKey=forPublication?generateKeyPairSync('ed25519'):null
const subject=fixtureKey?{...f.subject,publicKeyPem:fixtureKey.publicKey.export({format:'pem',type:'spki'}).toString(),publicKeySpkiSha256:format.sha(fixtureKey.publicKey.export({format:'der',type:'spki'}))}:f.subject
const installation={...subject,expectedSource:source,roleBindings:{publisher:{name:'publisher',signedOid:'14'}}}
const approval={expiresAt:Date.now()+60000,manifestSha256:['approval-manifest','current-approved-frozen-definition'].includes(fault)?migrationManifestDigest():frozenDefinition.manifestSha256,definitionSha256:catalogue.definitionSha256,catalogSha256:format.sha(format.canonical({base:catalogue.catalogSha256,security:security.sha256}))}
const definition=fault==='static-definition'?{...frozenDefinition,definitionSha256:'0'.repeat(64)}:frozenDefinition
const privateKeyFile=path.resolve('modeled-fixture-key.pem'),journalDirectory=path.resolve('modeled-journal'),files=new Map<string,Buffer>()
if(fixtureKey)files.set(privateKeyFile,Buffer.from(fixtureKey.privateKey.export({format:'pem',type:'pkcs8'})))
function file(filename:string){const value=files.get(filename);if(!value)throw Object.assign(Error('Fixture file absent'),{code:'ENOENT'});return value}
function stat(filename:string){return {size:file(filename).length,dev:1,ino:1,isFile:()=>true,isSymbolicLink:()=>false}}
const modules={
pg,'node:crypto':require('node:crypto'),'node:fs/promises':{},'node:path':require('node:path'),
'./operator-files.mjs':{withJournalLease:()=>{throw Error('Preflight must not use publication journal')},writeFile:()=>{throw Error('No publication writes')}},
pg,'node:crypto':require('node:crypto'),'node:fs/promises':{realpath:async(file:string)=>file,lstat:async(filename:string)=>stat(filename),unlink:async(filename:string)=>files.delete(filename),open:async(filename:string)=>({stat:async()=>stat(filename),readFile:async()=>Buffer.from(file(filename)),close:async()=>{}})},'node:path':path,
'./operator-contract.mjs':{retainPublisherTlsPeer:(actual:unknown)=>{
assert.equal(actual,client)
if(fault==='initial-tls')throw Error('TLS peer refused')
return ()=>{if(fault==='lost-tls'&&verifications===2)throw Error('Retained TLS lost')}
}},
'./operator-files.mjs':{withJournalLease:async(_directory:string,work:()=>Promise<unknown>)=>{assert(forPublication);events.push('journal-lease');return work()},writeFile:async(filename:string,value:Buffer)=>{assert(forPublication);files.set(filename,value)}},
'../../src/db/database-source-observation.js':{captureDatabaseSourceObservation:async()=>{events.push('capture-source');return {}},assertDatabaseSourceObservationCurrent:async()=>{events.push('fresh-source');if(fault==='late-source')throw Error('Source changed')}},
'../../src/db/migration-manifest.js':{MIGRATION_MANIFEST:[],migrationManifestDigest:()=> 'b'.repeat(64)},
'../../src/db/migration-manifest.js':{MIGRATION_MANIFEST:currentBuild?MIGRATION_MANIFEST:frozenMigrations,migrationManifestDigest:()=>currentBuild?migrationManifestDigest():frozenDefinition.manifestSha256},
'../../src/db/application-writer-profile-format.js':format,
'./context.mjs':{ownedContext:async()=>{contexts++;if(fault==='late-actor'&&contexts===2)throw Error('Actor changed');return {pid:1,source}},sameOwned:(a:unknown,b:unknown)=>assert.deepEqual(a,b),sourceCoordinates:(v:{source:typeof source})=>v.source,protectedStructure:async()=>{events.push('roles');if(fault==='role-structure')throw Error('Unsafe role');return security},readCurrent:readCurrentApplicationWriterProfile,HISTORY:'history',CURRENT:'current'},
'./context.mjs':{ownedContext:async()=>{contexts++;if(fault==='late-actor'&&contexts===2)throw Error('Actor changed');return {pid:1,source}},sameOwned:(a:unknown,b:unknown)=>assert.deepEqual(a,b),sourceCoordinates:(v:{source:typeof source})=>v.source,protectedStructure:async()=>{events.push('roles');if(fault==='role-structure')throw Error('Unsafe role');return security},readCurrent:forPublication?async()=>null:readCurrentApplicationWriterProfile,HISTORY:'history',CURRENT:'current'},
}
const api=compile<PublisherModule>(candidate,modules).createConnectedPublisher(installation,async()=>{events.push('catalogue');if(fault==='catalogue-eof')throw Error('Incomplete EOF');return catalogue},{verify:async()=>{verifications++;events.push('verify-source');if(fault==='source-verifier')throw Error('Unsigned source');if(fault==='late-expiry'&&verifications===2)approval.expiresAt=1;return {platformProtectionVerified:!(fault==='initial-unprotected'&&verifications===1||fault==='terminal-unprotected'&&verifications===2)}}},approval)
return {run:()=>api.preflightCurrentProfile(pool,{deadlineAt:Date.now()+30000}),queries,events,f}
const api=compile<PublisherModule>(candidate,modules).createConnectedPublisher(installation,async()=>{events.push('catalogue');if(fault==='catalogue-eof')throw Error('Incomplete EOF');return fault==='catalogue-manifest'?{...catalogue,manifestSha256:migrationManifestDigest()}:fault==='catalogue-definition'?{...catalogue,definitionSha256:'0'.repeat(64)}:catalogue},{verify:async()=>{verifications++;events.push('verify-source');if(fault==='source-verifier')throw Error('Unsigned source');if(fault==='late-expiry'&&verifications===2)approval.expiresAt=1;return {platformProtectionVerified:!(fault==='initial-unprotected'&&verifications===1||fault==='terminal-unprotected'&&verifications===2)}}},approval,definition)
return {run:()=>api.preflightCurrentProfile(pool,{deadlineAt:Date.now()+30000}),publish:()=>api.publishCurrentProfile(pool,{privateKeyFile,journalDirectory,expectedHeadSha256:null,deadlineAt:Date.now()+30000}),queries,events,markerLimits,files,get checkouts(){return checkouts},f}
}
test('current44 preflight authenticates signed current and actual component inputs on one read-only lease',async()=>{
const f=fixture(),result=await f.run();assert.equal(result.state,'matched');assert.equal(result.headSha256,f.f.state.current[0].head_sha256)
@@ -65,9 +100,17 @@ test('current44 preflight authenticates signed current and actual component inpu
assert.equal(result.platformProtectionVerified,true)
assert.equal(result.writerAuthorized,false);assert.equal(result.generationAuthenticated,false);assert.equal(result.readyAuthorized,false)
assert.equal(f.queries.filter(q=>q.startsWith('BEGIN')).length,1);assert(f.queries.includes('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'));assert.equal(f.queries.filter(q=>q==='ROLLBACK').length,1)
assert.equal(format.canonical(f.markerLimits),'[[45]]')
assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT|CREATE|ALTER|DROP)\b/.test(q)));assert.equal(f.events.at(-1),'release:false')
assert.deepEqual(f.events.slice(0,6),['capture-source','verify-source','roles','catalogue','verify-source','fresh-source'])
})
test('publisher refuses before SQL on a wrong peer and stops SQL when the retained TLS socket is lost',async()=>{
const initial=fixture('initial-tls')
await assert.rejects(initial.run());assert.deepEqual(initial.queries,[]);assert.deepEqual(initial.events,['release:true'])
const lost=fixture('lost-tls')
await assert.rejects(lost.run());assert.equal(lost.events.at(-1),'release:true')
assert(!lost.queries.some(q=>q==='ROLLBACK'||q.includes('pg_advisory_unlock')))
})
for(const fault of ['initial-unprotected','terminal-unprotected'])test('preflight cannot promote '+fault+' into protected evidence',async()=>{
const f=fixture(fault),result=await f.run();assert.equal(result.platformProtectionVerified,false)
assert.equal(result.writerAuthorized,false);assert.equal(result.readyAuthorized,false)
@@ -76,23 +119,64 @@ for(const fault of ['initial-unprotected','terminal-unprotected'])test('prefligh
for(const fault of ['tamper','empty','expired','signed-source','signed-role','signed-catalogue','signed-definition','signed-manifest','role-structure','catalogue-eof','source-verifier','late-source','late-actor','late-expiry','late-clock','rollback-ack'])test('preflight refuses '+fault+' without any write or replay',async()=>{
const f=fixture(fault);await assert.rejects(f.run());assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT|CREATE|ALTER|DROP)\b/.test(q)));assert.equal(f.queries.filter(q=>q.startsWith('BEGIN')).length,1)
})
test('existing operator main dispatches preflight only after protected input validation, without private key or publication',async()=>{
for(const fault of ['current-build','current-approved-frozen-definition','approval-manifest','static-definition'])test('incompatible static manifest/definition refuses '+fault+' before either pool checkout or publication lease',async()=>{
const f=fixture(fault,true)
await assert.rejects(f.run(),/Approved catalogue definition is not bound/)
await assert.rejects(f.publish(),/Approved catalogue definition is not bound/)
assert.equal(f.checkouts,0);assert.deepEqual(f.events,[]);assert.deepEqual(f.queries,[])
})
for(const fault of ['catalogue-manifest','catalogue-definition','marker-missing','marker-extra'])test('retained preflight refuses '+fault+' and rolls back',async()=>{
const f=fixture(fault)
await assert.rejects(f.run(),error=>error instanceof Error&&/Catalogue evidence differs|Exact current migration marker/.test((error.cause as Error).message))
assert.equal(f.checkouts,1);assert.equal(f.queries.filter(q=>q==='ROLLBACK').length,1)
assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT)\b/.test(q)))
if(fault.startsWith('marker-'))assert.equal(format.canonical(f.markerLimits),'[[45]]')
else assert.deepEqual(f.markerLimits,[])
})
test('publication checks exact marker set on both retained transactions and refuses late marker drift before registry writes',async()=>{
const positive=fixture('',true)
assert.equal((await positive.publish()).outcome,'committed')
assert.equal(positive.checkouts,1);assert.equal(format.canonical(positive.markerLimits),'[[45],[45]]')
assert.equal(positive.queries.filter(q=>q==='COMMIT').length,2)
const drift=fixture('marker-drift',true)
await assert.rejects(drift.publish(),error=>error instanceof Error&&/Exact current migration marker/.test((error.cause as Error).message))
assert.equal(drift.checkouts,1);assert.equal(format.canonical(drift.markerLimits),'[[45],[45]]')
assert.equal(drift.queries.filter(q=>q==='COMMIT').length,1);assert.equal(drift.queries.filter(q=>q==='ROLLBACK').length,1)
assert(!drift.queries.some(q=>/^(?:INSERT|UPDATE|DELETE)\b/.test(q)))
assert([...drift.files.keys()].some(filename=>filename.endsWith('pending-publication.json')))
})
test('operator requires the protected source verifier for preflight, publication and reconciliation',async()=>{
const cli=await fs.readFile(new URL('./database-writer-profile-operator.mts',import.meta.url),'utf8'),f=syntheticFixture('1'),reads:string[]=[],events:string[]=[]
const request={installationId:f.subject.installationId,roles:Object.fromEntries(['runtime','operator','capture','publisher','control'].map((k,i)=>[k,{name:k,signedOid:String(i+11)}])),source:f.policy.source,sourceHeadSha256:'a'.repeat(64),tls:{caSha256:'a'.repeat(64)}}
const files:Record<string,unknown>={bootstrap:{format:'application-writer-bootstrap-v1',publicationAuthorized:false,readyAuthorized:false,deploymentRequest:request,verificationSubject:f.subject},approval:{expiresAt:Date.now()+60000,definitionSha256:'b'.repeat(64)},definition:{},transport:{},ca:'fixture'}
let failEnd=false,asyncError=false,protectedRefused=false
const request={installationId:f.subject.installationId,roles:Object.fromEntries(['runtime','operator','capture','publisher','control'].map((k,i)=>[k,{name:k,signedOid:String(i+11)}])),source:f.policy.source,sourceHeadSha256:'a'.repeat(64),tls:{caSha256:'a'.repeat(64),spkiSha256:'d'.repeat(64)}}
const files:Record<string,unknown>={bootstrap:{format:'application-writer-bootstrap-v1',publicationAuthorized:false,readyAuthorized:false,deploymentRequest:request,verificationSubject:f.subject},approval:{expiresAt:Date.now()+60000,definitionSha256:'b'.repeat(64)},definition:{},transport:{},ca:'fixture','signing-private.pem':'fixture'}
let failEnd=false,asyncError=false,protectedRefused=false,activationRefused=false,incompatibleDefinition=false
const retainedPools:Pool[]=[]
class Pool extends EventEmitter{constructor(){super();retainedPools.push(this)}async end(){events.push('pool-end');if(failEnd)throw Error('Pool cleanup unconfirmed')}}
const result={state:'matched',epoch:'1',headSha256:'c'.repeat(64),notAfter:Date.now()+60000,platformProtectionVerified:true}
const protectedVerifier={verify:async()=>({platformProtectionVerified:true})}
const guardedVerifier={...protectedVerifier}
const modules={
'node:assert/strict':assert,'node:path':require('node:path'),'node:url':require('node:url'),'node:crypto':require('node:crypto'),
'./writer-profile/operator-files.mjs':{readProtected:async(file:string)=>{reads.push(file);assert(file in files);return Buffer.from(JSON.stringify(files[file]))},protectedDirectory:async()=>{},sha:format.sha},
'./writer-profile/operator-contract.mjs':{approval:(value:unknown)=>{events.push('approval');return value},reconciliationApproval:()=>{throw Error('No expired reconciliation bypass')},poolOptions:()=>{events.push('transport');return {}},roleKeys:['runtime','operator','capture','publisher','control'],assertJsDriverEnvironment:()=>{}},
'./database-writer-bootstrap-operator.mjs':{deploymentRequest:(value:unknown)=>value},'../src/db/database-source-binding.js':{createDatabaseSourceBindingVerifier:()=>{throw Error('Preflight must never fall back to unprotected source verification')}},
'./writer-profile/operator-contract.mjs':{approval:(value:unknown)=>{events.push('approval');return value},reconciliationApproval:(value:unknown)=>{events.push('reconciliation-approval');return value},poolOptions:()=>{events.push('transport');return {}},roleKeys:['runtime','operator','capture','publisher','control'],assertJsDriverEnvironment:()=>{}},
'./database-writer-bootstrap-operator.mjs':{deploymentRequest:(value:unknown)=>value},
'./writer-profile/protected-source-registry.mjs':{createProtectedDatabaseSourceBindingVerifier:(pin:{registryDirectory:string;currentHeadSha256:string})=>{events.push('protected-source');assert.equal(pin.registryDirectory,'source-registry');assert.equal(pin.currentHeadSha256,request.sourceHeadSha256);if(protectedRefused)throw Error('Protected source refused');return protectedVerifier}},
pg:{Pool},'./writer-profile/publisher.mjs':{createConnectedPublisher:(_installation:unknown,_collect:unknown,verifier:unknown)=>{assert.equal(verifier,protectedVerifier);return {preflightCurrentProfile:async()=>{events.push('preflight');if(asyncError){const activePool=retainedPools.at(-1);assert(activePool);activePool.emit('error',Error('Async pool fault'))}return result},publishCurrentProfile:()=>{throw Error('No publish')},reconcilePendingPublication:()=>{throw Error('No reconcile')}}}},
'./writer-profile/catalogue.mjs':{collectSource44Catalogue:()=>{},approvedDefinition:()=> 'b'.repeat(64)},
'./writer-profile/source-activation.mjs':{withActiveSourcePreflight:async(input:{directory:string;expectedSha256:string;sourcePin:{currentHeadSha256:string};expectedSource:unknown;tlsSpkiSha256:string;deadlineAt:number},verifier:unknown,work:(verifier:unknown)=>Promise<unknown>)=>{
assert.equal(verifier,protectedVerifier);assert.equal(input.directory,'active-registry');assert.equal(input.expectedSha256,'e'.repeat(64))
assert.equal(input.sourcePin.currentHeadSha256,request.sourceHeadSha256);assert.equal(input.tlsSpkiSha256,request.tls.spkiSha256);assert(input.deadlineAt>Date.now())
assert.equal(format.canonical(input.expectedSource),format.canonical({...request.source,databaseOid:Number(request.source.databaseOid),sessionUser:'publisher',currentUser:'publisher'}))
events.push('activation-admission');if(activationRefused)throw Error('Activation refused')
const value=await work(guardedVerifier);events.push('activation-terminal');return value
}},
pg:{Pool},'./writer-profile/publisher.mjs':{requireCurrentCatalogueDefinition:()=>{if(incompatibleDefinition)throw Error('Build manifest refused')},createConnectedPublisher:(_installation:unknown,_collect:unknown,verifier:unknown)=>{
assert.equal(verifier,events.includes('activation-admission')?guardedVerifier:protectedVerifier)
return {
preflightCurrentProfile:async()=>{events.push('preflight');if(asyncError){const activePool=retainedPools.at(-1);assert(activePool);activePool.emit('error',Error('Async pool fault'))}return result},
publishCurrentProfile:async()=>{events.push('publish');return {...result,outcome:'committed'}},
reconcilePendingPublication:async()=>{events.push('reconcile');return {state:'confirmed',headSha256:result.headSha256}},
}
}},
'./writer-profile/catalogue.mjs':{collectSource44Catalogue:()=>{},approvedDefinition:()=>({definitionSha256:'b'.repeat(64),manifestSha256:frozenDefinition.manifestSha256})},
}
const flags=['bootstrap','bootstrap-sha256','approval','approval-sha256','definition','definition-sha256','transport','transport-sha256','ca','source-registry','source-spki-sha256','journal'],args=['preflight',...flags.flatMap(flag=>['--'+flag,flag.endsWith('sha256')?'a'.repeat(64):flag])]
const output=await compile<OperatorModule>(cli,modules).main(args)
@@ -103,5 +187,37 @@ test('existing operator main dispatches preflight only after protected input val
asyncError=false;result.platformProtectionVerified=false;await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Publisher terminal unconfirmed/)
result.platformProtectionVerified=true;protectedRefused=true;const before=retainedPools.length
await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Protected source refused/);assert.equal(retainedPools.length,before,'Protection failure must not open a Pool or fall back')
for(const [command,approvalEvent,outcome] of [['publish','approval','committed'],['reconcile','reconciliation-approval','confirmed']]){
const commandArgs=[command,...args.slice(1)]
const poolCount=retainedPools.length
await assert.rejects(compile<OperatorModule>(cli,modules).main(commandArgs),/Protected source refused/)
assert.equal(retainedPools.length,poolCount)
protectedRefused=false
events.length=0;reads.length=0
const terminal=await compile<OperatorModule>(cli,modules).main(commandArgs)
assert.equal(terminal.outcome,outcome);assert.equal(terminal.readyAuthorized,false)
assert.deepEqual(events,[approvalEvent,'transport','protected-source',command,'pool-end'])
assert.equal(reads.includes('signing-private.pem'),command==='publish')
protectedRefused=true
}
const activeArgs=['source-preflight',...args.slice(1),'--activation-directory','active-registry','--activation-sha256','e'.repeat(64)]
protectedRefused=false;activationRefused=true;events.length=0;reads.length=0
const beforeActivation=retainedPools.length
await assert.rejects(compile<OperatorModule>(cli,modules).main(activeArgs),/Activation refused/)
assert.equal(retainedPools.length,beforeActivation)
assert.deepEqual(events,['approval','transport','protected-source','activation-admission'])
activationRefused=false;events.length=0;reads.length=0
const active=await compile<OperatorModule>(cli,modules).main(activeArgs)
assert.equal(active.event,'application-writer-profile-source-preflight');assert.equal(active.sourceActivationMatched,true)
assert.equal(active.readyAuthorized,false);assert.equal(active.writerAuthorized,false)
assert.deepEqual(events,['approval','transport','protected-source','activation-admission','preflight','pool-end','activation-terminal'])
assert.deepEqual(reads,['bootstrap','approval','definition','transport','ca'])
incompatibleDefinition=true;events.length=0;reads.length=0
const beforeBuildRefusal=retainedPools.length
for(const incompatibleArgs of [args,activeArgs,['publish',...args.slice(1)]])await assert.rejects(compile<OperatorModule>(cli,modules).main(incompatibleArgs),/Build manifest refused/)
assert.equal(retainedPools.length,beforeBuildRefusal)
assert(!events.some(event=>event==='protected-source'||event==='activation-admission'))
const historical=await compile<OperatorModule>(cli,modules).main(['reconcile',...args.slice(1)])
assert.equal(historical.outcome,'confirmed');assert.equal(historical.readyAuthorized,false)
})
+151 -5
View File
@@ -6,6 +6,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'
const endpoint = new URL(process.env.OWN_MCP_ENDPOINT), origin = endpoint.origin
assert.equal(endpoint.hostname, '127.0.0.1'); assert.equal(process.env.OWN_EXTERNAL_CLIENT, 'true')
let phase = 'start', next = 0, maxResponseBytes = 0, stderrBytes = 0, bridgePid = null
const responseLimitBytes = 1024 * 1024
const pending = new Map(), cases = []
const activeClients = new Set()
let closing = false
@@ -22,7 +23,7 @@ async function boundedFetch(input, init) {
if (response.body) {
const reader = response.body.getReader()
try {
for (;;) { const item = await reader.read(); if (item.done) break; count += item.value.length; if (count >= 1048576) { await reader.cancel(); throw Error('Own external client response bound') }; chunks.push(item.value) }
for (;;) { const item = await reader.read(); if (item.done) break; count += item.value.length; if (count >= responseLimitBytes) { await reader.cancel(); throw Error('Own external client response bound') }; chunks.push(item.value) }
} finally { reader.releaseLock() }
}
const bytes = Buffer.concat(chunks); maxResponseBytes = Math.max(maxResponseBytes, bytes.length)
@@ -36,14 +37,14 @@ function content(result) {
return parsed
}
async function denied(work) { try { const r = await work(); assert.equal(r?.isError, true) } catch (e) { if (e?.code === 'ERR_ASSERTION') throw e; assert(!/timeout|timed out/i.test(String(e?.message)), 'A timeout is not authorization denial') } }
async function opened(mode, token) {
async function opened(mode, token, maxBufferSize = 131072) {
const client = new Client({ name: 'actual-external-process-' + mode, version: '1.0.0' })
activeClients.add(client)
let transport
if (mode === 'http') transport = new StreamableHTTPClientTransport(endpoint, { requestInit: { headers: { authorization: 'Bearer ' + token } }, fetch: boundedFetch })
else {
transport = new StdioClientTransport({ command: process.execPath, args: ['--import', import.meta.resolve('tsx'), process.env.OWN_BRIDGE_FILE],
cwd: process.cwd(), env: { MCP_URL: endpoint.href, MCP_TOKEN: token }, stderr: 'pipe', maxBufferSize: 131072 })
cwd: process.cwd(), env: { MCP_URL: endpoint.href, MCP_TOKEN: token }, stderr: 'pipe', maxBufferSize })
transport.stderr?.on('data', chunk => { stderrBytes += chunk.length; const text = String(chunk); assert(!text.includes(token)); assert(stderrBytes < 1048576) })
const start = transport.start.bind(transport)
transport.start = async () => { await start(); bridgePid = transport.pid; assert(bridgePid && bridgePid !== process.pid && bridgePid !== Number(process.env.OWN_SERVER_PID)); process.send({ kind: 'bridge-pid', pid: bridgePid }) }
@@ -68,6 +69,36 @@ try {
phase = mode + ':initialize'; let client = await opened(mode, token)
try {
await scoped(client); cases.push(mode + ': separate SDK process initializes, filters tool/host scope, bounds page/result and redacts credentials')
phase = mode + ':policy-edit'
const editScope = async operationIds => {
const response = await boundedFetch(origin + '/api/mcp/connections/' + initial.connectionId, {
method: 'PUT', headers: { authorization: 'Bearer ' + process.env.OWN_PORTAL_JWT, 'content-type': 'application/json' },
body: JSON.stringify({ name: 'Owned external SDK process', mode: 'observer', expiresAt: new Date(Date.now() + 600000).toISOString(),
permissions: ['mm:servers:read'], scope: { serverIds: [Number(process.env.OWN_ALLOWED_SERVER)], operationIds, allowGlobal: false }, limits: { callsPerMinute: 200 } }),
})
assert.equal(response.status, 200)
}
await editScope(['logs.query'])
assert.deepEqual((await client.listTools({}, { timeout: 5000 })).tools.map(tool => tool.name), ['mm_logs_query'])
const allowedServer = Number(process.env.OWN_ALLOWED_SERVER), forbiddenServer = Number(process.env.OWN_FORBIDDEN_SERVER)
const queryLogs = arguments_ => client.callTool({ name: 'mm_logs_query', arguments: arguments_ }, undefined, { timeout: 5000 })
const filters = { q: 'external-sdk-log-', limit: 1 }
const first = content(await queryLogs(filters))
assert.ok(first.nextCursor)
const second = content(await queryLogs({ ...filters, cursor: first.nextCursor }))
assert.equal(second.nextCursor, null)
assert.deepEqual([first.entries, second.entries].map(entries => entries.map(({ serverId, message }) => ({ serverId, message }))), [
[{ serverId: allowedServer, message: 'external-sdk-log-1' }],
[{ serverId: allowedServer, message: 'external-sdk-log-2' }],
])
await denied(() => queryLogs({ ...filters, serverId: forbiddenServer }))
cases.push(mode + ': populated log search pages return only allowed server records and reject the forbidden server')
await denied(() => client.callTool({ name: 'mm_servers_list', arguments: {} }, { timeout: 5000 }))
await editScope([])
assert.deepEqual((await client.listTools({}, { timeout: 5000 })).tools, [])
await editScope(['servers.list'])
await scoped(client)
cases.push(mode + ': established client observes removed and restored tool scope through the web policy API')
if (mode === 'stdio' && process.env.OWN_EXTERNAL_CLEANUP_FAULT === 'failure') throw Error('Owned deliberate external client failure with bridge active')
if (mode === 'stdio' && process.env.OWN_EXTERNAL_CLEANUP_FAULT === 'timeout') { process.send({ kind: 'arm-cleanup-timeout' }); await new Promise(() => {}) }
phase = mode + ':result-bound'; await control('large-result')
@@ -86,7 +117,7 @@ try {
} finally { await client.close().catch(() => {}) }
}
phase = 'http:body-bound'; const bound = await control('new-connection')
const oversized = await fetch(endpoint, { method: 'POST', headers: { authorization: 'Bearer ' + bound.token, 'content-type': 'application/json', accept: 'application/json,text/event-stream' },
const oversized = await fetch(endpoint, { method: 'POST', headers: { authorization: 'Bearer ' + bound.token, 'content-type': 'application/json', accept: 'application/json,text/event-stream', connection: 'close' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'mm_servers_list', arguments: { padding: 'x'.repeat(1048576) } } }), signal: AbortSignal.timeout(5000) })
assert.equal(oversized.status, 413); assert((await oversized.text()).length < 65536)
cases.push('actual HTTP request envelope over1MiB refused before tool execution; successful client results remain bounded')
@@ -126,8 +157,123 @@ try {
await denied(() => oauthClient.callTool({ name: 'mm_servers_list', arguments: {} }, undefined, { timeout: 5000 }))
cases.push('actual OAuth family revocation refuses established and fresh external transport; no implicit new consent granted')
} finally { await oauthClient.close().catch(() => {}) }
phase = 'discovery:issue'
const catalogue = await control('discovery-connection')
const expectedNames = catalogue.names.toSorted()
assert(expectedNames.includes('mm_logs_follow'))
const discoveryByTransport = {}
for (const mode of ['http', 'stdio']) {
phase = `discovery:${mode}`
const client = await opened(mode, catalogue.token, responseLimitBytes)
try {
const names = [], cursors = new Set()
let cursor, largestPageBytes = 0, pages = 0
do {
const page = await client.listTools(cursor ? { cursor } : {}, { timeout: 10000 })
assert(page.tools.length > 0)
for (const tool of page.tools) {
assert.equal(tool.inputSchema.type, 'object')
names.push(tool.name)
}
const encoded = JSON.stringify(page)
assert(!encoded.includes(catalogue.token) && !encoded.includes(process.env.OWN_SERVER_SECRET))
largestPageBytes = Math.max(largestPageBytes, Buffer.byteLength(encoded))
assert(largestPageBytes < responseLimitBytes)
pages++
cursor = page.nextCursor
if (cursor) { assert(!cursors.has(cursor)); cursors.add(cursor) }
assert(names.length <= expectedNames.length)
} while (cursor)
assert.deepEqual(names.toSorted(), expectedNames)
discoveryByTransport[mode] = { tools: names.length, pages, largestPageBytes }
cases.push(mode + ': external SDK discovers every delegated tool exactly once with an object input schema')
} finally { await client.close().catch(() => {}) }
}
phase = 'discovery:revoke'
await control('revoke')
for (const [mode, arm] of [['http', 'arm-log-revocation'], ['stdio', 'arm-log-revocation-after-commit'], ['http', 'arm-log-follow-revocation']]) {
phase = mode + ':' + arm
const { token } = await control(arm)
const logClient = await opened(mode, token)
try {
const follow = arm === 'arm-log-follow-revocation'
const tool = follow ? 'mm_logs_follow' : 'mm_logs_query'
const filters = { q: follow ? 'external-sdk-live-event' : 'external-sdk-log-1', limit: 1 }
if (follow) {
Object.assign(filters, { serverId: Number(process.env.OWN_ALLOWED_SERVER), waitSeconds: 0 })
const start = content(await logClient.callTool({ name: tool, arguments: filters }, undefined, { timeout: 5000 }))
assert.deepEqual(start.entries, [])
assert.equal(typeof start.cursor, 'string')
filters.cursor = start.cursor
filters.waitSeconds = 10
}
const query = logClient.callTool({ name: tool, arguments: filters }, undefined, { timeout: 10000 })
const checked = query.then(result => {
assert.equal(result.isError, true)
const wire = JSON.stringify(result)
assert.match(wire, /OWNER_CLAIMS_REVOKED|UNAUTHORIZED/)
assert.doesNotMatch(wire, /external-sdk-log-1|external-sdk-live-event|"entries"|"nextCursor"|"cursor"/)
})
const deliver = async () => {
if (follow) await control('publish-follow-fixture-row')
await control('deliver-log-revocation')
}
await Promise.all([checked, deliver()])
await assert.rejects(opened(mode, token))
cases.push(mode + ': signed portal revocation ' + (mode === 'stdio' ? 'after' : 'before') + ' audit commit denies ' + tool + ' result and reconnect')
} finally { await logClient.close().catch(() => {}) }
}
phase = 'portal:revoke-export-download'
const exportAccess = await control('new-artifact-connection')
const exportClient = await opened('http', exportAccess.token)
try {
const exported = content(await exportClient.callTool({ name: 'mm_logs_export', arguments: {
serverId: Number(process.env.OWN_ALLOWED_SERVER), q: 'external-sdk-log-1', format: 'json',
} }, undefined, { timeout: 5000 }))
assert.equal(exported.rows, 1)
assert.equal(exported.downloadPath, '/api/mcp/artifacts/' + exported.artifactId)
assert.doesNotMatch(JSON.stringify(exported), /external-sdk-log-1/)
const download = jwt => boundedFetch(origin + exported.downloadPath, { headers: { authorization: 'Bearer ' + jwt } })
const before = await download(exportAccess.ownerJwt)
assert.equal(before.status, 200)
assert.equal(before.headers.get('cache-control'), 'private, no-store')
const bytes = Buffer.from(await before.arrayBuffer())
assert.equal(createHash('sha256').update(bytes).digest('hex'), exported.sha256)
assert.match(bytes.toString('utf8'), /external-sdk-log-1/)
const fresh = await control('revoke-artifact-owner')
for (const jwt of [exportAccess.ownerJwt, fresh.ownerJwt]) {
const refused = await download(jwt)
assert.ok([401, 403].includes(refused.status))
assert.equal(refused.headers.has('content-disposition'), false)
assert.doesNotMatch(await refused.text(), /external-sdk-log-1/)
}
const management = await boundedFetch(origin + '/api/mcp/connections', { headers: { authorization: 'Bearer ' + fresh.ownerJwt } })
assert.equal(management.status, 200)
cases.push('portal owner revocation blocks an existing unexpired log artifact even with refreshed owner login')
} finally { await exportClient.close().catch(() => {}) }
phase = 'portal:revoke-approved-plan'
const planAccess = await control('new-revocable-plan-connection')
const planClient = await opened('http', planAccess.token)
try {
const plan = content(await planClient.callTool({ name: 'mm_logs_settings_update_plan', arguments: {
settings: { enabled: false },
} }, undefined, { timeout: 5000 }))
assert.equal(plan.state, 'awaiting-approval')
const approved = await boundedFetch(origin + '/api/mcp/plans/' + plan.planId + '/decision', {
method: 'POST', headers: { authorization: 'Bearer ' + planAccess.ownerJwt, 'content-type': 'application/json' },
body: JSON.stringify({ approve: true }),
})
assert.equal(approved.status, 200)
assert.deepEqual(await approved.json(), { ok: true })
await control('revoke-approved-plan-owner')
await denied(() => planClient.callTool({ name: 'mm_change_apply', arguments: {
planId: plan.planId, idempotencyKey: randomUUID(),
} }, undefined, { timeout: 5000 }))
cases.push('signed portal revocation invalidates the exact approved log settings plan before apply')
} finally { await planClient.close().catch(() => {}) }
process.send({ kind: 'result', result: { cases: cases.length, names: cases, externalClientPid: process.pid, serverPid: Number(process.env.OWN_SERVER_PID), bridgePid,
sdkOnlyExternalClient: true, maxObservedHttpResponseBytes: maxResponseBytes, clientResponseLimitBytes: 1048576, stdioSdkBufferLimitBytes: 131072,
sdkOnlyExternalClient: true, maxObservedHttpResponseBytes: maxResponseBytes, clientResponseLimitBytes: responseLimitBytes, stdioSdkBufferLimitBytes: 131072,
discoveryByTransport, discoveryStdioSdkBufferLimitBytes: responseLimitBytes,
actualPublicStringLimitObserved: 16000, wholeGenericResultEnvelopeLimitAccepted: false,
syntheticPortalJwtAndConsent: true, liveAuthPortal: false, livePortalSender: false, endUserUi: false, wholeExternalDirectionComplete: false } })
} catch (error) { process.send?.({ kind: 'failure', phase, errorType: error?.name ?? 'Error', completedCases: cases }); process.exitCode = 1 }
@@ -12,7 +12,9 @@ const output = resolve(sourceRoot, scenario === 'bridge-vlan' ? 'tmp/mcp-bridge-
const paths = ['backend/src/scripts/mcp-stdio.ts', 'backend/src/routes/mcp-transport.ts', 'backend/src/routes/mcp-oauth.ts', 'backend/src/modules/mcp/store.ts',
'backend/src/modules/mcp/runtime.ts', 'backend/src/modules/mcp/actions.ts', 'backend/src/modules/mcp/security.ts', 'backend/src/modules/mcp/protocol-limits.ts',
'backend/src/modules/mcp/portal-revocation.ts', 'backend/src/plugins/auth.ts', 'backend/src/index.ts', 'node_modules/@modelcontextprotocol/client/package.json', 'node_modules/@modelcontextprotocol/server/package.json']
if (scenario === 'bridge-vlan') paths.push('backend/scripts/mcp-external-process/bridge-vlan-client.mjs', 'backend/scripts/mcp-external-process/bridge-vlan-fixture.ts',
if (scenario === 'acceptance') paths.push('backend/scripts/mcp-external-process/client.mjs', 'backend/scripts/mcp-external-process/server-fixture.ts',
'backend/src/routes/mcp-portal.ts', 'backend/src/modules/mcp/portal-revocation-event.ts', 'backend/src/services/router-logs-query.ts')
else paths.push('backend/scripts/mcp-external-process/bridge-vlan-client.mjs', 'backend/scripts/mcp-external-process/bridge-vlan-fixture.ts',
'backend/src/modules/mcp/bridge-vlan-actions.ts', 'backend/src/services/bridge-vlan-configuration.ts', 'packages/contracts/src/bridge-vlan.ts')
async function pins() { const result = []; for (const path of paths) result.push({ path, sha256: createHash('sha256').update(await readFile(resolve(sourceRoot, path))).digest('hex') }); return result }
const before = await pins(), directory = await mkdtemp(join(resolve(sourceRoot, 'tmp'), 'mm-mcp-external-')), state = join(directory, 'state'); await mkdir(state)
@@ -1,5 +1,5 @@
import assert from "node:assert/strict"
import { createHash } from "node:crypto"
import { createHash, createHmac, randomUUID } from "node:crypto"
import { spawn } from "node:child_process"
import { createServer } from "node:net"
import { fileURLToPath, pathToFileURL } from "node:url"
@@ -20,6 +20,7 @@ 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 { listActions } = await import(source("modules/mcp/actions.js")) as typeof import("../../src/modules/mcp/actions.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"],
@@ -28,7 +29,27 @@ let currentId: string | undefined, app: Awaited<ReturnType<typeof buildApp>> | u
const credentials: string[] = []
const bridgePids = new Set<number>()
let childExit: Promise<number> | undefined
let releaseLogRead: (() => void) | undefined
let logReadCompleted: Promise<void> | undefined
let revocationOwnerId: string | undefined
const revokedLogRequests: { connectionId: string; tool: string }[] = []
const originalConnect = pool.connect
const delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
async function deliverPortalRevocation(ownerId: string) {
const secret = randomUUID() + randomUUID()
process.env.MCP_PORTAL_WEBHOOK_SECRET = secret
const occurredAt = Date.now(), timestamp = String(Math.floor(occurredAt / 1000))
const eventId = randomUUID(), body = JSON.stringify({ version: 2, eventId, userId: ownerId, type: "permissions.changed", eventOccurredAt: occurredAt })
const signature = "sha256=" + createHmac("sha256", secret).update(timestamp + "." + body).digest("hex")
const response = await fetch(origin + "/_internal/mcp/portal/revocations", {
method: "POST", headers: { "content-type": "application/json", "x-mm-timestamp": timestamp, "x-mm-signature": signature },
body, signal: AbortSignal.timeout(5000),
})
assert.equal(response.status, 200)
assert.deepEqual(await response.json(), { ok: true, duplicate: false })
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_portal_events WHERE id=$1", [eventId])).rows[0].count, 1)
return occurredAt
}
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") })]) }
@@ -47,9 +68,18 @@ try {
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 pool.query(`INSERT INTO router_log_entries(server_id,server_name,host,router_id,raw_time,received_at,effective_at,topics,level,message,generation)
SELECT id,name,host,'fixture','00:00:01',now(),now()-n*interval '1 second',ARRAY['system'],'info','external-sdk-log-'||n,0
FROM servers CROSS JOIN generate_series(1,2) AS n WHERE id=ANY($1::int[])`, [[allowed, forbidden]])
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 artifactOwner = { ...actor, id: actor.id + "-artifact" }
const signArtifactOwner = () => new SignJWT({ sub: artifactOwner.id, apps: artifactOwner.apps, permissions: artifactOwner.permissions })
.setProtectedHeader({ alg: "HS256" }).setIssuer(env.authIssuer).setIssuedAt().setExpirationTime("1h").sign(new TextEncoder().encode(env.jwtSecret))
let artifactConnectionId: string | undefined
const planOwner = { ...actor, id: actor.id + "-approved-plan", permissions: [...actor.permissions, "mm:mcp:approve"] }
let planConnectionId: string | undefined
const portalHeaders = { authorization: `Bearer ${jwt}`, "content-type": "application/json" }
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_connections WHERE owner_id=$1", [actor.id])).rows[0].count, 0)
const callback = "http://localhost:45678/callback?registered=1"
@@ -123,10 +153,131 @@ try {
if (message.kind !== "control" || !Number.isSafeInteger(message.id)) return
try {
let value: unknown
if (message.command === "new-connection") {
if (message.command === "new-revocable-plan-connection") {
assert(!planConnectionId)
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({
name: "Owned approved plan revocation", mode: "approval", permissions: ["mm:settings:admin"],
expiresAt: new Date(Date.now() + 600000).toISOString(),
scope: { serverIds: [Number(allowed), Number(forbidden)], operationIds: ["logs.settings.update", "changes.apply"], allowGlobal: true },
}), planOwner)
currentId = planConnectionId = issued.connection.id
const ownerJwt = await new SignJWT({ sub: planOwner.id, apps: planOwner.apps, permissions: planOwner.permissions })
.setProtectedHeader({ alg: "HS256" }).setIssuer(env.authIssuer).setIssuedAt().setExpirationTime("1h").sign(new TextEncoder().encode(env.jwtSecret))
credentials.push(issued.token, ownerJwt)
value = { token: issued.token, ownerJwt }
} else if (message.command === "revoke-approved-plan-owner") {
assert(planConnectionId && currentId === planConnectionId)
const plans = (await pool.query("SELECT id,state,approved_by FROM mcp_plans WHERE connection_id=$1", [planConnectionId])).rows
assert.equal(plans.length, 1)
assert.equal(plans[0].state, "approved")
assert.equal(plans[0].approved_by, planOwner.id)
await deliverPortalRevocation(planOwner.id)
value = { ok: true }
} else if (message.command === "new-artifact-connection") {
assert(!artifactConnectionId)
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({
name: "Owned export revocation", mode: "observer", permissions: ["mm:servers:read"],
expiresAt: new Date(Date.now() + 600000).toISOString(),
scope: { serverIds: [Number(allowed)], operationIds: ["logs.export"], allowGlobal: false },
}), artifactOwner)
currentId = artifactConnectionId = issued.connection.id
const ownerJwt = await signArtifactOwner()
credentials.push(issued.token, ownerJwt)
value = { token: issued.token, ownerJwt }
} else if (message.command === "revoke-artifact-owner") {
assert(artifactConnectionId && currentId === artifactConnectionId)
const occurredAt = await deliverPortalRevocation(artifactOwner.id)
while (Math.floor(Date.now() / 1000) <= Math.floor(occurredAt / 1000)) await delay(25)
const ownerJwt = await signArtifactOwner()
credentials.push(ownerJwt)
value = { ownerJwt }
} else if (["arm-log-revocation", "arm-log-revocation-after-commit", "arm-log-follow-revocation"].includes(message.command ?? "")) {
assert(!releaseLogRead)
const afterCommit = message.command === "arm-log-revocation-after-commit"
const follow = message.command === "arm-log-follow-revocation"
const owner = { ...actor, id: `${actor.id}-${follow ? "follow" : afterCommit ? "postcommit" : "precommit"}` }
revocationOwnerId = owner.id
if (follow) {
await pool.query("UPDATE servers SET enabled=true WHERE id=$1", [allowed])
await pool.query("UPDATE router_log_settings SET enabled=true WHERE id=1")
}
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({
name: "Owned portal revocation during log read", mode: "observer", permissions: ["mm:servers:read"],
expiresAt: new Date(Date.now() + 600000).toISOString(),
scope: { serverIds: [Number(allowed)], operationIds: [follow ? "logs.follow" : "logs.query"], allowGlobal: false },
}), owner)
currentId = issued.connection.id
revokedLogRequests.push({ connectionId: currentId, tool: follow ? "mm_logs_follow" : "mm_logs_query" })
credentials.push(issued.token)
let completed!: () => void
logReadCompleted = new Promise<void>(resolve => { completed = resolve })
const hold = new Promise<void>(resolve => { releaseLogRead = resolve })
pool.connect = ((callback?: unknown) => {
if (typeof callback === "function") return Reflect.apply(originalConnect, pool, [callback])
return (async () => {
const client = await originalConnect.call(pool)
const query = client.query, release = client.release
let completingLog = false
client.query = (async (...args: unknown[]) => {
const params = args[1]
if (typeof args[0] === "string" && args[0].startsWith("UPDATE mcp_requests SET status=$2,result=")
&& Array.isArray(params) && params[1] === "completed" && String(params[2]).includes(follow ? "external-sdk-live-event" : "external-sdk-log-1")) {
completingLog = true
if (!afterCommit) { completed(); await hold }
}
const result = await Reflect.apply(query, client, args)
if (afterCommit && completingLog && args[0] === "COMMIT") { completed(); await hold }
return result
}) as typeof query
client.release = error => {
client.query = query
client.release = release
release(error)
}
return client
})()
}) as typeof originalConnect
value = { token: issued.token }
} else if (message.command === "publish-follow-fixture-row") {
assert(releaseLogRead && currentId && revokedLogRequests.at(-1)?.tool === "mm_logs_follow")
const deadline = Date.now() + 3000
for (;;) {
const live = await pool.query("SELECT count(*)::int AS count FROM mcp_read_leases WHERE connection_id=$1", [currentId])
if (live.rows[0].count === 1) break
assert(Date.now() < deadline, "Owned follow did not acquire a live read lease")
await delay(25)
}
await pool.query(`INSERT INTO router_log_entries(server_id,server_name,host,router_id,raw_time,received_at,effective_at,topics,level,message,generation)
SELECT id,name,host,'fixture','00:00:01',now(),now(),ARRAY['system'],'info','external-sdk-live-event',0
FROM servers WHERE id=$1`, [allowed])
value = { ok: true }
} else if (message.command === "deliver-log-revocation") {
assert(logReadCompleted && releaseLogRead && currentId && revocationOwnerId)
try {
await Promise.race([logReadCompleted, delay(5000).then(() => { throw Error("Owned log read did not reach the barrier") })])
await deliverPortalRevocation(revocationOwnerId)
value = { ok: true }
} finally {
pool.connect = originalConnect
releaseLogRead()
releaseLogRead = undefined
logReadCompleted = undefined
revocationOwnerId = undefined
}
} else 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 === "discovery-connection") {
const operations = listActions().map(action => action.operation)
const permissions = [...new Set(operations.flatMap(operation => operation.requiredPermissions))]
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({
name: "Owned full catalogue discovery", mode: "approval", expiresAt: new Date(Date.now() + 600000).toISOString(),
permissions, scope: { serverIds: [Number(allowed)], operationIds: operations.map(operation => operation.id), allowGlobal: true },
}), { ...actor, permissions: [...new Set([...actor.permissions, ...permissions])] })
currentId = issued.connection.id
credentials.push(issued.token)
value = { token: issued.token, names: operations.map(operation => operation.toolName) }
} 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 }
@@ -144,6 +295,22 @@ try {
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)
assert.equal(revokedLogRequests.length, 3)
assert.ok(planConnectionId)
const invalidated = (await pool.query("SELECT state FROM mcp_plans WHERE connection_id=$1", [planConnectionId])).rows
assert.deepEqual(invalidated, [{ state: "invalidated" }])
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_operations WHERE connection_id=$1", [planConnectionId])).rows[0].count, 0)
assert.equal((await pool.query("SELECT enabled FROM router_log_settings WHERE id=1")).rows[0].enabled, true)
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_artifacts WHERE connection_id=$1 AND expires_at>now()", [artifactConnectionId])).rows[0].count, 1)
for (const { connectionId, tool } of revokedLogRequests) {
const revokedRead = (await pool.query("SELECT status,error_code,result FROM mcp_requests WHERE connection_id=$1 AND tool=$2 AND status='error'", [connectionId, tool])).rows
assert.equal(revokedRead.length, 1)
assert.equal(revokedRead[0].status, "error")
assert.deepEqual(Object.keys(revokedRead[0].result), ["message"])
assert.doesNotMatch(JSON.stringify(revokedRead[0].result), /external-sdk-log-1|external-sdk-live-event|"entries"|"nextCursor"|"cursor"/)
assert.ok(["OWNER_CLAIMS_REVOKED", "UNAUTHORIZED"].includes(revokedRead[0].error_code))
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_read_leases WHERE connection_id=$1", [connectionId])).rows[0].count, 0)
}
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, denialWithoutConnection: true,
@@ -151,5 +318,7 @@ try {
await writeFile(process.env.OWN_EXTERNAL_RESULT_FILE!, JSON.stringify(final, null, 2))
console.log("Owned external process SDK result:", JSON.stringify(final))
} finally {
pool.connect = originalConnect
releaseLogRead?.()
try { await drainChildAndBridges() } finally { try { if (app) await app.close() } finally { await closePool() } }
}
@@ -0,0 +1,105 @@
import assert from 'node:assert/strict'
import {execFileSync} from 'node:child_process'
import {randomBytes, X509Certificate} from 'node:crypto'
import {mkdtemp, mkdir, cp, readFile, rm, chmod, access} from 'node:fs/promises'
import {createServer} from 'node:net'
import {tmpdir} from 'node:os'
import {dirname, join, resolve} from 'node:path'
import EmbeddedPostgres from 'embedded-postgres'
import pg from 'pg'
import {poolOptions, retainPublisherTlsPeer} from './writer-profile/operator-contract.mjs'
import {sha} from './writer-profile/operator-files.mjs'
const openssl = process.env.MMAPP_TLS_FIXTURE_OPENSSL || 'openssl'
const host = 'localhost'
const reservePort = async () => {
const listener = createServer()
await new Promise((ready, reject) => listener.once('error', reject).listen(0, '127.0.0.1', ready))
return listener
}
const releasePort = listener => new Promise((ready, reject) => listener.close(error => error ? reject(error) : ready()))
const sourcePort = await reservePort()
const clonePort = await reservePort()
const port = sourcePort.address().port, clonePortNumber = clonePort.address().port
await Promise.all([releasePort(sourcePort), releasePort(clonePort)])
const transport = {host, port, user: 'mcp_test', password: randomBytes(24).toString('hex'), database: 'postgres'}
const certFile = 'server.pem', keyFile = 'server-key.pem'
const root = await mkdtemp(join(tmpdir(), 'mm-writer-tls-owned-'))
const databaseDir = join(root, 'pg'), cloneDir = join(root, 'pg-clone')
const sourceTlsDir = join(root, 'source-tls'), cloneTlsDir = join(root, 'clone-tls')
const cluster = (databaseDir, port, tlsDir) => new EmbeddedPostgres({databaseDir, port, user: transport.user, password: transport.password,
persistent: true, createPostgresUser: false, postgresFlags: ['-h', '127.0.0.1', '-c', 'ssl=on', '-c', `ssl_cert_file=${join(tlsDir, certFile)}`, '-c', `ssl_key_file=${join(tlsDir, keyFile)}`],
onLog: () => {}, onError: () => {}})
const postgres = cluster(databaseDir, port, sourceTlsDir), clone = cluster(cloneDir, clonePortNumber, cloneTlsDir)
let sourceStarted = false, cloneStarted = false
const fingerprint = ca => sha(new X509Certificate(ca).publicKey.export({format:'der', type:'spki'}))
const issue = async tlsDir => {
await mkdir(tlsDir, {mode:0o700})
execFileSync(openssl, ['req', '-new', '-newkey', 'rsa:2048', '-x509', '-nodes', '-days', '1', '-subj', '/CN=localhost',
'-addext', 'subjectAltName=DNS:localhost', '-keyout', keyFile, '-out', certFile], {cwd: tlsDir, stdio: 'ignore', windowsHide: true})
await chmod(join(tlsDir, keyFile), 0o600)
return readFile(join(tlsDir, certFile))
}
const withClient = async (config, work) => {
const pool = new pg.Pool({connectionTimeoutMillis:5000, ...config})
let client
try { client = await pool.connect(); return await work(client) }
finally { client?.release(true); await pool.end() }
}
const query = "SELECT (pg_control_system()).system_identifier::text AS system, oid::text AS oid FROM pg_database WHERE datname=current_database()"
try {
await postgres.initialise()
const first = await issue(sourceTlsDir)
await postgres.start(); sourceStarted = true
const pin = {serverName:host, caSha256:sha(first), spkiSha256:fingerprint(first)}
const request = {tls:pin, roles:{publisher:{name:transport.user}}, source:{databaseName:transport.database}}
const baseline = await withClient(poolOptions(transport, request, first), async client => {
const retained = retainPublisherTlsPeer(client, pin)
retained()
const {rows:[source]} = await client.query(query)
const {rows:[status]} = await client.query('SELECT ssl FROM pg_stat_ssl WHERE pid=pg_backend_pid()')
assert.equal(status.ssl, true)
await client.query("CREATE TABLE owned_tls_fixture (note text PRIMARY KEY); INSERT INTO owned_tls_fixture VALUES ('original')")
const connection = client.connection, socket = connection.stream
connection.stream = undefined
try { assert.throws(retained, /retained TLS peer unavailable/) } finally { connection.stream = socket }
return source
})
await withClient({...transport, ssl:false}, async client => assert.throws(() => retainPublisherTlsPeer(client,pin), /retained TLS peer unavailable/))
await postgres.stop(); sourceStarted = false
await cp(databaseDir, cloneDir, {recursive:true, force:false, errorOnExist:true})
await assert.rejects(access(join(cloneDir, keyFile)), {code:'ENOENT'})
const second = await issue(cloneTlsDir)
assert.notEqual(fingerprint(second), pin.spkiSha256)
await postgres.start(); sourceStarted = true
await clone.start(); cloneStarted = true
const cloneTransport = {...transport, port:clonePortNumber}
const bothTrusted = Buffer.concat([first,second])
await assert.rejects(withClient(poolOptions(cloneTransport, {...request,tls:{...pin,caSha256:sha(bothTrusted)}}, bothTrusted),
() => assert.fail('Wrong physical cluster must not reach application SQL')), /TLS SPKI refused/)
await withClient({...cloneTransport, ssl:{ca:bothTrusted, servername:host, rejectUnauthorized:true}}, async client => {
assert.equal(client.connection.stream.authorized, true)
const {rows:[source]} = await client.query(query)
assert.deepEqual(source, baseline)
assert.throws(() => retainPublisherTlsPeer(client,pin), /TLS SPKI refused/)
retainPublisherTlsPeer(client,{...pin,spkiSha256:fingerprint(second)})()
assert.deepEqual((await client.query('SELECT note FROM owned_tls_fixture')).rows, [{note:'original'}])
await client.query("INSERT INTO owned_tls_fixture VALUES ('clone-only')")
})
await withClient(poolOptions(transport, request, first), async client => {
retainPublisherTlsPeer(client,pin)()
assert.deepEqual((await client.query('SELECT note FROM owned_tls_fixture')).rows, [{note:'original'}])
})
console.log('Genuine PostgreSQL TLS: original accepted; independently writable physical clone with identical DB coordinates and a different CA-valid TLS key refused')
} finally {
const stops = await Promise.allSettled([
cloneStarted ? clone.stop() : undefined,
sourceStarted ? postgres.stop() : undefined,
])
assert(stops.every(result => result.status === 'fulfilled'), 'Owned cluster stop unconfirmed')
assert.equal(dirname(resolve(root)), resolve(tmpdir()))
assert.match(root, /mm-writer-tls-owned-/)
await rm(root, {recursive:true, force:true, maxRetries:20, retryDelay:100})
await assert.rejects(access(root), {code:'ENOENT'})
console.log('Owned PostgreSQL and temporary TLS material removed')
}
+6 -2
View File
@@ -165,6 +165,10 @@ const chrVpn = process.argv.includes('--chr-vpn-catalog')
const chrFilters = process.argv.includes('--chr-filter-write')
const chrLegacyOspf = process.argv.includes('--chr-ospf-legacy')
const greRevision = process.argv.includes('--gre-revision')
const usersBindings = process.argv.includes('--users-bindings')
if (usersBindings && (process.argv.length !== 3 || process.argv[2] !== '--users-bindings')) throw new Error('User bindings acceptance requires its own disposable cluster')
const bridgeVlanConfiguration = process.argv.includes('--bridge-vlan-configuration')
if (bridgeVlanConfiguration && process.argv.slice(2).some(arg => arg !== '--bridge-vlan-configuration')) throw new Error('Bridge VLAN acceptance requires its own disposable cluster')
const applicationAdapters = process.argv.includes('--application-adapters')
const scopedRoutingDiagnostics = process.argv.includes('--scoped-routing-diagnostics')
const interfaceEventsCompatibility = process.argv.includes('--interface-events-compatibility')
@@ -246,7 +250,7 @@ try {
if (regressionOnly) console.log('Selected regression only: primary integration and terminal restore are not run')
if (profilesOnly) console.log('Profiles-only regression matrix: primary integration NOT RUN; terminal restore NOT RUN; full acceptance is not claimed')
const result = regressionOnly || profilesOnly ? 0 : await new Promise((resolve, reject) => {
const child = spawn(process.execPath, pollMemory ? ['--max-old-space-size=192', '--max-semi-space-size=16', fileURLToPath(new URL('../dist-mcp-memory/services/server-poll-memory.owned.test.js', import.meta.url))] : ['--import', 'tsx', principalSecuritySource ? 'src/modules/mcp/principal-security-source.owned.test.ts' : pollJournalSnapshot ? 'src/services/server-poll-journal-snapshot.owned.test.ts' : pollTerminalArchive ? 'src/services/server-poll-terminal-archive.owned.test.ts' : pollArchiveSchema ? 'src/db/server-poll-terminal-archives.sql.test.ts' : telemetrySettingsConfiguration ? 'src/modules/mcp/telemetry-settings-configuration.owned.test.ts' : routingReservationSchema ? 'src/db/routing-reservation-schema.owned.test.ts' : routingForwardRounds ? 'src/modules/mcp/routing-network-forward-rounds.owned.test.ts' : flowActiveServices ? 'src/services/traffic-flow-active-services.owned.test.ts' : flowActiveRebuild ? 'src/services/traffic-flow-active-rebuild.owned.test.ts' : flowQuality ? 'src/services/traffic-flow-quality.owned.test.ts' : flowWorkerIpc ? 'src/services/traffic-flow-worker-ipc.owned.test.ts' : manualUptime ? 'src/modules/mcp/manual-uptime-http.test.ts' : manualPingGroup ? 'src/modules/mcp/manual-ping-group-http.test.ts' : manualTraffic ? 'src/modules/mcp/manual-traffic-http.test.ts' : manualApiPing ? 'src/modules/mcp/manual-api-ping-http.test.ts' : simpleSpeedChr ? 'src/modules/mcp/simple-speed-chr.test.ts' : simpleSpeed ? 'src/modules/mcp/simple-speed-http.test.ts' : externalPeerMetadata ? 'src/services/bandwidth-external-peer-metadata.sql.test.ts' : operatorPeerMetadata ? 'src/services/bandwidth-operator-metadata.sql.test.ts' : publicRegistryMetadata ? 'src/services/bandwidth-public-registry.sql.test.ts' : operatorDockerSql ? 'src/services/bandwidth-operator-docker-stop.sql.test.ts' : probeOperationRetirement ? 'src/services/server-probe-operation-retirement.owned.test.ts' : ordinaryOperationRetirement ? 'src/modules/mcp/ordinary-operation-retirement.owned.test.ts' : pollOperationRetirement ? 'src/services/server-poll-operation-retirement.owned.test.ts' : mcpOperationOrigin ? 'src/modules/mcp/operation-execution-origin.sql.test.ts' : mcpSpeedOriginal ? 'src/modules/mcp/speed-bandwidth-original.sql.test.ts' : routerConfigurationGuard ? 'src/services/router-configuration-guard.sql.test.ts' : scheduledSpeedOriginal ? 'src/services/bandwidth-scheduled-speed-original.sql.test.ts' : bandwidthSpeedWorkflow ? 'src/services/bandwidth-speed-workflow.sql.test.ts' : restSpeedOriginal ? 'src/services/bandwidth-rest-speed-original.sql.test.ts' : operationLocks ? 'src/services/operation-config-lock-lease.sql.test.ts' : serviceExecutionOrigin ? 'src/modules/mcp/service-execution-origin.sql.test.ts' : bandwidthNodeSource ? 'src/services/bandwidth-node-source.sql.test.ts' : bandwidthExternalRegistry ? 'src/services/bandwidth-external-node-registry.sql.test.ts' : bandwidthNodeRegistry ? 'src/services/bandwidth-node-registry.sql.test.ts' : bandwidthControlSource ? 'src/services/bandwidth-control-source.sql.test.ts' : bandwidthAdmission ? 'src/services/bandwidth-admission.sql.test.ts' : serverPollStorageFresh ? 'src/services/server-poll-storage-fresh.sql.test.ts' : serverPollStorage ? 'src/services/server-poll-storage.sql.test.ts' : probeStorageFresh ? 'src/services/server-probe-storage-fresh.sql.test.ts' : probeLegacyOrphans ? 'src/services/server-probe-legacy-orphans.sql.test.ts' : diagnosticCancelHost ? 'src/modules/mcp/diagnostic-cancellation-storage.sql.test.ts' : ipsecExportHttp ? 'src/modules/mcp/ipsec-existing-certificate-export-http.sql.test.ts' : ipsecExportJournal ? 'src/services/ipsec-existing-certificate-export-rest-journal.sql.test.ts' : migrationDataInventory ? 'src/db/migration-data-inventory.sql.test.ts' : probeStorageHistory ? 'src/services/server-probe-storage-history.sql.test.ts' : probeStorageJournal ? 'src/services/server-probe-storage-journal.sql.test.ts' : controlledSchemaReference ? 'src/db/controlled-schema-reference.sql.test.ts' : controlledMigrations ? 'src/db/controlled-migrations.sql.test.ts' : databaseSchemaControl ? 'src/db/database-schema-control.sql.test.ts' : migrationSchemaInventory ? 'src/db/migration-schema-inventory.sql.test.ts' : routingNetworkNative ? 'src/modules/mcp/routing-network-native.sql.test.ts' : routingReservationRuntime ? 'src/modules/mcp/routing-reservation-runtime.sql.test.ts' : routingReservations ? 'src/modules/mcp/routing-reservation.sql.test.ts' : collectorPartitionWriters ? 'src/services/collector-partition-writes.sql.test.ts' : serverPollDurable ? 'src/services/server-poll-durable-session.sql.test.ts' : routingRecoveryAuthority ? 'src/modules/mcp/routing-recovery-authority.sql.test.ts' : legacyFlowRebuild ? 'src/test/legacy-flow-rebuild-sql.ts' : probeJournal ? 'src/services/server-probe-journal.sql.test.ts' : flowHostKeys ? 'src/modules/mcp/flow-host-keys-http.test.ts' : flowRebuildMcp ? 'src/modules/mcp/flow-facts-rebuild-http.test.ts' : flowRebuildLine truncated
const child = spawn(process.execPath, pollMemory ? ['--max-old-space-size=192', '--max-semi-space-size=16', fileURLToPath(new URL('../dist-mcp-memory/services/server-poll-memory.owned.test.js', import.meta.url))] : ['--import', 'tsx', usersBindings ? 'src/modules/mcp/users-bindings-owned.test.ts' : bridgeVlanConfiguration ? 'src/modules/mcp/bridge-vlan-configuration-http.test.ts' : principalSecuritySource ? 'src/modules/mcp/principal-security-source.owned.test.ts' : pollJournalSnapshot ? 'src/services/server-poll-journal-snapshot.owned.test.ts' : pollTerminalArchive ? 'src/services/server-poll-terminal-archive.owned.test.ts' : pollArchiveSchema ? 'src/db/server-poll-terminal-archives.sql.test.ts' : telemetrySettingsConfiguration ? 'src/modules/mcp/telemetry-settings-configuration.owned.test.ts' : routingReservationSchema ? 'src/db/routing-reservation-schema.owned.test.ts' : routingForwardRounds ? 'src/modules/mcp/routing-network-forward-rounds.owned.test.ts' : flowActiveServices ? 'src/services/traffic-flow-active-services.owned.test.ts' : flowActiveRebuild ? 'src/services/traffic-flow-active-rebuild.owned.test.ts' : flowQuality ? 'src/services/traffic-flow-quality.owned.test.ts' : flowWorkerIpc ? 'src/services/traffic-flow-worker-ipc.owned.test.ts' : manualUptime ? 'src/modules/mcp/manual-uptime-http.test.ts' : manualPingGroup ? 'src/modules/mcp/manual-ping-group-http.test.ts' : manualTraffic ? 'src/modules/mcp/manual-traffic-http.test.ts' : manualApiPing ? 'src/modules/mcp/manual-api-ping-http.test.ts' : simpleSpeedChr ? 'src/modules/mcp/simple-speed-chr.test.ts' : simpleSpeed ? 'src/modules/mcp/simple-speed-http.test.ts' : externalPeerMetadata ? 'src/services/bandwidth-external-peer-metadata.sql.test.ts' : operatorPeerMetadata ? 'src/services/bandwidth-operator-metadata.sql.test.ts' : publicRegistryMetadata ? 'src/services/bandwidth-public-registry.sql.test.ts' : operatorDockerSql ? 'src/services/bandwidth-operator-docker-stop.sql.test.ts' : probeOperationRetirement ? 'src/services/server-probe-operation-retirement.owned.test.ts' : ordinaryOperationRetirement ? 'src/modules/mcp/ordinary-operation-retirement.owned.test.ts' : pollOperationRetirement ? 'src/services/server-poll-operation-retirement.owned.test.ts' : mcpOperationOrigin ? 'src/modules/mcp/operation-execution-origin.sql.test.ts' : mcpSpeedOriginal ? 'src/modules/mcp/speed-bandwidth-original.sql.test.ts' : routerConfigurationGuard ? 'src/services/router-configuration-guard.sql.test.ts' : scheduledSpeedOriginal ? 'src/services/bandwidth-scheduled-speed-original.sql.test.ts' : bandwidthSpeedWorkflow ? 'src/services/bandwidth-speed-workflow.sql.test.ts' : restSpeedOriginal ? 'src/services/bandwidth-rest-speed-original.sql.test.ts' : operationLocks ? 'src/services/operation-config-lock-lease.sql.test.ts' : serviceExecutionOrigin ? 'src/modules/mcp/service-execution-origin.sql.test.ts' : bandwidthNodeSource ? 'src/services/bandwidth-node-source.sql.test.ts' : bandwidthExternalRegistry ? 'src/services/bandwidth-external-node-registry.sql.test.ts' : bandwidthNodeRegistry ? 'src/services/bandwidth-node-registry.sql.test.ts' : bandwidthControlSource ? 'src/services/bandwidth-control-source.sql.test.ts' : bandwidthAdmission ? 'src/services/bandwidth-admission.sql.test.ts' : serverPollStorageFresh ? 'src/services/server-poll-storage-fresh.sql.test.ts' : serverPollStorage ? 'src/services/server-poll-storage.sql.test.ts' : probeStorageFresh ? 'src/services/server-probe-storage-fresh.sql.test.ts' : probeLegacyOrphans ? 'src/services/server-probe-legacy-orphans.sql.test.ts' : diagnosticCancelHost ? 'src/modules/mcp/diagnostic-cancellation-storage.sql.test.ts' : ipsecExportHttp ? 'src/modules/mcp/ipsec-existing-certificate-export-http.sql.test.ts' : ipsecExportJournal ? 'src/services/ipsec-existing-certificate-export-rest-journal.sql.test.ts' : migrationDataInventory ? 'src/db/migration-data-inventory.sql.test.ts' : probeStorageHistory ? 'src/services/server-probe-storage-history.sql.test.ts' : probeStorageJournal ? 'src/services/server-probe-storage-journal.sql.test.ts' : controlledSchemaReference ? 'src/db/controlled-schema-reference.sql.test.ts' : controlledMigrations ? 'src/db/controlled-migrations.sql.test.ts' : databaseSchemaControl ? 'src/db/database-schema-control.sql.test.ts' : migrationSchemaInventory ? 'src/db/migration-schema-inventory.sql.test.ts' : routingNetworkNative ? 'src/modules/mcp/routing-network-native.sql.test.ts' : routingReservationRuntime ? 'src/modules/mcp/routing-reservation-runtime.sql.test.ts' : routingReservations ? 'src/modules/mcp/routing-reservation.sql.test.ts' : collectorPartitionWriters ? 'src/services/collector-partition-writes.sql.test.ts' : serverPollDurable ? 'src/services/server-poll-durable-session.sql.test.ts' : routingRecoveryAuthority ? 'src/modules/mcp/routing-recovery-authority.sql.test.ts' : legacyFlowRebuild ? 'src/test/legacy-flow-rebuild-sql.ts' : probeJournal ? 'src/services/server-probe-journal.sql.testLine truncated
cwd: pollMemory ? directory : new URL('../', import.meta.url), stdio: 'inherit', env: { ...(pollMemory ? pollMemoryEnvironment : process.env),
...(routingOriginalEffects ? { MMAPP_OWN_ROUTING_ORIGINAL_EFFECTS: 'true' } : {}),
DATABASE_URL: `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres`,
@@ -257,7 +261,7 @@ try {
child.once('error', reject); child.once('exit', code => resolve(code ?? 1))
})
exitCode = result
if (result === 0 && !regressionOnly && !profilesOnly && !pollMemory && !principalSecuritySource && !pollJournalSnapshot && !pollArchiveSchema && !telemetrySettingsConfiguration && !routingReservationSchema && !routingForwardRounds && !flowActiveServices && !flowActiveRebuild && !flowQuality && !flowWorkerIpc && !manualUptime && !manualPingGroup && !manualTraffic && !manualApiPing && !simpleSpeed && !simpleSpeedChr && !externalPeerMetadata && !operatorPeerMetadata && !publicRegistryMetadata && !operatorDockerSql && !mcpOperationOrigin && !mcpSpeedOriginal && !routerConfigurationGuard && !scheduledSpeedOriginal && !bandwidthSpeedWorkflow && !restSpeedOriginal && !operationLocks && !serviceExecutionOrigin && !bandwidthNodeSource && !bandwidthNodeRegistry && !bandwidthExternalRegistry && !bandwidthControlSource && !bandwidthAdmission && !probeLegacyOrphans && !probeStorageJournal && !probeStorageHistory && !probeJournal && !chr && !flowWrites && !flowRebuild && !flowRebuildMcp && (process.argv.includes('--restore') || process.env.MMAPP_PG_CLIENT_BIN)) {
if (result === 0 && !regressionOnly && !profilesOnly && !usersBindings && !bridgeVlanConfiguration && !pollMemory && !principalSecuritySource && !pollJournalSnapshot && !pollArchiveSchema && !telemetrySettingsConfiguration && !routingReservationSchema && !routingForwardRounds && !flowActiveServices && !flowActiveRebuild && !flowQuality && !flowWorkerIpc && !manualUptime && !manualPingGroup && !manualTraffic && !manualApiPing && !simpleSpeed && !simpleSpeedChr && !externalPeerMetadata && !operatorPeerMetadata && !publicRegistryMetadata && !operatorDockerSql && !mcpOperationOrigin && !mcpSpeedOriginal && !routerConfigurationGuard && !scheduledSpeedOriginal && !bandwidthSpeedWorkflow && !restSpeedOriginal && !operationLocks && !serviceExecutionOrigin && !bandwidthNodeSource && !bandwidthNodeRegistry && !bandwidthExternalRegistry && !bandwidthControlSource && !bandwidthAdmission && !probeLegacyOrphans && !probeStorageJournal && !probeStorageHistory && !probeJournal && !chr && !flowWrites && !flowRebuild && !flowRebuildMcp && (process.argv.includes('--restore') || process.env.MMAPP_PG_CLIENT_BIN)) {
exitCode = await new Promise((resolve, reject) => {
const child = spawn(process.execPath, ['--import', 'tsx', 'src/modules/mcp/database-restore.test.ts'], {
cwd: new URL('../', import.meta.url), stdio: 'inherit', env: { ...process.env,
@@ -6,10 +6,12 @@ import { randomBytes, createHash } from 'node:crypto'
import { createServer } from 'node:net'
import { spawn } from 'node:child_process'
const allowed = new Set(['--default-scoped', '--main-only', '--check-only'])
if (process.argv.slice(2).some(arg => !allowed.has(arg)) || new Set(process.argv.slice(2)).size !== process.argv.slice(2).length || process.argv.includes('--default-scoped') && process.argv.includes('--main-only')) throw new Error('Usage: node backend/scripts/test-routing-controller-baseline.mjs [--default-scoped | --main-only] [--check-only]')
const modes = ['--default-scoped', '--main-only', '--scoped-bare-readback']
const allowed = new Set([...modes, '--check-only']), args = process.argv.slice(2)
const selected = modes.filter(flag => args.includes(flag))
if (args.some(arg => !allowed.has(arg)) || new Set(args).size !== args.length || selected.length > 1) throw new Error('Usage: node backend/scripts/test-routing-controller-baseline.mjs [--default-scoped | --main-only | --scoped-bare-readback] [--check-only]')
const root = resolve(import.meta.dirname, '../..'), require = createRequire(import.meta.url), ts = require('typescript')
const mode = process.argv.includes('--default-scoped') ? 'default-scoped' : process.argv.includes('--main-only') ? 'main-only' : 'main-and-forward-rounds'
const mode = selected[0]?.slice(2) ?? 'main-and-forward-rounds'
const target = join(root, 'backend/src/modules/mcp/routing-controller-active-writer.ts')
await readFile(target)
await readFile(join(root, 'backend/src/modules/mcp/routing-main-transition.ts'))
+2 -2
View File
@@ -7,7 +7,7 @@ export function approvedDefinition(value:unknown){
assert(value&&typeof value==='object');const v=value as Record<string,unknown>
assert.deepEqual(Object.keys(v).sort(),['format','provenance','template']);assert.equal(v.format,'application-writer-source44-definition-approval-v1')
assert.equal(canonical(v.provenance),canonical(BUILD44_PROVENANCE));assert.equal(canonical(v.template),canonical(FIXED44_ROLE_TEMPLATE))
return BUILD44_PROVENANCE.templateSha256
return Object.freeze({definitionSha256:BUILD44_PROVENANCE.templateSha256,manifestSha256:BUILD44_PROVENANCE.manifestSha256})
}
/** Real incremental DATA verifier on caller's retained transaction. No pool or attestor. */
export async function collectSource44Catalogue(s:{query:(sql:string,values?:unknown[])=>Promise<{rows:Record<string,unknown>[]} >},installation:{roleBindings:ConstructorParameters<typeof Fixed44CatalogueValueBridgeData>[0]}){
@@ -21,7 +21,7 @@ export async function collectSource44Catalogue(s:{query:(sql:string,values?:unkn
await s.query('DECLARE mm_publisher44 NO SCROLL CURSOR WITHOUT HOLD FOR '+FIXED44_CATALOGUE_BUILD_SQL,values);cursor=true
for(;;){const page=(await s.query('FETCH FORWARD 128 FROM mm_publisher44')).rows;bridge.acceptPage(page);if(page.length===0)break}
const result=bridge.finish();await s.query('CLOSE mm_publisher44');cursor=false
return {catalogSha256:sha(canonical({values:result.values.snapshotSha256,catalogue:result.catalogue})),definitionSha256:result.catalogue.definitionSha256,leafCount:result.catalogue.leafRelations}
return {catalogSha256:sha(canonical({values:result.values.snapshotSha256,catalogue:result.catalogue})),definitionSha256:result.catalogue.definitionSha256,manifestSha256:result.catalogue.manifestSha256,leafCount:result.catalogue.leafRelations}
}catch(error){primary=error;failed=true;throw error}
finally{
try{if(cursor)await s.query('CLOSE mm_publisher44');await s.query("SELECT pg_catalog.set_config('search_path',$1,true),pg_catalog.set_config('TimeZone',$2,true),pg_catalog.set_config('DateStyle',$3,true)",[settings[0].path,settings[0].zone,settings[0].dates])}
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict'
import {X509Certificate} from 'node:crypto'
import {checkServerIdentity} from 'node:tls'
import {checkServerIdentity,TLSSocket} from 'node:tls'
import {sha} from './operator-files.mjs'
export const roleKeys=Object.freeze(['runtime','operator','capture','publisher','control'])
export const STARTUP_OPTIONS='-c statement_timeout=10000 -c lock_timeout=5000 -c client_encoding=UTF8'
@@ -18,15 +18,29 @@ export function approval(value){
}
// Pinned historical inspection data only; never publication authorization.
export function reconciliationApproval(value){return parsedApproval(value)}
function peerError(pin,host,cert){
const failed=checkServerIdentity(pin.serverName,cert);if(failed)return failed
try{if(host!==pin.serverName||sha(new X509Certificate(cert.raw).publicKey.export({format:'der',type:'spki'}))!==pin.spkiSha256)return Error('Publisher TLS SPKI refused')}catch{return Error('Publisher TLS certificate refused')}
}
// pg replaces connection.stream with the negotiated TLS socket. Hold its
// identity on the retained client; handshake callbacks are not session evidence.
export function retainPublisherTlsPeer(client,pin){
const socket=client.connection?.stream
const assertCurrent=()=>{
assert(socket instanceof TLSSocket&&client.connection?.stream===socket&&socket.encrypted&&socket.authorized&&!socket.destroyed&&socket.writable,'Publisher retained TLS peer unavailable')
}
assertCurrent()
const failed=peerError(pin,socket.servername,socket.getPeerCertificate());if(failed)throw failed
socket.disableRenegotiation()
return assertCurrent
}
export function poolOptions(transport,request,ca){
assert(exact(transport,['host','port','user','password','database']))
assert(typeof transport.host==='string'&&transport.host===request.tls.serverName&&Number.isSafeInteger(transport.port)&&transport.port>0&&transport.port<=65535&&transport.user===request.roles.publisher.name&&typeof transport.password==='string'&&transport.password.length>0&&transport.password.length<=4096&&transport.database===request.source.databaseName)
assert(sha(ca)===request.tls.caSha256);new X509Certificate(ca)
return {host:transport.host,port:transport.port,user:transport.user,password:transport.password,database:transport.database,max:1,connectionTimeoutMillis:5000,idleTimeoutMillis:5000,statement_timeout:10000,lock_timeout:5000,options:STARTUP_OPTIONS,application_name:'mmapp-writer-publisher',client_encoding:'UTF8',
ssl:{ca,servername:request.tls.serverName,rejectUnauthorized:true,checkServerIdentity(host,cert){
const failed=checkServerIdentity(request.tls.serverName,cert);if(failed)return failed
try{if(host!==request.tls.serverName||sha(new X509Certificate(cert.raw).publicKey.export({format:'der',type:'spki'}))!==request.tls.spkiSha256)return Error('Publisher TLS SPKI refused')}catch{return Error('Publisher TLS certificate refused')}
}}}
ssl:{ca,servername:request.tls.serverName,rejectUnauthorized:true,checkServerIdentity:(host,cert)=>peerError(request.tls,host,cert)}}
}
export function assertPublicationApproval(current,catalogue,structure,pinned,manifestSha256){
assert(pinned.expiresAt>Date.now()&&pinned.manifestSha256===manifestSha256&&pinned.definitionSha256===catalogue.definitionSha256)
+39 -17
View File
@@ -3,6 +3,7 @@ import pg from 'pg'
import {createPrivateKey,createPublicKey,sign} from 'node:crypto'
import {open,lstat,realpath,unlink} from 'node:fs/promises'
import {writeFile,withJournalLease} from './operator-files.mjs'
import {retainPublisherTlsPeer} from './operator-contract.mjs'
import path from 'node:path'
import {captureDatabaseSourceObservation,assertDatabaseSourceObservationCurrent} from '../../src/db/database-source-observation.js'
import {MIGRATION_MANIFEST,migrationManifestDigest} from '../../src/db/migration-manifest.js'
@@ -10,7 +11,13 @@ import {MIGRATION_MANIFEST,migrationManifestDigest} from '../../src/db/migration
import {ownedContext,sameOwned,sourceCoordinates,protectedStructure,readCurrent,HISTORY,CURRENT} from './context.mjs'
import {bytes,canonical,sha,nextEpoch,POLICY_DOMAIN,HEAD_DOMAIN,KEY,liveValidity,policySchema,headSchema,publicKey} from '../../src/db/application-writer-profile-format.js'
export function createConnectedPublisher(installation,collectFixedCatalogue,sourceVerifier,approval){
export function requireCurrentCatalogueDefinition(approval,definition){
const manifestSha256=migrationManifestDigest()
if(approval.manifestSha256!==manifestSha256||definition.manifestSha256!==manifestSha256||definition.definitionSha256!==approval.definitionSha256)throw Error('Approved catalogue definition is not bound to this build migration manifest')
return manifestSha256
}
export function createConnectedPublisher(installation,collectFixedCatalogue,sourceVerifier,approval,definition){
const PUBLISHER=installation.roleBindings.publisher.name
class ProfilePublicationError extends Error{
automaticReplay=false
@@ -29,13 +36,18 @@ async function privateKey(filename){
// detached application callbacks or cleanup/COMMIT races.
async function session(pool,deadlineAt,work){
if(!(pool instanceof pg.Pool)||!Number.isSafeInteger(deadlineAt)||deadlineAt<=Date.now()||deadlineAt-Date.now()>120000)throw Error('Bounded genuine publisher pool/lifetime required')
let client,retired=false,released=false,locked=false,begun=false,commitSent=false,committed=false,confirmedValue,proposal,phase='checkout',stopReject,cleanupUnconfirmed=false
let client,assertTransport,retired=false,released=false,locked=false,begun=false,commitSent=false,committed=false,confirmedValue,proposal,phase='checkout',stopReject,cleanupUnconfirmed=false
const stopped=new Promise((_,reject)=>{stopReject=reject});void stopped.catch(()=>{})
const release=bad=>{if(!client||released)return;released=true;client.off('error',fault);client.off('end',fault);try{client.release(bad)}catch{cleanupUnconfirmed=true}}
const fault=()=>{if(retired)return;retired=true;cleanupUnconfirmed=true;release(true);stopReject(Error('Publisher connection/lifetime ended'))}
const timer=setTimeout(fault,Math.max(1,deadlineAt-Date.now())),bounded=p=>Promise.race([p,stopped])
const query=async(text,values)=>{if(retired||Date.now()>=deadlineAt)throw Error('Publisher lifetime closed');return bounded(client.query(text,values))}
const fence=()=>{
if(retired||Date.now()>=deadlineAt)throw Error('Publisher lifetime closed')
try{assertTransport()}catch(error){fault();throw error}
}
const query=async(text,values)=>{fence();return bounded(client.query(text,values))}
const state={query,phase:value=>{phase=value},proposal:value=>{proposal=Object.freeze(value)},begun:value=>{begun=value},commit:async value=>{
fence()
confirmedValue=Object.freeze({...value})
commitSent=true
const response=await bounded(client.query('COMMIT').then(result=>{if(result.command==='COMMIT'){committed=true;begun=false}return result}))
@@ -45,6 +57,7 @@ async function session(pool,deadlineAt,work){
try{
const pending=pool.connect().then(value=>{client=value;if(retired){release(true);return value}client.on('error',fault);client.on('end',fault);return value});void pending.catch(()=>{})
await bounded(pending);if(retired)throw Error('Late publisher checkout')
try{assertTransport=retainPublisherTlsPeer(client,installation.tls)}catch(error){retired=true;release(true);throw error}
await query("SET statement_timeout='10s'");await query("SET lock_timeout='5s'")
phase='exclusive-admission';await query('SELECT pg_catalog.pg_advisory_lock($1::bigint)',[KEY]);locked=true
value=await work(state)
@@ -62,7 +75,16 @@ async function session(pool,deadlineAt,work){
if(failed||cleanupUnconfirmed||retired)throw new ProfilePublicationError(commitSent||begun?'unknown':'not-committed',phase,proposal,cleanupUnconfirmed||retired,primary)
return value
}
async function publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256,journalDirectory,validityMs=86400000,deadlineAt=Date.now()+120000}){
async function observeCurrentCatalogue(s,manifestSha256){
const security=await protectedStructure(s,installation),catalogue=await collectFixedCatalogue(s)
if(catalogue.manifestSha256!==manifestSha256||catalogue.definitionSha256!==definition.definitionSha256)throw Error('Catalogue evidence differs from the approved migration manifest/definition')
// Inspect one extra row so an unexpected marker cannot hide after the expected prefix.
const expected=MIGRATION_MANIFEST.map(pin=>pin.file.slice(0,-4))
const markers=(await s.query('SELECT id FROM public.schema_migrations ORDER BY id COLLATE "C" LIMIT $1',[expected.length+1])).rows.map(row=>row.id)
if(canonical(markers)!==canonical(expected))throw Error('Exact current migration marker metadata required')
return {security,catalogue,catalogSha256:sha(canonical({base:catalogue.catalogSha256,security:security.sha256}))}
}
async function publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256,journalDirectory,validityMs=86400000,deadlineAt=Date.now()+120000},manifestSha256){
if(!Number.isSafeInteger(validityMs)||validityMs<1||validityMs>86400000||expectedHeadSha256!==null&&!(/^[a-f0-9]{64}$/).test(expectedHeadSha256))throw Error('Explicit previous head and bounded validity required')
const key=await privateKey(privateKeyFile),directory=await realpath(journalDirectory),pendingFile=path.join(directory,'pending-publication.json')
try{await lstat(pendingFile);throw Error('Pending publication must be reconciled before signing')}catch(error){if(error.code!=='ENOENT')throw error}
@@ -73,18 +95,15 @@ async function publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256,jou
const observation=await captureDatabaseSourceObservation(facade,options)
await sourceVerifier.verify(facade,observation,options)
if(canonical(sourceCoordinates(first))!==canonical(installation.expectedSource))throw Error('Pinned source mismatch')
const security=await protectedStructure(s,installation),catalogue=await collectFixedCatalogue(s)
const markers=(await s.query('SELECT id FROM public.schema_migrations ORDER BY id LIMIT 45')).rows.map(row=>row.id)
if(canonical(markers)!==canonical(MIGRATION_MANIFEST.map(pin=>pin.file.slice(0,-4))))throw Error('Exact current migration marker metadata required')
const {security,catalogue,catalogSha256}=await observeCurrentCatalogue(s,manifestSha256)
const old=await readCurrent(s,installation,{allowEmpty:true,validNow:false})
if((old?.headSha256??null)!==expectedHeadSha256)throw Error('Expected protected current head mismatch')
const catalogSha256=sha(canonical({base:catalogue.catalogSha256,security:security.sha256}))
if(approval.expiresAt<=Date.now()||approval.manifestSha256!==migrationManifestDigest()||approval.definitionSha256!==catalogue.definitionSha256||approval.catalogSha256!==catalogSha256)throw Error('Independently approved policy differs from actual source44 catalogue')
if(old&&(canonical(old.policy.source)!==canonical(sourceCoordinates(first))||canonical(old.policy.roles)!==canonical(security.roles)||old.policy.manifestSha256!==migrationManifestDigest()||old.policy.definitionSha256!==catalogue.definitionSha256||old.policy.catalogSha256!==catalogSha256))throw Error('Renewal cannot adopt a changed source/subject/policy')
if(approval.expiresAt<=Date.now()||approval.catalogSha256!==catalogSha256)throw Error('Independently approved policy differs from actual catalogue')
if(old&&(canonical(old.policy.source)!==canonical(sourceCoordinates(first))||canonical(old.policy.roles)!==canonical(security.roles)||old.policy.manifestSha256!==manifestSha256||old.policy.definitionSha256!==catalogue.definitionSha256||old.policy.catalogSha256!==catalogSha256))throw Error('Renewal cannot adopt a changed source/subject/policy')
await sourceVerifier.verify(facade,observation,options);await assertDatabaseSourceObservationCurrent(facade,observation,options);sameOwned(first,await ownedContext(s,installation,PUBLISHER,'publisher-read'))
await s.query('COMMIT');s.begun(false)
const number=old?nextEpoch(old.epoch):'1',now=Date.now()
const body=policySchema.parse({format:'application-writer-policy-v1',installationId:installation.installationId,epoch:number,issuedAt:now,notBefore:now,notAfter:now+validityMs,source:sourceCoordinates(first),sourceObservationSha256:sha(canonical(observation)),roles:security.roles,manifestSha256:migrationManifestDigest(),definitionSha256:catalogue.definitionSha256,catalogSha256,futureLeaves:'fixed12-native-policy-v1'})
const body=policySchema.parse({format:'application-writer-policy-v1',installationId:installation.installationId,epoch:number,issuedAt:now,notBefore:now,notAfter:now+validityMs,source:sourceCoordinates(first),sourceObservationSha256:sha(canonical(observation)),roles:security.roles,manifestSha256,definitionSha256:catalogue.definitionSha256,catalogSha256,futureLeaves:'fixed12-native-policy-v1'})
const envelope=(domain,body)=>bytes({body,signature:sign(null,Buffer.from(domain+canonical(body)),key).toString('base64url')})
const policyBytes=envelope(POLICY_DOMAIN,body),policySha256=sha(policyBytes)
const head=headSchema.parse({format:'application-writer-head-v1',installationId:installation.installationId,epoch:number,policySha256,previousHeadSha256:old?.headSha256??null})
@@ -101,8 +120,8 @@ async function publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256,jou
await s.query(`SELECT id FROM mmapp_control.${CURRENT} WHERE id=1 FOR UPDATE`)
const current=await readCurrent(s,installation,{allowEmpty:true,validNow:false})
if((current?.headSha256??null)!==expectedHeadSha256)throw Error('CAS predecessor changed')
const againSecurity=await protectedStructure(s,installation),againCatalogue=await collectFixedCatalogue(s)
if(sha(canonical({base:againCatalogue.catalogSha256,security:againSecurity.sha256}))!==catalogSha256)throw Error('Policy drift during retained exclusive publication')
const again=await observeCurrentCatalogue(s,manifestSha256)
if(again.catalogSha256!==catalogSha256)throw Error('Policy drift during retained exclusive publication')
if(Date.now()>=body.notAfter||Date.now()>=approval.expiresAt)throw Error('Policy or approval expired before publication dispatch')
await s.query(`INSERT INTO mmapp_control.${HISTORY}(installation_id,epoch,head_sha256,previous_installation_id,previous_epoch,previous_head_sha256,policy_sha256,policy_bytes,head_bytes) VALUES($1::uuid,$2::bigint,$3,$4::uuid,$5::bigint,$6,$7,$8::bytea,$9::bytea)`,[installation.installationId,number,headSha256,old?installation.installationId:null,old?.epoch??null,old?.headSha256??null,policySha256,policyBytes,headBytes])
const changed=old?await s.query(`UPDATE mmapp_control.${CURRENT} SET epoch=$1::bigint,head_sha256=$2 WHERE id=1 AND installation_id=$3::uuid AND epoch=$4::bigint AND head_sha256=$5 RETURNING id`,[number,headSha256,installation.installationId,old.epoch,old.headSha256]):await s.query(`INSERT INTO mmapp_control.${CURRENT}(id,installation_id,epoch,head_sha256) VALUES(1,$1::uuid,$2::bigint,$3) RETURNING id`,[installation.installationId,number,headSha256])
@@ -136,17 +155,17 @@ async function inspectPublication(pool,proposal,{deadlineAt=Date.now()+120000}={
// Read-only diagnostics on the actual protected publisher session. This does
// not admit application/operator writes, bootstrap, restore, or READY.
async function preflightCurrentProfile(pool,{deadlineAt=Date.now()+120000}={}){
const manifestSha256=requireCurrentCatalogueDefinition(approval,definition)
return session(pool,deadlineAt,async s=>{
s.phase('profile-preflight');await s.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');s.begun(true)
const first=await ownedContext(s,installation,PUBLISHER,'publisher-read')
if(canonical(sourceCoordinates(first))!==canonical(installation.expectedSource))throw Error('Pinned preflight source mismatch')
const facade=Object.freeze({query:(sql,values)=>s.query(sql,values)}),options={deadlineAt}
const observation=await captureDatabaseSourceObservation(facade,options),initialSource=await sourceVerifier.verify(facade,observation,options)
const security=await protectedStructure(s,installation),catalogue=await collectFixedCatalogue(s)
const {security,catalogue,catalogSha256}=await observeCurrentCatalogue(s,manifestSha256)
const current=await readCurrent(s,installation)
if(!current)throw Error('Current signed policy required for preflight')
const catalogSha256=sha(canonical({base:catalogue.catalogSha256,security:security.sha256})),manifestSha256=migrationManifestDigest()
if(approval.expiresAt<=Date.now()||approval.manifestSha256!==manifestSha256||approval.definitionSha256!==catalogue.definitionSha256||approval.catalogSha256!==catalogSha256)throw Error('Independent preflight approval differs from actual source44 catalogue')
if(approval.expiresAt<=Date.now()||approval.catalogSha256!==catalogSha256)throw Error('Independent preflight approval differs from actual catalogue')
if(canonical(current.policy.source)!==canonical(sourceCoordinates(first))||canonical(current.policy.roles)!==canonical(security.roles)||current.policy.manifestSha256!==manifestSha256||current.policy.definitionSha256!==catalogue.definitionSha256||current.policy.catalogSha256!==catalogSha256)throw Error('Current signed policy differs from actual source/roles/catalogue')
const terminalSource=await sourceVerifier.verify(facade,observation,options);await assertDatabaseSourceObservationCurrent(facade,observation,options)
sameOwned(first,await ownedContext(s,installation,PUBLISHER,'publisher-read'))
@@ -173,7 +192,10 @@ async function reconcilePendingPublication(pool,{journalDirectory,deadlineAt=Dat
}
return Object.freeze({
preflightCurrentProfile,
publishCurrentProfile:(pool,options)=>withJournalLease(options.journalDirectory,()=>publishCurrentProfile(pool,options)),
publishCurrentProfile:async(pool,options)=>{
const manifestSha256=requireCurrentCatalogueDefinition(approval,definition)
return withJournalLease(options.journalDirectory,()=>publishCurrentProfile(pool,options,manifestSha256))
},
inspectPublication,
reconcilePendingPublication:(pool,options)=>withJournalLease(options.journalDirectory,()=>reconcilePendingPublication(pool,options)),
})
@@ -0,0 +1,150 @@
/** Deployment operator state. Store outside PGDATA, backups and runtime mounts. */
import assert from 'node:assert/strict'
import {constants} from 'node:fs'
import {lstat,open,rename} from 'node:fs/promises'
import path from 'node:path'
import {z} from 'zod'
import {sourceBindingBytes,sourceBindingCanonical,type DatabaseSourceBindingBody} from '../../src/db/database-source-binding.js'
import {protectedDirectory,readProtected,sha,withJournalLease} from './operator-files.mjs'
import {readProtectedDatabaseSourceBindingRegistry, type createProtectedDatabaseSourceBindingVerifier} from './protected-source-registry.mjs'
const digest=z.string().regex(/^[a-f0-9]{64}$/),uuid=z.string().uuid()
const sourcePin=z.object({registryDirectory:z.string().min(1).max(4096),installationId:uuid,publicKeySpkiSha256:digest,currentHeadSha256:digest}).strict()
const directoryPath=z.string().min(1).max(4096)
const approval=z.object({format:z.literal('database-source-activation-approval-v1'),activationDirectory:directoryPath,sourcePin,tlsSpkiSha256:digest,
expectedActivationSha256:digest.nullable()}).strict()
const generation=z.object({generation:uuid,headSha256:digest,bindingSha256:digest,tlsSpkiSha256:digest}).strict()
const stateSchema=z.object({format:z.literal('database-source-activation-v1'),activationDirectory:directoryPath,installationId:uuid,sourceIdentityId:uuid,
publicKeySpkiSha256:digest,status:z.enum(['active','revoked']),generations:z.array(generation).min(1).max(256)}).strict()
const maximum=131072, filename='source-activation.json'
type State=z.infer<typeof stateSchema>
const parse=(bytes:Buffer):unknown=>JSON.parse(new TextDecoder('utf8',{fatal:true}).decode(bytes))
async function present(file:string){
try{await lstat(file);return true}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT')return false;throw error}
}
// Caller holds the same protected lease used by all activation writers.
async function current(directory:string){
const file=path.join(directory,filename)
assert(!await present(file+'.pending'),'Unresolved activation transition')
if(!await present(file))return undefined
const bytes=await readProtected(file,maximum),state=stateSchema.parse(parse(bytes))
assert.equal(state.activationDirectory,directory,'Activation record belongs to another directory')
assert(sourceBindingBytes(state).equals(bytes),'Noncanonical activation record')
for(const field of ['generation','headSha256','tlsSpkiSha256'] as const){
assert.equal(new Set(state.generations.map(item=>item[field].toLowerCase())).size,state.generations.length,'Reused activation identity')
}
return {state,activationSha256:sha(bytes)}
}
async function persist(directory:string,state:State){
await protectedDirectory(directory)
const bytes=sourceBindingBytes(stateSchema.parse(state))
assert(bytes.length<=maximum)
const file=path.join(directory,filename),pending=file+'.pending'
const parent=await open(directory,constants.O_RDONLY|constants.O_DIRECTORY|constants.O_NOFOLLOW)
let confirmed=false,cleanupUnconfirmed=false
try{
const handle=await open(pending,constants.O_WRONLY|constants.O_CREAT|constants.O_EXCL|constants.O_NOFOLLOW,0o600)
try{await handle.writeFile(bytes);await handle.sync()}finally{await handle.close()}
try{await rename(pending,file);await parent.sync()}
catch{throw Object.assign(Error('Activation terminal requires inspection'),{code:'SOURCE_ACTIVATION_TERMINAL_UNKNOWN'})}
confirmed=true
}finally{try{await parent.close()}catch{cleanupUnconfirmed=confirmed}}
return {activationSha256:sha(bytes),cleanupUnconfirmed}
}
function receipt(state:State,activationSha256:string,outcome:'committed'|'observed'){
return {event:'database-source-activation',outcome,activationSha256,status:state.status,
ordinal:state.generations.length,...state.generations.at(-1),generationAuthenticated:false,readyAuthorized:false,automaticReplay:false}
}
type SourceVerifier=ReturnType<typeof createProtectedDatabaseSourceBindingVerifier>
/** Keep revocation and generation changes fenced throughout one read-only SQL preflight. */
export async function withActiveSourcePreflight<T>(input:{directory:string,expectedSha256:string,sourcePin:z.infer<typeof sourcePin>,expectedSource:DatabaseSourceBindingBody['source'],tlsSpkiSha256:string,deadlineAt:number},
verifier:SourceVerifier,work:(guardedVerifier:SourceVerifier)=>Promise<T>){
const {directory,deadlineAt}=input
const expectedSha256=digest.parse(input.expectedSha256),tlsSpkiSha256=digest.parse(input.tlsSpkiSha256)
const pin=sourcePin.parse(input.sourcePin)
return withJournalLease(directory,async()=>{
async function readActive(){
assert(Date.now()<deadlineAt,'Activation preflight expired')
const active=await current(directory)
assert(active?.state.status==='active'&&active.activationSha256===expectedSha256,'Active activation digest changed')
assert(Date.now()<deadlineAt,'Activation preflight expired')
return active.state
}
const state=await readActive(),generation=state.generations.at(-1)!
const registry=await readProtectedDatabaseSourceBindingRegistry(pin,{deadlineAt})
assert(state.publicKeySpkiSha256===registry.publicKeySpkiSha256&&generation.tlsSpkiSha256===tlsSpkiSha256,'Activation key differs from preflight')
function assertGeneration(evidence:{installationId:string,sourceIdentityId:string,generation:string,ordinal:number,headSha256:string,bindingSha256:string}){
assert(evidence.installationId===state.installationId&&evidence.sourceIdentityId===state.sourceIdentityId&&
evidence.generation===generation.generation&&evidence.ordinal===state.generations.length&&
evidence.headSha256===generation.headSha256&&evidence.bindingSha256===generation.bindingSha256,'Activation differs from signed source generation')
}
assertGeneration({...registry.head,headSha256:registry.headSha256})
assert.equal(sourceBindingCanonical(registry.body.source),sourceBindingCanonical(input.expectedSource),'Activation source differs from reviewed configuration')
await readActive()
let sourceChecks=0
const guardedVerifier:SourceVerifier=Object.freeze({async verify(...args:Parameters<SourceVerifier['verify']>){
await readActive()
const evidence=await verifier.verify(...args)
assert(evidence.platformProtectionVerified&&evidence.operatorSignatureVerified&&evidence.currentHeadPinVerified&&evidence.genuineSourceFactsMatched,'Unprotected activation source evidence')
assertGeneration(evidence)
await readActive()
sourceChecks++
return evidence
}})
const result=await work(guardedVerifier)
assert.equal(sourceChecks,2,'Initial and terminal source evidence required')
await readActive()
return result
})
}
export async function sourceActivationMain(args:string[]){
const command=args[0], flags=command==='source-activate'?['directory','approval','approval-sha256']:
command==='source-revoke'?['directory','expected-sha256']:['directory']
assert(['source-activate','source-revoke','source-status'].includes(command)&&args.length===1+2*flags.length)
const values=new Map<string,string>()
for(let i=1;i<args.length;i+=2){
const name=args[i].slice(2)
assert(args[i].startsWith('--')&&flags.includes(name)&&!values.has(name)&&args[i+1]);values.set(name,args[i+1])
}
const directory=values.get('directory')!
return withJournalLease(directory,async()=>{
const before=await current(directory)
if(command==='source-status'){
assert(before,'Activation record absent')
return receipt(before.state,before.activationSha256,'observed')
}
if(command==='source-revoke'){
const expected=digest.parse(values.get('expected-sha256'))
assert(before?.activationSha256===expected&&before.state.status==='active','Active activation digest changed')
const revoked={...before.state,status:'revoked' as const}
const saved=await persist(directory,revoked)
return {...receipt(revoked,saved.activationSha256,'committed'),cleanupUnconfirmed:saved.cleanupUnconfirmed}
}
const request=approval.parse(parse(await readProtected(values.get('approval')!,16384,digest.parse(values.get('approval-sha256')))))
assert.equal(request.activationDirectory,directory,'Approval belongs to another activation directory')
assert.equal(before?.activationSha256??null,request.expectedActivationSha256,'Activation predecessor changed')
const registry=await readProtectedDatabaseSourceBindingRegistry(request.sourcePin,{deadlineAt:Date.now()+120000})
const previous=before?.state.generations??[]
assert.equal(registry.head.ordinal,previous.length+1,'Source ordinal changed')
assert.equal(registry.head.previousHeadSha256,previous.at(-1)?.headSha256??null,'Signed predecessor differs')
if(before){
assert.equal(registry.head.installationId,before.state.installationId)
assert.equal(registry.head.sourceIdentityId,before.state.sourceIdentityId)
assert.equal(registry.publicKeySpkiSha256,before.state.publicKeySpkiSha256)
}
assert(!previous.some(item=>item.generation.toLowerCase()===registry.head.generation.toLowerCase()||item.tlsSpkiSha256===request.tlsSpkiSha256),'Generation or TLS key was already used')
const next:State={format:'database-source-activation-v1',activationDirectory:directory,installationId:registry.head.installationId,
sourceIdentityId:registry.head.sourceIdentityId,publicKeySpkiSha256:registry.publicKeySpkiSha256,status:'active',
generations:[...previous,{generation:registry.head.generation,headSha256:registry.headSha256,
bindingSha256:registry.head.bindingSha256,tlsSpkiSha256:request.tlsSpkiSha256}]}
const saved=await persist(directory,next)
return {...receipt(next,saved.activationSha256,'committed'),cleanupUnconfirmed:saved.cleanupUnconfirmed}
})
}
@@ -0,0 +1,134 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import os from 'node:os'
import {createHash,generateKeyPairSync,randomUUID} from 'node:crypto'
import {createRequire} from 'node:module'
import {runInNewContext} from 'node:vm'
import {provisionDatabaseSourceBindingOffline} from '../database-source-binding-operator.ts'
import {readDatabaseSourceBindingRegistry,sourceBindingBytes,sourceBindingCanonical,type DatabaseSourceDeploymentPin} from '../../src/db/database-source-binding.ts'
import type * as Activation from './source-activation.mts'
const require=createRequire(import.meta.url),ts=require('typescript')
const source=await fs.readFile(new URL('./source-activation.mts',import.meta.url),'utf8')
const sha=(value:Buffer)=>createHash('sha256').update(value).digest('hex')
test('protected activation transition model preserves revocation and refuses concurrent, stale or reused generations',async()=>{
const root=await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(),'mm-source-activation-test-')))
try{
// Genuine issuer signatures and filesystem files. Windows models the
// protection/lease boundary and directory fsync used on the POSIX target.
const lock=path.join(root,'test-lease'),state=path.join(root,'source-activation.json')
const files={sha,protectedDirectory:async(directory:string)=>{assert.equal(directory,root)},
readProtected:async(filename:string,maximum:number,expected?:string)=>{
assert.equal(path.dirname(filename),root)
const bytes=await fs.readFile(filename);assert(bytes.length<=maximum)
if(expected)assert.equal(sha(bytes),expected)
return bytes
},async withJournalLease(directory:string,work:()=>Promise<unknown>){
assert.equal(directory,root);const handle=await fs.open(lock,'wx')
try{return await work()}finally{await handle.close();await fs.unlink(lock)}
}}
let parentFailure=''
const modelFs={...fs,open:async(filename:string,flags:number,mode?:number)=>filename===root?
{sync:async()=>{if(parentFailure==='sync')throw Error('fixture directory sync failure')},
close:async()=>{if(parentFailure==='close')throw Error('fixture directory close failure')}}:fs.open(filename,flags,mode)}
const imports:Record<string,unknown>={'node:fs/promises':modelFs,'./operator-files.mjs':files,
'./protected-source-registry.mjs':{readProtectedDatabaseSourceBindingRegistry:readDatabaseSourceBindingRegistry},
'../../src/db/database-source-binding.js':{sourceBindingBytes,sourceBindingCanonical}}
const load=()=>{
const exports={}
const compiled=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText
runInNewContext(compiled,{exports,require:(name:string)=>imports[name]??require(name),Buffer,TextDecoder})
return exports as typeof Activation
}
const {withActiveSourcePreflight}=load()
let {sourceActivationMain:run}=load()
const privateKeyFile=path.join(root,'issuer.pem'),key=generateKeyPairSync('ed25519').privateKey
await fs.writeFile(privateKeyFile,key.export({format:'pem',type:'pkcs8'}),{mode:0o600})
const installationId=randomUUID(),sourceIdentityId=randomUUID()
const issue=async(previousPin?:DatabaseSourceDeploymentPin,generation=randomUUID())=>{
return (await provisionDatabaseSourceBindingOffline({privateKeyFile,previousPin,outputDirectory:path.join(root,randomUUID()),
request:{installationId,sourceIdentityId,generation,source:{systemIdentifier:'123',databaseOid:16384,databaseName:'synthetic',
sessionUser:'publisher',currentUser:'publisher',inRecovery:false,serverVersionNum:180004}}})).deploymentPin
}
const status=['source-status','--directory',root]
const revoke=(expected:string)=>['source-revoke','--directory',root,'--expected-sha256',expected]
const activate=async(pin:DatabaseSourceDeploymentPin,tlsSpkiSha256:string,expectedActivationSha256:string|null,activationDirectory=root)=>{
const bytes=sourceBindingBytes({format:'database-source-activation-approval-v1',activationDirectory,sourcePin:pin,tlsSpkiSha256,expectedActivationSha256})
const file=path.join(root,randomUUID()+'.json');await fs.writeFile(file,bytes,{mode:0o600,flag:'wx'})
return ['source-activate','--directory',root,'--approval',file,'--approval-sha256',sha(bytes)]
}
const g1=await issue(undefined,'5ac5e8bd-0eb2-498b-901a-dd2010cd4b31'),g2=await issue(g1),g3=await issue(g2)
await assert.rejects(run(await activate(g1,'1'.repeat(64),null,path.join(root,'other'))),/another activation directory/)
const start=await activate(g1,'1'.repeat(64),null)
const races=await Promise.allSettled([run(start),run(start)])
assert.deepEqual(races.map(value=>value.status).sort(),['fulfilled','rejected'],races.map(value=>value.status==='rejected'?String(value.reason):value.status).join(', '))
const first=await run(status)
assert.equal(first.ordinal,1);assert.equal(first.headSha256,g1.currentHeadSha256)
assert.equal(first.generationAuthenticated,false)
const registry=await readDatabaseSourceBindingRegistry(g1)
const evidence={format:'database-source-binding-evidence-v1' as const,
installationId:registry.head.installationId,sourceIdentityId:registry.head.sourceIdentityId,generation:registry.head.generation,
ordinal:registry.head.ordinal,bindingSha256:registry.head.bindingSha256,headSha256:registry.headSha256,
operatorSignatureVerified:true as const,currentHeadPinVerified:true as const,
genuineSourceFactsMatched:true as const,platformProtectionVerified:true as const,generationAuthenticated:false as const,
allWriterDrainAuthenticated:false as const,readyAuthorized:false as const}
// The actual preflight exercises the checked-out PostgreSQL client. The guard fixture models its source verifier.
type Verifier=Parameters<typeof withActiveSourcePreflight>[1]
const verifierArgs=[] as unknown as Parameters<Verifier['verify']>
const preflight={directory:root,sourcePin:g1,expectedSource:registry.body.source,expectedSha256:first.activationSha256,tlsSpkiSha256:'1'.repeat(64),deadlineAt:Date.now()+30000}
const refuseConnect=()=>{throw Error('Reached SQL connection before activation was trusted')}
const verifier={verify:async()=>evidence}
await assert.rejects(withActiveSourcePreflight({...preflight,expectedSha256:'0'.repeat(64)},verifier,refuseConnect),/activation digest changed/)
await assert.rejects(withActiveSourcePreflight({...preflight,tlsSpkiSha256:'2'.repeat(64)},verifier,refuseConnect),/Activation key differs/)
await assert.rejects(withActiveSourcePreflight({...preflight,sourcePin:g2},verifier,refuseConnect),/differs from signed source generation/)
await assert.rejects(withActiveSourcePreflight({...preflight,expectedSource:{...preflight.expectedSource,currentUser:'different'}},verifier,refuseConnect),/differs from reviewed configuration/)
let verifications=0
const result=await withActiveSourcePreflight(preflight,{verify:async()=>{verifications++;return evidence}},async guarded=>{
assert.equal(await guarded.verify(...verifierArgs),evidence)
await assert.rejects(run(revoke(first.activationSha256)),{code:'EEXIST'})
assert.equal(await guarded.verify(...verifierArgs),evidence)
return 'matched'
})
assert.equal(result,'matched');assert.equal(verifications,2)
await assert.rejects(fs.access(lock),{code:'ENOENT'})
await assert.rejects(withActiveSourcePreflight(preflight,verifier,async guarded=>{
await guarded.verify(...verifierArgs)
}),/Initial and terminal source evidence required/)
let reads=0
await assert.rejects(withActiveSourcePreflight(preflight,{verify:async()=>++reads===1?evidence:{...evidence,bindingSha256:'0'.repeat(64)}},async guarded=>{
await guarded.verify(...verifierArgs)
await guarded.verify(...verifierArgs)
}),/differs from signed source generation/)
await fs.writeFile(state+'.pending','interrupted',{flag:'wx'})
await assert.rejects(withActiveSourcePreflight(preflight,verifier,refuseConnect),/Unresolved activation transition/)
await fs.unlink(state+'.pending')
await assert.rejects(run(start),/predecessor changed/)
const revoked=await run(revoke(first.activationSha256))
await assert.rejects(withActiveSourcePreflight({...preflight,expectedSha256:revoked.activationSha256},verifier,refuseConnect),/activation digest changed/)
run=load().sourceActivationMain
assert.equal((await run(status)).status,'revoked')
await assert.rejects(run(revoke(first.activationSha256)),/digest changed/)
await assert.rejects(run(await activate(g3,'3'.repeat(64),revoked.activationSha256)),/ordinal changed/)
const second=await run(await activate(g2,'2'.repeat(64),revoked.activationSha256))
assert.equal(second.ordinal,2);assert.equal(second.status,'active')
await assert.rejects(run(await activate(g3,'1'.repeat(64),second.activationSha256)),/already used/)
const repeatedGeneration=await issue(g2,first.generation!.toUpperCase())
await assert.rejects(run(await activate(repeatedGeneration,'3'.repeat(64),second.activationSha256)),/already used/)
await assert.rejects(run(await activate({...g3,currentHeadSha256:'0'.repeat(64)},'3'.repeat(64),second.activationSha256)))
assert.equal((await run(status)).activationSha256,second.activationSha256)
parentFailure='sync'
await assert.rejects(run(revoke(second.activationSha256)),{code:'SOURCE_ACTIVATION_TERMINAL_UNKNOWN'})
parentFailure='close'
const observed=await run(status)
assert.equal(observed.status,'revoked')
const third=await run(await activate(g3,'3'.repeat(64),observed.activationSha256))
assert.equal(third.outcome,'committed')
assert('cleanupUnconfirmed' in third && third.cleanupUnconfirmed===true)
await fs.writeFile(state+'.pending','interrupted',{flag:'wx'})
await assert.rejects(run(status),/Unresolved activation transition/)
await assert.rejects(run(revoke(third.activationSha256)),/Unresolved activation transition/)
}finally{await fs.rm(root,{recursive:true,force:true})}
})
@@ -53,7 +53,7 @@ try {
assert(baseline.sections.constraints.length > 100)
assert(baseline.sections.functions.length > 0)
assert.deepEqual(compareMigrationSchemas(baseline, await capture(reference.connection)), { equal: true, evidenceOnly: true, enrollmentAuthorized: false, sha256: baseline.sha256 })
cases.push("all36 pinned migrations produce repeatable complete PG18 catalog evidence on an actual checked-out read-only backend")
cases.push(`all ${inventory.length} pinned migrations produce repeatable complete PG18 catalog evidence on an actual checked-out read-only backend`)
} finally { await reference.connection.end() }
const independent = await database("independent")
try {
+2 -2
View File
@@ -26,11 +26,11 @@ export interface ChangeEvidence {
requiresHuman?: boolean;
verificationScope?: "configuration" | "acknowledgement";
validUntil?: string;
observationChecks?: Array<{ id: string; srcServerId: number; dstServerId: number; srcInterface: string; family: string; table: string; fingerprint: string; validUntil: string }>;
observationChecks?: Array<{ id: string; srcServerId: number; dstServerId: number; srcInterface: string; dstInterface?: string; metric?: "pingAt" | "bandwidthAt"; family: string; table: string; fingerprint: string; validUntil: string }>;
ospfOwnerGraph?: { family: "ipv4" | "ipv6"; sourceSecretProofs: Array<{ serverId: number; proof: string }>; instances: Array<{ serverId: number; fingerprint: string }> };
managementChannels?: Array<{ channelId: string; serverId: number; expiresAt: string }>;
recovery?: { step: ChangeStep; expected: unknown; validUntil: string; authorityProfileId: string };
healthChecks?: Array<{ serverId: number; resource: string; id: string; field: "active" | "resolved" | "running" | "state" | "bfd-up"; phase?: "after"; identity?: Record<string, string> }>;
healthChecks?: Array<{ serverId: number; resource: string; id: string; field: "active" | "resolved" | "running" | "state" | "bfd-up"; phase?: "after"; identity?: Record<string, string>; nextHopInterface?: { name: string; id: string } }>;
warnings: string[];
}
export interface McpAction {
+44 -9
View File
@@ -13,13 +13,13 @@ import { setTimeout as sleep } from "node:timers/promises"
import { logsFollowCancellation } from "./logs-follow-cancellation.js"
import { z } from "zod"
import { logFiltersSchema, logSettingsPatchSchema } from "@mmapp/contracts"
import { appUserCreateSchema, appUserUpdateSchema, appUserRoleSchema, sectionPermSchema, serverPermSchema, userBindingCreateSchema } from "@mmapp/contracts/users"
import { appUserCreateSchema, appUserUpdateSchema, appUserRoleSchema, interfaceCatalogStateSchema, sectionPermSchema, serverPermSchema, userBindingCreateSchema } from "@mmapp/contracts/users"
import type { McpOperationDescriptor } from "@mmapp/contracts/mcp"
import { hasPermission } from "../../lib/permissions.js"
import { assertOperationReplayScope, findOperationReplayTombstone, operationHistoryExpired } from "./operation-replay-tombstones.js"
import { pool } from "../../db/index.js"
import { getServerReadById } from "../servers/service/servers-service.js"
import { listUsers, getUserById, listInterfaceCatalog, getInterfaceCatalogState, validateUserBindingCreate, UsersServiceError } from "../users/service/users-service.js"
import { listUsers, getUserById, getInterfaceCatalogState, projectLegacyInterfaceCatalog, validateUserBindingCreate, UsersServiceError } from "../users/service/users-service.js"
import { getLogSettings } from "../../services/router-logs-collector.js"
import { readLogPage, readLogSummary, readLogContext, readLogEntry } from "../../services/router-logs-query.js"
import { logWhere } from "../../services/router-logs-model.js"
@@ -31,7 +31,7 @@ import { readLogStream } from "../../services/router-logs-query.js"
import { subscribeLogDevice } from "../../services/router-logs-collector.js"
import { rosResources, readRosSnapshot, resourceById, routerClient, validateRosStep, splitOspfPassiveUpdate } from "./resources.js"
import { registerMcpOperation, listMcpOperations } from "./registry.js"
import { accessGeneration, decryptSecret, encryptSecret, fingerprint, McpError, redact } from "./security.js"
import { accessGeneration, decryptSecret, encryptSecret, fingerprint, MAX_MCP_PUBLIC_ARRAY_ITEMS, MAX_MCP_PUBLIC_STRING_LENGTH, McpError, redact } from "./security.js"
import { validatePrincipal } from "./store.js"
import type { McpAction, ActionContext, ChangeEvidence } from "./action-types.js"
import { registerRoutingActions } from "./routing-actions.js"
@@ -79,7 +79,7 @@ import { prepareGreNativeChange, registerGreNativeServices } from "./gre-native-
import { registerVpnCatalogActions } from "./vpn-catalog-actions.js"
import { registerEvoBgpActions } from "./evobgp-actions.js"
import { registerRoutingProfileActions } from "./routing-profiles.js"
import { registerRoutingControls } from "./routing-controls.js"
import { readRpfDependencies, registerRoutingControls } from "./routing-controls.js"
import { registerLifecycleActions } from "./lifecycle-actions.js"
import { registerRecoveryActions } from "./recovery-actions.js"
import { registerPathActions } from "./path-verification.js"
@@ -156,7 +156,7 @@ for (const resource of rosResources) {
? z.object({ serverId, fields: resource.fields.default({}), ...(resource.id === "vrf" ? { productionAdmission: z.literal(true).optional() } : {}) }).strict()
: z.object({ serverId, id: rowId.optional(), fields: resource.fields.default({}), ...(resource.id === "vrf" ? { productionAdmission: z.literal(true).optional() } : {}) }).strict()
const delegated = resource.id === "wireguard" && action !== "create" || resource.id === "wireguard_peer" || resource.id === "gre" && action !== "create" || resource.id === "ipsec_peer" && action !== "create" || resource.id === "certificate" && action === "delete" || resource.id === "container" && action === "delete"
const previousVersion = action === "create" ? (["ospf_template", "wireguard_peer", "wireguard"].includes(resource.id) ? 3 : 2) : resource.id === "ipsec_peer" || resource.id === "certificate" && action === "delete" ? 2 : resource.id === "wireguard" && action === "update" ? 3 : resource.id === "ospf_template" && action !== "delete" || resource.id === "wireguard_peer" || resource.id === "wireguard" ? 2 : 1
const previousVersion = action === "create" ? (["ospf_template", "wireguard_peer", "wireguard"].includes(resource.id) ? 3 : 2) : resource.id === "ipsec_peer" || resource.id === "certificate" && action === "delete" ? 2 : resource.id === "wireguard" && action === "update" ? 3 : resource.id === "ospf_template" && action !== "delete" || resource.id === "wireguard_peer" || resource.id === "wireguard" || resource.id === "ip_settings" ? 2 : 1
add(`ros.${resource.id}.${action}`, `mm_${resource.id}_${action}_plan`, `${resource.title}: ${action}`, resource.permission.split(":")[1], resource.id === "certificate" && action === "update" ? ["mm:certificates:write", "mm:network:write"] : resource.permission + ":write", "write", "servers", schema, {
version: resource.id === "ipsec_peer" ? (action === "create" ? 5 : 3) : resource.id === "certificate" ? (action === "update" ? 4 : 3) : previousVersion + (delegated ? 0 : 1) + (resource.id === "ospf_template" ? 1 : 0) + (resource.id === "vrf" ? (action === "delete" ? 3 : 2) : 0) + (["bridge", "bridge_port", "bridge_vlan"].includes(resource.id) ? 1 : 0),
prepare: async (input, context) => {
@@ -195,6 +195,8 @@ for (const resource of rosResources) {
const references: ChangeEvidence["objects"] = [{ kind: resource.id, id: id ?? String(fields.name ?? "new"), serverId: sid }]
if (resource.id.endsWith("settings")) references.push({ kind: resource.id, id: "singleton", serverId: sid })
const snapshots = [before]
const rpf = resource.id === "ip_settings" && fields["rp-filter"] ? await readRpfDependencies(sid, fields["rp-filter"], before.sourceProof) : undefined
if (rpf) snapshots.push(...rpf.snapshots)
if (["ospf_instance", "ospf_area", "ospf_template", "bfd_configuration", "bgp_instance", "bgp_connection", "vrf"].includes(resource.id)) await addRoutingDependencies(sid, resource.id, action, current, fields, snapshots, references)
if (["ipv4_route", "ipv6_route", "routing_rule", "bgp_connection"].includes(resource.id)) {
const tables = await readRosSnapshot(sid, "routing_table")
@@ -271,7 +273,7 @@ for (const resource of rosResources) {
...(resource.id === "vrf" && input.productionAdmission === true ? { vrfProductionAdmission: true as const } : {}),
steps: splitOspfPassiveUpdate({ serverId: sid, resource: resource.id, action, id, fields, ...(typeof placeBeforeId === "string" ? { placeBeforeId } : {}) }).map(step => ({ kind: "ros" as const, step })),
requiresHuman: true,
warnings: ["Нативное изменение может затронуть управление устройством и требует подтверждения человеком.", "Автоматическое каскадное изменение зависимостей не выполняется.", ...(resource.id === "vrf" && action === "delete" ? ["При удалении VRF RouterOS может удалить автоматический IPv6 link-local и его connected-маршрут пустого bridge; статические адреса сохраняются."] : []), ...(placeBeforeId ? [`VRF создаётся перед записью ${placeBeforeId}; порядок определяет принадлежность интерфейсов.`] : [])],
warnings: ["Нативное изменение может затронуть управление устройством и требует подтверждения человеком.", "Автоматическое каскадное изменение зависимостей не выполняется.", ...(rpf?.warnings ?? []), ...(resource.id === "vrf" && action === "delete" ? ["При удалении VRF RouterOS может удалить автоматический IPv6 link-local и его connected-маршрут пустого bridge; статические адреса сохраняются."] : []), ...(placeBeforeId ? [`VRF создаётся перед записью ${placeBeforeId}; порядок определяет принадлежность интерфейсов.`] : [])],
}
},
})
@@ -316,11 +318,33 @@ add("users.list", "mm_users_list", "Пользователи", "users", "mm:user
add("users.get", "mm_users_get", "Пользователь по ID", "users", "mm:users:read", "read", "global", z.object({ id: rowId }).strict(), {
read: async (input, context) => ({ user: scopedUser(await getUserById(String(input.id)), context) }),
})
const invalidInterfaceCatalog = () => new McpError("STATE_UNAVAILABLE", "Не подтверждён контекст каталога интерфейсов.", 502)
async function readMcpInterfaceCatalog(serverId: number) {
const state = await getInterfaceCatalogState(serverId).catch(error => {
if (error instanceof UsersServiceError) {
if (error.status === 404) throw new McpError("NOT_FOUND", "Сервер не найден", 404)
if (error.status === 502) throw invalidInterfaceCatalog()
}
throw error
})
const parsed = interfaceCatalogStateSchema.safeParse(state)
if (!parsed.success || parsed.data.serverId !== serverId) throw invalidInterfaceCatalog()
return parsed.data
}
function catalogueExceedsPublicLimits(value: unknown): boolean {
if (typeof value === "string") return value.length > MAX_MCP_PUBLIC_STRING_LENGTH
if (Array.isArray(value)) return value.length > MAX_MCP_PUBLIC_ARRAY_ITEMS || value.some(catalogueExceedsPublicLimits)
return value !== null && typeof value === "object" && Object.values(value).some(catalogueExceedsPublicLimits)
}
function intactInterfaceCatalog<T>(result: T): T {
if (catalogueExceedsPublicLimits(result)) throw new McpError("BUDGET_EXCEEDED", "Каталог интерфейсов превышает предел ответа MCP.", 413)
return result
}
add("users.interfaces.read", "mm_user_interfaces_list", "Каталог интерфейсов для привязок пользователей", "users", "mm:users:read", "read", "servers", z.object({ serverId }).strict(), {
read: async input => ({ interfaces: await listInterfaceCatalog(Number(input.serverId)) }),
read: async input => intactInterfaceCatalog({ interfaces: projectLegacyInterfaceCatalog(await readMcpInterfaceCatalog(Number(input.serverId))) }),
})
add("users.interfaces.state", "mm_user_interfaces_state_get", "Состояние каталога интерфейсов для привязок", "users", "mm:users:read", "read", "servers", z.object({ serverId }).strict(), {
read: async input => getInterfaceCatalogState(Number(input.serverId)),
read: async input => intactInterfaceCatalog(await readMcpInterfaceCatalog(Number(input.serverId))),
})
for (const action of ["create", "update", "delete", "binding_add", "binding_remove"] as const) {
const schema = action === "create" ? z.object({ user: appUserCreateSchema.omit({ role: true, sections: true, servers: true }).strict() }).strict()
@@ -465,7 +489,18 @@ add("logs.settings.update", "mm_logs_settings_update_plan", "Изменение
prepare: async (input, context) => {
const ids = (await pool.query<{ id: number }>("SELECT id FROM servers")).rows.map(row => Number(row.id))
if (ids.some(id => !context.connection.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Глобальная настройка сборщика затрагивает скрытые серверы")
return { target: { scope: "global" }, objects: ids.map(serverId => ({ kind: "server", id: String(serverId), serverId })), snapshots: [], before: await getLogSettings(), steps: [{ kind: "service", service: "logs.settings.update", input }], warnings: ["Отключение сбора и уменьшение retention влияют на последующую доступность истории."] }
const before = await getLogSettings()
const settings = logSettingsPatchSchema.parse(input.settings)
const warnings: string[] = []
if (before.enabled && settings.enabled === false) warnings.push("После применения сборщик прекратит получать новые события.")
if (settings.retentionDays !== undefined && settings.retentionDays < before.retentionDays) {
warnings.push(`Срок хранения сокращается с ${before.retentionDays} до ${settings.retentionDays} суток. При последующей очистке удаляются записи, полученные более ${settings.retentionDays} суток назад.`)
}
return {
target: { scope: "global" },
objects: ids.map(serverId => ({ kind: "server", id: String(serverId), serverId })),
snapshots: [], before, steps: [{ kind: "service", service: "logs.settings.update", input }], warnings,
}
},
})
@@ -1,5 +1,5 @@
import assert from "node:assert/strict"
import { randomUUID, createHmac } from "node:crypto"
import { randomUUID, createHash, createHmac } from "node:crypto"
import { readFile, writeFile } from "node:fs/promises"
import path from "node:path"
import type { PoolClient } from "pg"
@@ -8,7 +8,7 @@ import { pool, closePool } from "../../db/index.js"
import { mcpConnectionCreateSchema } from "@mmapp/contracts/mcp"
import { type AuthUser } from "../../lib/permissions.js"
import { accessGeneration, mcpStateDirectory } from "./security.js"
import { createMcpConnection, authenticateMcpToken, updateMcpSettings } from "./store.js"
import { createMcpConnection, authenticateMcpToken, validatePrincipal, updateMcpSettings } from "./store.js"
import { invokeMcpTool, approvePlan, applyPlan } from "./runtime.js"
import { uploadRestoreArchive, readRestoreReceipt, executeMcpDatabaseRestore } from "./database-restore.js"
import { exportSystemDatabaseBackup, restoreSystemDatabaseBackup } from "../../services/system-database-backup.js"
@@ -51,25 +51,39 @@ try {
await updateMcpSettings({ enabled: true }, actor)
await pool.query("CREATE TABLE mcp_restore_acceptance_marker (value text NOT NULL)")
await pool.query("INSERT INTO mcp_restore_acceptance_marker VALUES('before')")
const dump = await exportSystemDatabaseBackup()
assert.equal(dump.buffer.subarray(0, 5).toString(), "PGDMP")
if (verifyData) dataExpectation = await (await import("./database-restore-data-equivalence.sql-helper.js")).prepareOwnedRestoreDataExpectation(dump.buffer)
const revokedOwner = { id: "restore-revocation-after-backup", issuedAt: Date.now() - 60000 }
const portalSecret = "restore-acceptance-only-portal-secret-32", stamp = String(Math.floor(Date.now() / 1000))
const portalEvent = Buffer.from(JSON.stringify({ eventId: randomUUID(), userId: revokedOwner.id, type: "permissions.changed" }))
await acceptPortalRevocation(portalEvent, stamp, "sha256=" + createHmac("sha256", portalSecret).update(stamp + ".").update(portalEvent).digest("hex"), portalSecret)
const source = await uploadRestoreArchive(dump.buffer, actor)
const encrypted = await readFile(path.join(mcpStateDirectory(), "restore-vault", source.archiveRef + ".archive.bin"))
assert.ok(!encrypted.equals(dump.buffer))
assert.ok(!encrypted.subarray(0, 5).equals(Buffer.from("PGDMP")))
await assert.rejects(uploadRestoreArchive(dump.buffer, { ...actor, permissions: ["mm:mcp:admin"] }))
await assert.rejects(uploadRestoreArchive(Buffer.from("invalid"), actor))
const serverIds = (await pool.query("SELECT id FROM servers")).rows.map(row => Number(row.id))
// Combined owned profiles retain their evidence/disabled synthetic servers.
// The human restore fixture grants this exact catalog instead of inheriting
// the ordinary five-server default; production policy guards stay unchanged.
assert(serverIds.length <= 1000)
const issue = (owner = actor) => createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Database restore acceptance", mode: "approval", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:settings:admin"], scope: { allowGlobal: true, serverIds, operationIds: ["database.restore"] }, limits: { serversPerPlan: Math.max(1, serverIds.length) } }), owner)
const cutoff = Date.now() - 1000
const revokedOwner = { ...actor, id: "restore-revocation-after-backup", issuedAt: cutoff - 1 }
const oldAccess = await issue(revokedOwner), oldPrincipal = await authenticateMcpToken(oldAccess.token)
const readHistoricalAccess = async () => (await pool.query<{ status: string; credential_current: boolean; generation: string }>(
"SELECT c.policy->>'status' AS status, k.revoked_at IS NULL AND k.expires_at>now() AS credential_current, c.generation FROM mcp_connections c JOIN mcp_credentials k ON k.connection_id=c.id WHERE c.id=$1 AND k.id=$2",
[oldPrincipal.connectionId, oldPrincipal.credentialId],
)).rows[0]
const beforeDumpAccess = await readHistoricalAccess()
assert.deepEqual(beforeDumpAccess, { status: "active", credential_current: true, generation: await accessGeneration() })
const dump = await exportSystemDatabaseBackup()
assert.equal(dump.buffer.subarray(0, 5).toString(), "PGDMP")
if (verifyData) dataExpectation = await (await import("./database-restore-data-equivalence.sql-helper.js")).prepareOwnedRestoreDataExpectation(dump.buffer)
const portalSecret = "restore-acceptance-only-portal-secret-32", stamp = String(Math.floor(Date.now() / 1000))
const eventId = randomUUID()
const portalEvent = Buffer.from(JSON.stringify({ version: 2, eventId, userId: revokedOwner.id, type: "permissions.changed", eventOccurredAt: cutoff }))
assert.deepEqual(await acceptPortalRevocation(portalEvent, stamp, "sha256=" + createHmac("sha256", portalSecret).update(stamp + ".").update(portalEvent).digest("hex"), portalSecret), { ok: true, duplicate: false })
const cutoffFile = path.join(mcpStateDirectory(), "owner-revocations", createHash("sha256").update(revokedOwner.id).digest("hex") + ".json")
const cutoffBytes = await readFile(cutoffFile)
assert.deepEqual(JSON.parse(cutoffBytes.toString("utf8")), { invalidBefore: cutoff })
assert.deepEqual(await readHistoricalAccess(), { ...beforeDumpAccess, status: "revoked", credential_current: false })
await assert.rejects(validatePrincipal(oldPrincipal), { code: "OWNER_CLAIMS_REVOKED" })
const source = await uploadRestoreArchive(dump.buffer, actor)
const encrypted = await readFile(path.join(mcpStateDirectory(), "restore-vault", source.archiveRef + ".archive.bin"))
assert.ok(!encrypted.equals(dump.buffer))
assert.ok(!encrypted.subarray(0, 5).equals(Buffer.from("PGDMP")))
await assert.rejects(uploadRestoreArchive(dump.buffer, { ...actor, permissions: ["mm:mcp:admin"] }))
await assert.rejects(uploadRestoreArchive(Buffer.from("invalid"), actor))
const issued = await issue(), principal = await authenticateMcpToken(issued.token)
const other = await issue({ ...actor, id: "another-owner" }), otherPrincipal = await authenticateMcpToken(other.token)
await assert.rejects(invokeMcpTool(otherPrincipal, "mm_database_restore_plan", { archiveRef: source.archiveRef, expectedSha256: source.sha256 }))
@@ -144,13 +158,26 @@ try {
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_operations WHERE id=$1", [operation.id])).rows[0].n, 0)
assert.notEqual(await accessGeneration(), generation)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_owner_revocations WHERE owner_id=$1", [revokedOwner.id])).rows[0].n, 0)
await assert.rejects(assertPortalClaimsCurrent(revokedOwner))
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_portal_events WHERE id=$1", [eventId])).rows[0].n, 0)
assert.deepEqual(await readHistoricalAccess(), beforeDumpAccess)
assert.deepEqual(await readFile(cutoffFile), cutoffBytes)
// Both paths must report the external owner cutoff, independently of the
// blanket generation change that also invalidates older connections.
await assert.rejects(assertPortalClaimsCurrent(revokedOwner), { code: "OWNER_CLAIMS_REVOKED" })
await assert.rejects(authenticateMcpToken(oldAccess.token), { code: "OWNER_CLAIMS_REVOKED" })
await assert.rejects(validatePrincipal(oldPrincipal), { code: "OWNER_CLAIMS_REVOKED" })
await assert.rejects(authenticateMcpToken(issued.token))
await assert.rejects(readRestoreReceipt(operation.id, { ...actor, permissions: ["mm:mcp:read"] }))
// The applied receipt is durable before the terminal callback releases its
// last SQL lease/exclusive admission. Await that existing owner, rather than
// racing its cleanup with the stale-callback/no-replay assertion below.
await waitForDatabaseRetirement()
await assert.rejects(issue(revokedOwner), { code: "OWNER_CLAIMS_REVOKED" })
const refreshedOwnerAccess = await issue({ ...revokedOwner, issuedAt: cutoff + 1 })
const refreshedOwner = await validatePrincipal(await authenticateMcpToken(refreshedOwnerAccess.token))
assert.equal(refreshedOwner.owner_id, revokedOwner.id)
assert.equal(refreshedOwner.generation, await accessGeneration())
assert.notEqual(refreshedOwner.generation, beforeDumpAccess.generation)
// A stale callback or process replay cannot invoke pg_restore a second time.
await pool.query("UPDATE mcp_restore_acceptance_marker SET value='after-receipt'")
await executeMcpDatabaseRestore(operation.id, principal)
@@ -159,7 +186,7 @@ try {
if (dataExpectation) {
console.log("Owned restore data negative cases: foreign owner, wrong operation and unconfirmed receipt refused; repeat validation writes no audit; full equality observed before consumers resume")
}
console.log("✓ Terminal restore: real pg_dump/pg_restore transaction, encrypted human upload, approval, global protection, request drain, external receipt, revoked tokens, preserved external owner revocation and no replay")
console.log("✓ Terminal restore: real pg_dump/pg_restore transaction, encrypted human upload, approval, global protection, request drain, external receipt, restored live credential refused by external Portal cutoff, post-cutoff claims admitted and no replay")
// Archive authentication is checked before a terminal attempt can replace data.
await updateMcpSettings({ enabled: true }, actor)
+45 -1
View File
@@ -1,11 +1,12 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { analyzeEcmpPreflight, preflightObservation, preflightPaths, type PreflightObservation, type PreflightSource } from "./ecmp-preflight.js"
import { analyzeEcmpPreflight, duplicateEcmpAdjacency, ecmpAdjacency, preflightObservation, preflightPaths, type PreflightObservation, type PreflightSource } from "./ecmp-preflight.js"
function fixture(): Record<PreflightSource, PreflightObservation> {
const result = Object.fromEntries(Object.keys(preflightPaths("ipv4")).map(key => [key, { rows: [], error: null }])) as Record<PreflightSource, PreflightObservation>
result.routes.rows = [1, 2].map(index => ({ ".id": `*${index}`, "dst-address": "192.0.2.0/24", "routing-table": "main", gateway: `192.0.2.${index}`, "immediate-gw": `192.0.2.${index}%gre${index}`, disabled: "false", active: index === 1 ? "true" : "false", distance: String(index * 10) }))
result.settings.rows = [{ "rp-filter": "no", "ipv4-multipath-hash-policy": "l3", "multipath-hash-policy": "l4" }]
result.tables.rows = [{ ".id": "*main", name: "main", fib: "true" }]
result.connectionTracking.rows = [{ enabled: "auto" }]
return result
}
@@ -15,6 +16,23 @@ test("preflight observes unequal/inactive candidates without claiming packet/pat
assert.equal(result.decision, "review-required"); assert.equal(result.independentPaths, "not-proven"); assert.equal(result.networkValidation, "not-performed")
assert.equal(result.hashPolicy, "l3"); assert.deepEqual(result.findings, [])
})
test("preflight flags missing, ambiguous or inactive FIB without accepting unrelated tables", () => {
for (const tables of [
[],
[{ ".id": "*main", name: "main" }],
[{ ".id": "*main", name: "main", fib: "" }],
[{ ".id": "*main", name: "main", fib: "false" }],
[{ ".id": "*main", name: "main", fib: "true", disabled: "true" }],
[{ ".id": "*main", name: "main", fib: "true", invalid: "true" }],
[{ ".id": "*main", name: "main", fib: "true" }, { ".id": "*alias", name: "main", fib: "true" }],
]) {
const sources = fixture()
sources.tables.rows = [...tables, { ".id": "*other", name: "other", fib: "true" }]
const result = analyzeEcmpPreflight("ipv4", ["*1", "*2"], sources)
assert.equal(result.configurationObserved, false)
assert.deepEqual(result.findings.find(item => item.code === "FIB_TABLE_UNCONFIRMED")?.ids, ["*1", "*2"])
}
})
test("NAT, FastTrack, marks, notrack, strict RPF and shared egress require explicit review", () => {
const sources = fixture()
sources.filter.rows = [{ ".id": "*fast", action: "fasttrack-connection", disabled: "false" }, { ".id": "*disabled", action: "fasttrack-connection", disabled: "true" }]
@@ -52,3 +70,29 @@ test("IPv6 uses IPv6 paths/hash policy and explicitly has no IPv4 RPF setting",
assert.deepEqual(result.rpf, { supported: false, mode: null }); assert.equal(result.hashPolicy, "l4")
assert.ok(!result.findings.some(item => item.code.startsWith("RPF")))
})
test("ECMP identifies adjacency by canonical address and egress link", () => {
const duplicate = (first: string, second: string) => duplicateEcmpAdjacency([ecmpAdjacency(first), ecmpAdjacency(second)])
assert.ok(duplicate("fe80::1%gre1@main", "fe80:0:0:0:0:0:0:1%gre1@tenant"))
assert.ok(!duplicate("fe80::1%gre1", "fe80::1%gre2"))
assert.ok(!duplicate("192.0.2.1%gre1", "192.0.2.2%gre1"))
assert.ok(duplicate("192.0.2.1", "192.0.2.1%gre1"))
assert.equal(ecmpAdjacency("fe80::1"), null)
assert.equal(ecmpAdjacency("192.0.2.1%gre1,192.0.2.2%gre2"), null)
assert.equal(ecmpAdjacency("gre1"), null)
assert.deepEqual(ecmpAdjacency("gre1", new Set(["gre1"])), { address: null, interface: "gre1" })
const sources = fixture()
sources.routes.rows[1]["immediate-gw"] = sources.routes.rows[0]["immediate-gw"]
assert.ok(analyzeEcmpPreflight("ipv4", ["*1", "*2"], sources).findings.some(item => item.code === "DUPLICATE_IMMEDIATE_GATEWAY"))
})
test("interface-only ECMP next-hop requires a unique observed name", () => {
const sources = fixture()
sources.routes.rows[0]["immediate-gw"] = "gre2"
const analyze = (rows: Record<string, unknown>[]) => analyzeEcmpPreflight("ipv4", ["*1", "*2"], sources, preflightObservation(rows))
const known = analyze([{ ".id": "*gre", name: "gre2" }])
assert.equal(known.configurationObserved, true)
assert.ok(known.findings.some(item => item.code === "SHARED_EGRESS_INTERFACE"))
const ambiguous = analyze([{ ".id": "*gre", name: "gre2" }, { ".id": "*other", name: "gre2" }])
assert.equal(ambiguous.configurationObserved, false)
assert.ok(ambiguous.findings.some(item => item.code === "EGRESS_INTERFACE_UNKNOWN"))
assert.equal(analyze([{ name: "gre2" }]).sources.interfaces.observed, false)
})
+63 -4
View File
@@ -1,4 +1,16 @@
import { ipv6ToInt } from "../../services/traffic-flow-ip.js"
import { isConfirmedRoutingFibTable } from "../../services/routing-table-fib.js"
import { routeGatewayContext } from "./routing-community-context-model.js"
type Row = Record<string, unknown>
export function confirmedEcmpTable(rows: readonly Row[], name: string) {
const matches = rows.filter(row => row.name === name)
if (matches.length !== 1) return undefined
const table = matches[0], id = table[".id"]
// The /routing/table catalogue can return an empty FIB value for a non-FIB table.
if (typeof id !== "string" || !id.trim() || table.fib === "" || !isConfirmedRoutingFibTable(table)) return undefined
return { id }
}
export type PreflightObservation = { rows: Row[]; error: string | null }
export const preflightPaths = (family: "ipv4" | "ipv6") => {
const base = family === "ipv4" ? "ip" : "ipv6"
@@ -17,12 +29,48 @@ export function preflightObservation(raw: unknown, singleton = false): Preflight
const text = (value: unknown) => typeof value === "string" && value.trim() ? value.trim() : null
const flag = (value: unknown) => value === true || value === "true" || value === "yes" ? true : value === false || value === "false" || value === "no" ? false : null
export function ecmpAdjacency(value: unknown, interfaces?: ReadonlySet<string>) {
if (typeof value !== "string" || !value || /[\r\n,]/.test(value)) return null
try {
const hop = routeGatewayContext(value)
const address = hop.family === 6 ? ipv6ToInt(hop.address) : hop.address
// fe80::/10 needs a confirmed link to identify the adjacency.
if (address === null || (typeof address === "bigint" && address >> 118n === 0x3fan && hop.interface === null)) return null
return { address, interface: hop.interface }
} catch {
return interfaces?.has(value) ? { address: null, interface: value } : null
}
}
export function confirmedEcmpInterfaces(rows: readonly Row[]) {
const names = new Map<string, string>()
const ambiguous = new Set<string>()
for (const row of rows) {
if (typeof row.name !== "string" || !row.name.trim() || typeof row[".id"] !== "string" || !row[".id"].trim()) continue
if (names.has(row.name)) ambiguous.add(row.name)
names.set(row.name, row[".id"])
}
for (const name of ambiguous) names.delete(name)
return names
}
export function confirmedEcmpInterfaceNames(rows: readonly Row[]) {
return new Set(confirmedEcmpInterfaces(rows).keys())
}
export function duplicateEcmpAdjacency(hops: ReturnType<typeof ecmpAdjacency>[]) {
return hops.some((hop, index) => hop && hops.slice(0, index).some(previous =>
previous && previous.address === hop.address
&& (previous.interface === hop.interface || previous.interface === null || hop.interface === null)))
}
/** Configuration evidence only. Rule presence does not prove packet matching,
* physical independence, NAT affinity, return path or traffic distribution. */
export function analyzeEcmpPreflight(family: "ipv4" | "ipv6", routeIds: string[], observed: Record<PreflightSource, PreflightObservation>) {
export function analyzeEcmpPreflight(family: "ipv4" | "ipv6", routeIds: string[], observed: Record<PreflightSource, PreflightObservation>, inventory?: PreflightObservation) {
const findings: { code: string; level: "review" | "unknown"; resource: string; ids: string[]; objectCount: number; idsTruncated: boolean; message: string }[] = []
const add = (code: string, level: "review" | "unknown", resource: string, ids: string[], message: string) => findings.push({ code, level, resource, ids: ids.slice(0, 100), objectCount: ids.length, idsTruncated: ids.length > 100, message })
for (const [resource, observation] of Object.entries(observed)) if (observation.error) add("OBSERVATION_UNAVAILABLE", "unknown", resource, [], observation.error)
const sources = inventory ? { ...observed, interfaces: inventory } : observed
for (const [resource, observation] of Object.entries(sources)) if (observation.error) add("OBSERVATION_UNAVAILABLE", "unknown", resource, [], observation.error)
const candidates = routeIds.map(id => observed.routes.rows.find(row => row[".id"] === id))
const selected = candidates.filter((row): row is Row => !!row)
if (selected.length !== routeIds.length) add("ROUTE_NOT_OBSERVED", "unknown", "routes", routeIds.filter((_id, index) => !candidates[index]), "Не все выбранные маршруты подтверждены")
@@ -30,9 +78,20 @@ export function analyzeEcmpPreflight(family: "ipv4" | "ipv6", routeIds: string[]
if (routeSummary.some(row => !row.prefix || !row.table || !row.gateway || !row.immediateGateway || row.disabled === null)) add("ROUTE_CONTEXT_UNKNOWN", "unknown", "routes", routeSummary.map(row => row.id), "Не подтверждены prefix/table/gateway/disabled; отсутствие свойства не считается значением по умолчанию")
if (routeSummary.some(row => row.disabled === true)) add("DISABLED_ROUTE", "review", "routes", routeSummary.filter(row => row.disabled === true).map(row => row.id), "В наборе есть отключённые маршруты")
if (new Set(routeSummary.map(row => row.prefix).filter(Boolean)).size > 1 || new Set(routeSummary.map(row => row.table).filter(Boolean)).size > 1) add("DIFFERENT_ROUTE_CONTEXTS", "review", "routes", routeSummary.map(row => row.id), "Prefix или routing table выбранных маршрутов различаются")
if (observed.tables.error === null) {
const unconfirmed = new Set<string>()
for (const table of new Set(routeSummary.map(row => row.table))) {
if (table !== null && !confirmedEcmpTable(observed.tables.rows, table)) unconfirmed.add(table)
}
const affected = routeSummary.filter(row => row.table !== null && unconfirmed.has(row.table))
if (affected.length) add("FIB_TABLE_UNCONFIRMED", "unknown", "routes", affected.map(row => row.id), "Для выбранных маршрутов не подтверждена однозначная активная таблица FIB")
}
const knownGateways = routeSummary.map(row => row.gateway).filter(Boolean)
if (new Set(knownGateways).size !== knownGateways.length) add("DUPLICATE_GATEWAY", "review", "routes", routeSummary.map(row => row.id), "Next-hop выбранных маршрутов повторяется")
const interfaces = routeSummary.map(row => row.immediateGateway?.split("%")[1]?.split("@")[0] || null)
const names = inventory?.error === null ? confirmedEcmpInterfaceNames(inventory.rows) : undefined
const hops = selected.map(row => ecmpAdjacency(row["immediate-gw"], names))
if (duplicateEcmpAdjacency(hops)) add("DUPLICATE_IMMEDIATE_GATEWAY", "review", "routes", routeSummary.map(row => row.id), "Выбранные маршруты разрешаются в один и тот же next-hop")
const interfaces = hops.map(hop => hop?.interface ?? null)
if (interfaces.some(iface => iface === null)) add("EGRESS_INTERFACE_UNKNOWN", "unknown", "routes", routeSummary.map(row => row.id), "Не подтверждён интерфейс immediate gateway")
else if (new Set(interfaces).size < interfaces.length) add("SHARED_EGRESS_INTERFACE", "review", "routes", routeSummary.map(row => row.id), "Несколько next-hop используют один интерфейс; независимость путей не подтверждена")
const maybeEnabled = (resource: PreflightSource) => {
@@ -59,5 +118,5 @@ export function analyzeEcmpPreflight(family: "ipv4" | "ipv6", routeIds: string[]
if (!connectionTracking) add("CONNECTION_TRACKING_UNKNOWN", "unknown", "connectionTracking", [], "Режим connection tracking не подтверждён")
else if (connectionTracking === "no" || connectionTracking === "false") add("CONNECTION_TRACKING_DISABLED", "review", "connectionTracking", [], "Connection tracking отключён; проверить зависимости NAT/stateful firewall")
return { family, decision: "review-required", configurationObserved: !findings.some(item => item.level === "unknown"), networkValidation: "not-performed", independentPaths: "not-proven", routeSummary, hashPolicy, rpf: { supported: family === "ipv4", mode: rpf }, connectionTracking,
sources: Object.fromEntries(Object.entries(observed).map(([key, value]) => [key, { observed: value.error === null, count: value.rows.length }])), findings }
sources: Object.fromEntries(Object.entries(sources).map(([key, value]) => [key, { observed: value.error === null, count: value.rows.length }])), findings }
}
+40 -15
View File
@@ -1282,6 +1282,7 @@ try {
preflightFixtures = Object.fromEntries(Object.values(preflightPaths("ipv4")).map(path => ["/rest" + path, []]))
preflightFixtures["/rest/ip/route"] = [1, 2].map(index => ({ ".id": `*p${index}`, "dst-address": "192.0.2.0/24", gateway: `192.0.2.${index}`, "immediate-gw": `192.0.2.${index}%same-gre`, "routing-table": "main", distance: "10", active: "true", disabled: "false" }))
preflightFixtures["/rest/ip/settings"] = { "rp-filter": "strict", "ipv4-multipath-hash-policy": "l3" }
preflightFixtures["/rest/routing/table"] = [{ ".id": "*main", name: "main", fib: "true" }]
preflightFixtures["/rest/ip/firewall/connection/tracking"] = { enabled: "auto" }
preflightFixtures["/rest/ip/firewall/filter"] = [{ ".id": "*ft", action: "fasttrack-connection", disabled: "false" }]
preflightFixtures["/rest/ip/firewall/nat"] = [{ ".id": "*nat", action: "masquerade", disabled: "false" }]
@@ -1293,6 +1294,11 @@ try {
assert.equal(preflight.configurationObserved, true); assert.equal(preflight.networkValidation, "not-performed"); assert.equal(catalogCalls, 10)
for (const code of ["SHARED_EGRESS_INTERFACE", "STRICT_RPF_REVIEW", "FASTTRACK_REVIEW", "NAT_AFFINITY_REVIEW"]) assert.ok(preflight.findings.some(item => item.code === code), code)
await assert.rejects(invokeMcpTool(preflightPrincipal, "mm_ecmp_preflight_get", { ...preflightInput, serverId: 2 })); assert.equal(catalogCalls, 10)
preflightFixtures["/rest/routing/table"] = [{ ".id": "*main", name: "main", fib: "" }]
const noFibPreflight = await invokeMcpTool(preflightPrincipal, "mm_ecmp_preflight_get", preflightInput) as typeof preflight
assert.equal(noFibPreflight.configurationObserved, false)
assert.ok(noFibPreflight.findings.some(item => item.code === "FIB_TABLE_UNCONFIRMED"))
preflightFixtures["/rest/routing/table"] = [{ ".id": "*main", name: "main", fib: "true" }]
preflightUnavailable = "/rest/ip/firewall/nat"
const unknownPreflight = await invokeMcpTool(preflightPrincipal, "mm_ecmp_preflight_get", preflightInput) as typeof preflight
assert.equal(unknownPreflight.configurationObserved, false); assert.ok(unknownPreflight.findings.some(item => item.code === "OBSERVATION_UNAVAILABLE")); assert.equal(writes, preflightWritesBefore)
@@ -2224,7 +2230,7 @@ try {
await pool.query("INSERT INTO mcp_routing_observations(id,src_server_id,dst_server_id,src_interface,dst_interface,family,routing_table,ping_ms,loss_pct,dl_mbps,ul_mbps,ping_at,bandwidth_at,context_verified,source) VALUES($1,1,2,'fixture-gre','peer','ipv4','main',12,0,100,200,now(),now(),true,'fixture')", [observationId])
await attestRoutingFixture(observationId)
try {
type OspfAnalysis = { ranking: { currentCost: number; proposedCost: number; observationId: string; peerId: number }[]; serverIds: number[]; changes: number; proposalOnly: boolean; executionAvailable: boolean }
type OspfAnalysis = { ranking: { currentCost: number; proposedCost: number; observationId: string; bandwidthObservationId: string | null; peerId: number }[]; serverIds: number[]; changes: number; proposalOnly: boolean; executionAvailable: boolean }
const applicationPreviewPolicy={name:"OSPF application preview",mode:"observer",expiresAt:new Date(Date.now()+1800000).toISOString(),permissions:["mm:network:read"],scope:{serverIds:[1,2],operationIds:["routing.ospf.application.preview"],allowGlobal:false}}
const applicationPreviewAccess=await createMcpConnection(mcpConnectionCreateSchema.parse(applicationPreviewPolicy),actor),applicationPreviewPrincipal=await authenticateMcpToken(applicationPreviewAccess.token)
const isolatedPreviewAccess=await createMcpConnection(mcpConnectionCreateSchema.parse({...applicationPreviewPolicy,name:"OSPF source-only preview",scope:{...applicationPreviewPolicy.scope,serverIds:[1]}}),actor),isolatedPreviewPrincipal=await authenticateMcpToken(isolatedPreviewAccess.token)
@@ -2388,10 +2394,14 @@ try {
})}finally{countCatalogCalls=false}
assert.equal((await (await import("./routing-observations.js")).readRoutingObservations([1, 2]))[0].sourceVerified, false, "Disable/re-enable must invalidate previous source attestation")
await attestRoutingFixture(observationId)
const pingOnlyId = randomUUID(), otherPeerInterfaceId = randomUUID()
const insertPingObservation = (id: string, peerInterface: string) => pool.query("INSERT INTO mcp_routing_observations(id,src_server_id,dst_server_id,src_interface,dst_interface,family,routing_table,ping_ms,loss_pct,ping_at,context_verified,source) VALUES($1,1,2,'fixture-gre',$2,'ipv4','main',11,0,now(),true,'fixture')", [id, peerInterface])
await insertPingObservation(pingOnlyId, "peer")
await attestRoutingFixture(pingOnlyId)
const proposal = await invokeMcpTool(analysisPrincipal, "mm_ospf_optimizer_analyze", { serverId: 1 }) as OspfAnalysis
assert.deepEqual(proposal.serverIds, [1, 2]); assert.equal(proposal.changes, 1)
assert.equal(proposal.ranking[0].currentCost, 30); assert.equal(proposal.ranking[0].proposedCost, 10)
assert.equal(proposal.ranking[0].observationId, observationId); assert.equal(proposal.ranking[0].peerId, 2)
assert.equal(proposal.ranking[0].observationId, pingOnlyId); assert.equal(proposal.ranking[0].bandwidthObservationId, observationId); assert.equal(proposal.ranking[0].peerId, 2)
assert.equal(proposal.proposalOnly, true); assert.equal(proposal.executionAvailable, false)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_plans WHERE connection_id=$1", [analysisAccess.connection.id])).rows[0].n, 0)
assert.equal(ospfAnalysisDispatches, 0)
@@ -2405,6 +2415,9 @@ try {
await verifyRoutingObservationChecks(observationEvidence.observationChecks)
await assert.rejects(verifyRoutingObservationChecks(observationEvidence.observationChecks.map((check: Record<string, unknown>) => ({ ...check, validUntil: new Date(Date.now() - 1).toISOString() }))))
await approvePlan(prepared.planId, actor, true)
await insertPingObservation(otherPeerInterfaceId, "another-peer-interface")
await attestRoutingFixture(otherPeerInterfaceId)
await verifyRoutingObservationChecks(observationEvidence.observationChecks)
await pool.query("UPDATE mcp_routing_observations SET created_at=now()-interval '1 minute' WHERE id=$1", [observationId])
await attestRoutingFixture(observationId)
await pool.query("INSERT INTO mcp_routing_observations(id,src_server_id,dst_server_id,src_interface,dst_interface,family,routing_table,ping_ms,loss_pct,dl_mbps,ul_mbps,ping_at,bandwidth_at,context_verified,source) VALUES($1,1,2,'fixture-gre','peer','ipv4','main',12,50,100,200,now(),now(),true,'fixture')", [badObservationId])
@@ -2422,7 +2435,7 @@ try {
await pool.query("UPDATE mcp_routing_observations SET loss_pct=0,context_verified=false WHERE id=$1", [badObservationId])
await attestRoutingFixture(badObservationId)
await assert.rejects(invokeMcpTool(analysisPrincipal, "mm_ospf_optimizer_analyze", { serverId: 1 }))
await pool.query("DELETE FROM mcp_routing_observations WHERE id=$1", [badObservationId])
await pool.query("DELETE FROM mcp_routing_observations WHERE id=ANY($1::uuid[])", [[badObservationId, pingOnlyId, otherPeerInterfaceId]])
await pool.query("UPDATE mcp_routing_observations SET loss_pct=5 WHERE id=$1", [observationId])
await attestRoutingFixture(observationId)
await assert.rejects(verifyRoutingObservationChecks(observationEvidence.observationChecks))
@@ -2589,9 +2602,10 @@ try {
const nativeRequests = ospfAnalysisNativeRequests - requestsBeforeApply
assert.ok(nativeRequests <= 160, `One OSPF cost PATCH caused ${nativeRequests} native requests; full graph polling must not run inside each catalog callback`)
console.log(`✓ OSPF dispatch read budget: ${nativeRequests} native requests across three owners, including one PATCH; no nested full graph polling`)
await pool.query("UPDATE mcp_plans SET operation_version=8 WHERE id=$1", [prepared.planId])
const { rows: [version] } = await pool.query<{ operation_version: number }>("SELECT operation_version FROM mcp_plans WHERE id=$1", [prepared.planId])
await pool.query("UPDATE mcp_plans SET operation_version=$2 WHERE id=$1", [prepared.planId, version.operation_version - 1])
try { await assert.rejects(invokeMcpTool(graphPrincipal, "mm_operation_recovery_plan", { operationId: operation.id }), (error: { code?: string }) => error.code === "PLAN_EXPIRED") }
finally { await pool.query("UPDATE mcp_plans SET operation_version=9 WHERE id=$1", [prepared.planId]) }
finally { await pool.query("UPDATE mcp_plans SET operation_version=$2 WHERE id=$1", [prepared.planId, version.operation_version]) }
const oldInverse = await invokeMcpTool(graphPrincipal, "mm_operation_recovery_plan", { operationId: operation.id }) as { planId: string }
await approvePlan(oldInverse.planId, actor, true)
await pool.query("UPDATE mcp_plans SET operation_version=1 WHERE id=$1", [oldInverse.planId])
@@ -2755,8 +2769,8 @@ try {
await approvePlan(brokenEcmp.planId, actor, true)
loseEcmpGateway = true
const brokenOperation = await applyPlan(ecmpPrincipal, brokenEcmp.planId, randomUUID())
await awaitOperation(brokenOperation.id, "partial"); assert.equal(ecmpWrites, 6)
await applyPlan(ecmpPrincipal, brokenEcmp.planId, randomUUID()); await delay(30); assert.equal(ecmpWrites, 6)
await awaitOperation(brokenOperation.id, "partial"); assert.equal(ecmpWrites, 5)
await applyPlan(ecmpPrincipal, brokenEcmp.planId, randomUUID()); await delay(30); assert.equal(ecmpWrites, 5)
loseEcmpGateway = false; fixtureEcmpRoutes = structuredClone(ecmpBaseline)
const tableProtection = randomUUID()
await pool.query("INSERT INTO mcp_protections(id,object_ref,created_by) VALUES($1,$2::jsonb,'integration')", [tableProtection, JSON.stringify({ kind: "routing_table", id: "*main", serverId: 1, protection: "deny" })])
@@ -2786,7 +2800,13 @@ try {
assert.equal(state, "applied"); assert.equal(writes, 1); assert.equal(settings["rp-filter"], "loose")
assert.equal((await pool.query("SELECT state FROM mcp_operation_steps WHERE operation_id=$1", [operation.id])).rows[0].state, "verified")
console.log("✓ Human approval, write-ahead intent, single idempotent write and read-back verification")
const stale = await invokeMcpTool(principal, "mm_ip_settings_update_plan", { serverId: 1, fields: { "rp-filter": "strict" } }) as { planId: string }
const { detectMcpDrift } = await import("./workers.js")
const driftCount = async () => (await pool.query("SELECT count(*)::int AS n FROM mcp_drift WHERE operation_id=$1", [operation.id])).rows[0].n
const readsBeforeMatchingDrift = reconcileSettingsReads
await detectMcpDrift()
assert(reconcileSettingsReads > readsBeforeMatchingDrift)
assert.equal(await driftCount(), 0)
const stale = await invokeMcpTool(principal, "mm_ip_settings_update_plan", { serverId: 1, fields: { "rp-filter": "loose" } }) as { planId: string }
await approvePlan(stale.planId, actor, true)
settings["rp-filter"] = "no"
const conflict = await applyPlan(principal, stale.planId, "conflict-key")
@@ -2797,6 +2817,14 @@ try {
}
assert.equal(state, "rejected"); assert.equal(writes, 1)
console.log("✓ Changed snapshots reject execution before contacting the write endpoint")
const readsBeforeConfirmedDrift = reconcileSettingsReads
await detectMcpDrift()
assert(reconcileSettingsReads > readsBeforeConfirmedDrift)
assert.equal(await driftCount(), 1)
await detectMcpDrift()
assert.equal(await driftCount(), 1)
assert.equal(writes, 1)
console.log("✓ Drift detection reads the bound RouterOS source, records one confirmed incident and never replays writes")
const expiring = await invokeMcpTool(principal, "mm_ip_settings_update_plan", { serverId: 1, fields: { "rp-filter": "loose" } }) as { planId: string }
const sealedEvidence = (await pool.query("SELECT evidence FROM mcp_plans WHERE id=$1", [expiring.planId])).rows[0].evidence
const expiringEvidence = JSON.parse(await decryptSecret(sealedEvidence.sealed))
@@ -2855,6 +2883,7 @@ try {
process.env.MCP_PUBLIC_URL = endpoint
app = await buildApp({ startScheduler: false })
await app.listen({ port: appPort, host: "127.0.0.1" })
await (await import("../../test/users-profile-permissions.js")).verifyUsersProfilePermissions(app)
await (await import("../../test/mcp-backup-download.js")).verifyMcpBackupDownload(actor,app)
await (await import("../../test/mcp-database-backup.js")).verifyMcpDatabaseBackup(actor,app)
const fixtureScheduler = await import("../../services/scheduler-running.js")
@@ -2935,7 +2964,9 @@ try {
} finally { release(); await following.catch(() => undefined) }
} finally { pool.query = original; await follower.close() }
}
await (await import("./logs-follow-sdk-cancellation-acceptance.js")).verifyLogsFollowSdkCancellation(app, endpoint, actor)
const logsSdkAcceptance = await import("./logs-follow-sdk-cancellation-acceptance.js")
await logsSdkAcceptance.verifyLogsFollowSdkCancellation(app, endpoint, actor)
await logsSdkAcceptance.verifyLogsScopedSdkReads(app, endpoint, actor)
const stdio = new Client({ name: "stdio-integration", version: "1.0.0" })
try {
await stdio.connect(new StdioClientTransport({ command: process.execPath, args: ["--import", "tsx", fileURLToPath(new URL("../../scripts/mcp-stdio.ts", import.meta.url))], env: { MCP_URL: endpoint, MCP_TOKEN: rotated.token }, stderr: "pipe" }))
@@ -3035,12 +3066,6 @@ try {
assert.equal((await tokenRequest(refreshPayload)).statusCode, 400)
await assert.rejects(authenticateMcpToken(refreshed.json().access_token))
console.log("✓ OAuth consent, PKCE, resource binding, one-use code, refresh rotation and reuse-family revocation")
const { detectMcpDrift } = await import("./workers.js")
await detectMcpDrift()
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_drift WHERE operation_id=$1", [operation.id])).rows[0].n, 1)
await detectMcpDrift()
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_drift WHERE operation_id=$1", [operation.id])).rows[0].n, 1)
console.log("✓ Drift detection records a single open incident without replaying writes")
const currentPrincipal = await authenticateMcpToken(rotated.token)
await assert.rejects(invokeMcpTool(currentPrincipal, "mm_operation_recovery_plan", { operationId: operation.id }), error => error instanceof Error && "code" in error && error.code === "STATE_CONFLICT")
const beforeRecovery = settings["rp-filter"]
@@ -1,13 +1,121 @@
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { createHash, randomUUID } from "node:crypto"
import type { IncomingMessage, ServerResponse } from "node:http"
import { setTimeout as delay } from "node:timers/promises"
import type { FastifyInstance } from "fastify"
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"
import { SignJWT } from "jose"
import type { LogEntry } from "@mmapp/contracts"
import { mcpConnectionCreateSchema } from "@mmapp/contracts/mcp"
import { pool } from "../../db/index.js"
import { env } from "../../config.js"
import type { AuthUser } from "../../lib/permissions.js"
import { createMcpConnection } from "./store.js"
import { createMcpConnection, setConnectionStatus } from "./store.js"
export async function verifyLogsScopedSdkReads(app: FastifyInstance, endpoint: string, actor: AuthUser) {
const historicalId = 93, hiddenId = 94, marker = `mcp-scoped-logs-${randomUUID()}`
const from = "2001-01-01T00:00:00Z", to = "2001-01-01T01:00:00Z"
const client = new Client({ name: "logs-scoped-sdk-reads", version: "1.0.0" })
try {
await pool.query("INSERT INTO servers(id,name,host,enabled,username,password) VALUES($1,$2,'127.0.0.1',false,'fixture','fixture')", [historicalId, marker])
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({
name: marker, mode: "observer", permissions: ["mm:servers:read"],
expiresAt: new Date(Math.min(actor.expiresAt ?? Infinity, Date.now() + 600_000)).toISOString(),
scope: { serverIds: [1, historicalId], operationIds: ["logs.get", "logs.context", "logs.summary", "logs.export"], allowGlobal: false },
}), actor)
const { rows } = await pool.query<{ id: string; serverId: number; index: string }>(`
INSERT INTO router_log_entries(server_id,server_name,host,router_id,raw_time,received_at,effective_at,topics,level,message,generation)
SELECT server_id,$1,'127.0.0.1',n::text,'00:30:00',now(),$2::timestamptz+(30+n)*interval '1 minute',ARRAY['system'],'info',
CASE WHEN server_id=$4 THEN $1 || ' hidden-row' ELSE $1 END,0
FROM (SELECT 1 AS server_id,n FROM generate_series(-12,12) AS n UNION ALL SELECT $3,0 UNION ALL SELECT $4,0) AS fixture
RETURNING id::text,server_id AS "serverId",router_id AS "index"`, [marker, from, historicalId, hiddenId])
await pool.query("INSERT INTO router_log_collection_state(server_id,server_name,host) SELECT id,$1,'127.0.0.1' FROM unnest($2::int[]) AS id", [marker, [historicalId, hiddenId]])
assert.equal((await pool.query("DELETE FROM servers WHERE id=$1 AND name=$2", [historicalId, marker])).rowCount, 1)
await client.connect(new StreamableHTTPClientTransport(new URL(endpoint), {
requestInit: { headers: { Authorization: `Bearer ${access.token}` } },
}))
const idAt = (serverId: number, index: number) => {
const row = rows.find(item => item.serverId === serverId && Number(item.index) === index)
assert.ok(row)
return row.id
}
const call = (name: string, args: Record<string, unknown>) => client.callTool({ name, arguments: args })
const resultOf = async <T>(name: string, args: Record<string, unknown>) => {
const response = await call(name, args)
assert.ok(!response.isError)
return (response.structuredContent as { result: T }).result
}
const id = idAt(1, 0)
assert.equal((await resultOf<LogEntry>("mm_logs_get", { serverId: 1, id })).id, id)
type LogContext = { entry: LogEntry; before: LogEntry[]; after: LogEntry[] }
const context = await resultOf<LogContext>("mm_logs_context_get", { serverId: 1, id })
assert.equal(context.entry.id, id)
assert.deepEqual(context.before.map(row => row.id), Array.from({ length: 10 }, (_, i) => idAt(1, i - 10)))
assert.deepEqual(context.after.map(row => row.id), Array.from({ length: 10 }, (_, i) => idAt(1, i + 1)))
const historical = { serverId: historicalId, id: idAt(historicalId, 0) }
assert.equal((await resultOf<LogEntry>("mm_logs_get", historical)).serverId, historicalId)
const historicalContext = await resultOf<LogContext>("mm_logs_context_get", historical)
assert.equal(historicalContext.entry.id, historical.id)
assert.deepEqual([...historicalContext.before, ...historicalContext.after], [])
for (const name of ["mm_logs_get", "mm_logs_context_get"]) {
const foreignId = idAt(hiddenId, 0)
const foreign = await call(name, { serverId: 1, id: foreignId })
assert.equal(foreign.isError, true)
assert.match(JSON.stringify(foreign), /NOT_FOUND/)
assert.doesNotMatch(JSON.stringify(foreign), new RegExp(marker))
const outOfScope = await call(name, { serverId: hiddenId, id: foreignId })
assert.equal(outOfScope.isError, true)
assert.match(JSON.stringify(outOfScope), /SERVER_DENIED/)
}
const summary = await resultOf<{ total: number; levels: { info: number }; states: { serverId: number }[] }>(
"mm_logs_summary_get", { from, to, q: marker },
)
assert.equal(summary.total, 26)
assert.equal(summary.levels.info, 26)
assert.ok(summary.states.some(state => state.serverId === historicalId))
assert.ok(summary.states.every(state => state.serverId === 1 || state.serverId === historicalId))
assert.ok(!("settings" in summary))
console.log("✓ MCP logs SDK: scoped entries, same-server context and historical summary")
const portalJwt = (subject: string, permissions: string[]) =>
new SignJWT({ sub: subject, apps: ["mm"], permissions })
.setProtectedHeader({ alg: "HS256" }).setIssuer(env.authIssuer).setExpirationTime("5m")
.sign(new TextEncoder().encode(env.jwtSecret))
const ownerToken = await portalJwt(actor.id, actor.permissions)
const otherToken = await portalJwt(`${actor.id}-other`, ["mm:mcp:read", "mm:servers:read"])
const restrictedToken = await portalJwt(actor.id, ["mm:mcp:read"])
type Export = { artifactId: string; downloadPath: string; expiresInSeconds: number; rows: number; sha256: string }
const exportLogs = (format: "csv" | "json") => resultOf<Export>("mm_logs_export", { from, to, q: marker, format })
const download = (url: string, token?: string) =>
app.inject({ url, headers: token ? { authorization: `Bearer ${token}` } : {} })
const csv = await exportLogs("csv")
assert.equal(csv.rows, 26)
assert.equal(csv.expiresInSeconds, 900)
assert.equal(csv.downloadPath, `/api/mcp/artifacts/${csv.artifactId}`)
const response = await download(csv.downloadPath, ownerToken)
assert.equal(response.statusCode, 200)
assert.equal(response.headers["cache-control"], "private, no-store")
assert.match(String(response.headers["content-type"]), /^text\/csv/)
assert.equal(createHash("sha256").update(response.rawPayload).digest("hex"), csv.sha256)
assert.ok(!response.body.includes("hidden-row"))
assert.equal((await download(csv.downloadPath)).statusCode, 401)
assert.equal((await download(csv.downloadPath, access.token)).statusCode, 401)
assert.equal((await download(csv.downloadPath, otherToken)).statusCode, 404)
assert.equal((await download(csv.downloadPath, restrictedToken)).statusCode, 403)
const json = await exportLogs("json")
await pool.query("UPDATE mcp_artifacts SET expires_at=now()-interval '1 second' WHERE id=$1 AND connection_id=$2", [json.artifactId, access.connection.id])
assert.equal((await download(json.downloadPath, ownerToken)).statusCode, 404)
await setConnectionStatus(access.connection.id, "revoked", actor)
assert.equal((await download(csv.downloadPath, ownerToken)).statusCode, 403)
console.log("✓ MCP logs export: CSV owner download, scope, non-cacheable response, expiry and revocation")
} finally {
await client.close()
await pool.query("DELETE FROM router_log_entries WHERE message=ANY($1::text[])", [[marker, `${marker} hidden-row`]])
await pool.query("DELETE FROM router_log_collection_state WHERE server_id=ANY($1::int[]) AND server_name=$2", [[historicalId, hiddenId], marker])
await pool.query("DELETE FROM servers WHERE id=$1 AND name=$2", [historicalId, marker])
}
}
export async function verifyLogsFollowSdkCancellation(app: FastifyInstance, endpoint: string, actor: AuthUser) {
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({
@@ -6,6 +6,7 @@ import { mcpConnectionCreateSchema } from "@mmapp/contracts/mcp"
import { pool } from "../../db/index.js"
import type { AuthUser } from "../../lib/permissions.js"
import { managedComment } from "../../managed-markers.js"
import { updateServer } from "../servers/service/servers-service.js"
import type { ChangeEvidence } from "./action-types.js"
import { findAction } from "./actions.js"
import { readRosSnapshot } from "./resources.js"
@@ -18,13 +19,16 @@ import { authenticateMcpToken, createMcpConnection, setConnectionStatus } from "
export async function acceptNativeConservation(actor: AuthUser) {
const serverId = 198
const filterPath = "/ip/firewall/filter", rulePath = "/routing/rule"
const routePaths = ["/ip/route", "/ipv6/route"], settingsPaths = ["/ip/settings", "/ipv6/settings"]
const base = () => ["a", "b", "c"].map(id => ({ ".id": `*${id}`, chain: "fixture", action: "accept", comment: id, disabled: "true" }))
const ruleBase = () => ["a", "b", "c"].map(id => ({ ".id": `*${id}`, action: "lookup", table: "main", comment: id, disabled: "true" }))
let catalogs: Record<string, Record<string, string>[]> = {}, mode = "normal", writes = 0, next = 0, polls = 0
let connectionId: string | undefined
let afterIntent: (() => Promise<void>) | undefined, afterVerified: (() => Promise<void>) | undefined
let afterRouteRead: (() => Promise<void>) | undefined
const query = pool.query.bind(pool)
const commands: Array<{ method: string; path: string }> = []
const readPaths = new Set<string>()
const reset = () => {
mode = "normal"
catalogs = { [filterPath]: base(), [rulePath]: ruleBase(), "/routing/table": [{ ".id": "*main", name: "main", fib: "true", disabled: "false" }], "/ip/settings": [{ "rp-filter": "no", "ipv4-multipath-hash-policy": "l3" }], "/ip/route": [] }
@@ -33,8 +37,18 @@ export async function acceptNativeConservation(actor: AuthUser) {
const router = createServer(async (req, res) => {
const path = decodeURIComponent((req.url ?? "").replace(/^\/rest/, ""))
res.setHeader("content-type", "application/json")
if (req.method === "GET") { polls++; res.end(JSON.stringify(catalogs[path] ?? [])); return }
const parent = [filterPath, rulePath, "/ip/route", "/ip/settings"].find(value => path === value || path.startsWith(`${value}/`))
if (req.method === "GET") {
polls++; readPaths.add(path)
const body = JSON.stringify(catalogs[path] ?? [])
if (path === "/ip/route" && afterRouteRead) {
const hook = afterRouteRead
afterRouteRead = undefined
await hook()
}
res.end(body)
return
}
const parent = [filterPath, rulePath, ...routePaths, ...settingsPaths].find(value => path === value || path.startsWith(`${value}/`))
if (!parent || !["PUT", "PATCH", "DELETE"].includes(req.method ?? "")) { res.statusCode = 404; res.end("{}"); return }
let text = ""; for await (const chunk of req) text += chunk
const fields = text ? JSON.parse(text) as Record<string, string> : {}
@@ -42,11 +56,11 @@ export async function acceptNativeConservation(actor: AuthUser) {
writes++; commands.push({ method: req.method!, path })
if (mode !== "ignored") {
if (req.method === "PUT") rows.push({ ".id": id, ...fields })
if (req.method === "PATCH") Object.assign(parent === "/ip/settings" ? rows[0] : rows.find(row => row[".id"] === id)!, fields)
if (req.method === "PATCH") Object.assign(settingsPaths.includes(parent) ? rows[0] : rows.find(row => row[".id"] === id)!, fields)
if (req.method === "DELETE") catalogs[parent] = rows.filter(row => row[".id"] !== id)
}
const after = catalogs[parent]
if (parent === "/ip/route") for (const row of after) row.active = Number(row.distance) === Math.min(...after.map(item => Number(item.distance))) ? "true" : "false"
if (routePaths.includes(parent)) for (const row of after) row.active = Number(row.distance) === Math.min(...after.map(item => Number(item.distance))) ? "true" : "false"
if (mode === "two-new") after.push({ ".id": `*extra${next}`, ...fields })
if (mode === "collateral-update") after.find(row => row[".id"] === "*b")!.comment = "external"
if (mode === "collateral-remove") catalogs[parent] = after.filter(row => row[".id"] !== "*b")
@@ -80,7 +94,7 @@ export async function acceptNativeConservation(actor: AuthUser) {
try {
assert.equal((await query("SELECT count(*)::int AS n FROM servers WHERE id=$1", [serverId])).rows[0].n, 0)
await query("INSERT INTO servers(id,name,host,port,use_ssl,password) VALUES($1,'native-conservation','127.0.0.1',$2,false,'fixture-only')", [serverId, (router.address() as { port: number }).port])
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Native conservation", mode: "approval", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read", "mm:network:write"], scope: { serverIds: [serverId], operationIds: ["ros.ip_filter.create", "ros.ip_filter.update", "ros.ip_filter.delete", "ros.routing_rule.update", "ros.ipv4_route.update", "ros.ip_settings.update", "routing.ecmp.update", "routing.rpf.update", "operations.recover", "operations.reconcile"], allowGlobal: false }, limits: { callsPerMinute: 1000, mutationsPerHour: 1000 } }), actor)
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Native conservation", mode: "approval", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read", "mm:network:write"], scope: { serverIds: [serverId], operationIds: ["ros.ip_filter.create", "ros.ip_filter.update", "ros.ip_filter.delete", "ros.routing_rule.update", "ros.ipv4_route.update", "ros.ip_settings.update", "routing.ecmp.update", "routing.rpf.update", "operations.recover", "operations.reconcile", "operations.read"], allowGlobal: false }, limits: { callsPerMinute: 1000, mutationsPerHour: 1000 } }), actor)
connectionId = access.connection.id
let principal = await authenticateMcpToken(access.token)
const prepare = (action: "create" | "update" | "delete", resource = "ip_filter") => invokeMcpTool(principal, `mm_${resource}_${action}_plan`, { serverId, ...(action === "create" ? {} : { id: "*a" }), fields: action === "create" ? { chain: "fixture", action: "accept", comment: "new", disabled: true } : action === "update" ? { comment: "changed" } : {} }) as Promise<{ planId: string }>
@@ -204,13 +218,233 @@ export async function acceptNativeConservation(actor: AuthUser) {
const routeBaseline = [0, 1].map(index => ({ ".id": `*route${index}`, "dst-address": "192.0.2.0/24", "routing-table": "main", gateway: `198.18.0.${index + 1}`, "immediate-gw": `198.18.0.${index + 1}%fixture`, scope: "0", distance: String((index + 1) * 10), active: index === 0 ? "true" : "false", disabled: "false", dynamic: "false", comment: managedComment("conservation") }))
catalogs["/ip/route"] = structuredClone(routeBaseline)
const originalSettings = structuredClone(catalogs["/ip/settings"])
const equal = await invokeMcpTool(principal, "mm_ecmp_plan", { serverId, family: "ipv4", routeIds: ["*route0", "*route1"], distance: 10, hashPolicy: "l4" }) as { planId: string }
const ecmpArgs = { serverId, family: "ipv4", routeIds: ["*route0", "*route1"], distance: 10, hashPolicy: "l4" }
const beforeEcmp = writes
const observer = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "ECMP interface-only preflight", mode: "observer", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read"], scope: { serverIds: [serverId], operationIds: ["routing.ecmp.preflight.read"], allowGlobal: false } }), actor)
try {
const reader = await authenticateMcpToken(observer.token)
const { distance, hashPolicy, ...preflightInput } = ecmpArgs
catalogs["/ip/firewall/connection/tracking"] = [{ enabled: "auto" }]
catalogs["/ip/route"] = routeBaseline.map((row, index) => ({ ...row, gateway: `gre${index}`, "immediate-gw": `gre${index}` }))
catalogs["/interface"] = [0, 1].map(index => ({ ".id": `*gre${index}`, name: `gre${index}` }))
const before = polls
const result = await invokeMcpTool(reader, "mm_ecmp_preflight_get", preflightInput) as { configurationObserved: boolean; networkValidation: string; sources: { interfaces: { count: number } } }
assert.equal(result.configurationObserved, true)
assert.equal(result.networkValidation, "not-performed")
assert.equal(result.sources.interfaces.count, 2)
assert.equal(polls - before, 11)
assert.equal(writes, beforeEcmp)
} finally {
await query("DELETE FROM mcp_connections WHERE id=$1", [observer.connection.id])
catalogs["/ip/route"] = structuredClone(routeBaseline)
}
catalogs["/ip/route"] = routeBaseline.map((row, index) => ({ ...row, gateway: `gre${index}`, "immediate-gw": `gre${index}` }))
const namedHopPlan = await invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs) as { planId: string; changes: unknown }
assert.ok(!JSON.stringify(namedHopPlan.changes).includes("nextHopInterface"))
const inventory = structuredClone(catalogs["/interface"])
afterIntent = async () => { catalogs["/interface"][0][".id"] = "*replacement" }
const replacedHop = await apply(namedHopPlan.planId)
assert.equal(afterIntent, undefined)
assert.equal(replacedHop.state, "rejected")
assert.equal(replacedHop.error_code, "STATE_CONFLICT")
assert.equal(writes, beforeEcmp)
catalogs["/interface"] = inventory
catalogs["/ip/route"] = structuredClone(routeBaseline)
console.log("ECMP interface-only next-hop: same-name replacement after intent refused before PATCH; native identity stays private")
afterRouteRead = async () => { catalogs["/ip/route"][1].distance = "25" }
await assert.rejects(invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs), { code: "STATE_CONFLICT" })
assert.equal(afterRouteRead, undefined)
catalogs["/ip/route"] = structuredClone(routeBaseline)
afterRouteRead = async () => {
await query("UPDATE servers SET password='source-aba-fixture' WHERE id=$1", [serverId])
await query("UPDATE servers SET password='fixture-only' WHERE id=$1", [serverId])
}
await assert.rejects(invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs), { code: "STATE_CONFLICT" })
assert.equal(afterRouteRead, undefined)
assert.equal(writes, beforeEcmp)
catalogs["/ip/route"][1]["immediate-gw"] = routeBaseline[0]["immediate-gw"]
await assert.rejects(invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs), { code: "STATE_CONFLICT" })
assert.equal(writes, beforeEcmp)
catalogs["/ip/route"] = structuredClone(routeBaseline)
const fibTable = catalogs["/routing/table"]
for (const rows of [
[{ ...fibTable[0], fib: "false" }],
[{ ...fibTable[0], fib: "" }],
[{ ".id": "*main", name: "main" }],
[{ ...fibTable[0], disabled: "true" }],
[{ ...fibTable[0], invalid: "true" }],
[...fibTable, { ...fibTable[0], ".id": "*duplicate" }],
]) {
catalogs["/routing/table"] = rows
await assert.rejects(invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs), { code: "STATE_CONFLICT" })
assert.equal(writes, beforeEcmp)
}
catalogs["/routing/table"] = fibTable
const staleFib = await invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs) as { planId: string }
await approvePlan(staleFib.planId, actor, true)
catalogs["/routing/table"] = [{ ...fibTable[0], fib: "false" }]
assert.equal((await wait((await applyPlan(principal, staleFib.planId, randomUUID())).id)).state, "rejected")
assert.equal(writes, beforeEcmp)
catalogs["/routing/table"] = fibTable
console.log("ECMP FIB SQL/HTTP: inactive, unconfirmed and ambiguous tables refused; drift rejected before native write")
const equal = await invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs) as { planId: string }
const beforeEqual: number = writes, equalResult = await apply(equal.planId)
assert.equal(equalResult.state, "applied"); assert.equal(writes, beforeEqual + 3)
assert.equal(catalogs["/ip/settings"][0]["ipv4-multipath-hash-policy"], "l4")
const unequal = await invokeMcpTool(principal, "mm_operation_recovery_plan", { operationId: equalResult.operation.id }) as { planId: string }
assert.equal((await apply(unequal.planId)).state, "applied"); assert.equal(writes, beforeEqual + 6)
assert.deepEqual(catalogs["/ip/route"], routeBaseline); assert.deepEqual(catalogs["/ip/settings"], originalSettings)
const lateEcmp = await invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs) as { planId: string }
afterIntent = async () => { catalogs["/ip/route"][1]["immediate-gw"] = "" }
const lateEcmpResult = await apply(lateEcmp.planId)
assert.equal(afterIntent, undefined)
assert.equal(lateEcmpResult.state, "rejected")
assert.equal(lateEcmpResult.error_code, "STATE_CONFLICT")
assert.equal(writes, beforeEqual + 6)
catalogs["/ip/route"] = structuredClone(routeBaseline)
const sourceChanged = await invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs) as { planId: string }
let lateRouteReads = 0
const rotateDuringHealthRead = async () => {
if (++lateRouteReads === 1) {
// The prefix reads the configuration first; the next GET checks the live next-hop.
afterRouteRead = rotateDuringHealthRead
return
}
await assert.rejects(updateServer(serverId, { password: "concurrent-update-fixture" }), { statusCode: 409 })
await query("UPDATE servers SET password='health-source-aba-fixture' WHERE id=$1", [serverId])
await query("UPDATE servers SET password='fixture-only' WHERE id=$1", [serverId])
}
afterIntent = async () => { afterRouteRead = rotateDuringHealthRead }
const staleHealthSource = await apply(sourceChanged.planId)
assert.equal(lateRouteReads, 2)
assert.equal(staleHealthSource.state, "rejected")
assert.equal(staleHealthSource.error_code, "STATE_CONFLICT")
assert.equal(writes, beforeEqual + 6)
const beforeNextStep = await invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs) as { planId: string }
afterVerified = async () => { catalogs["/ip/route"][1]["immediate-gw"] = "" }
const partialEcmp = await apply(beforeNextStep.planId)
assert.equal(partialEcmp.state, "partial")
assert.equal(writes, beforeEqual + 7)
for (const [result, confirmedWrites] of [[lateEcmpResult, 0], [partialEcmp, 1]] as const) {
const stored = await invokeMcpTool(principal, "mm_operation_get", { operationId: result.operation.id }) as {
state: string; result: { failedStepDispatched: boolean; confirmedWrites: number; needsReview: boolean }
}
assert.equal(stored.state, result.state)
assert.equal(stored.result.failedStepDispatched, false)
assert.equal(stored.result.confirmedWrites, confirmedWrites)
assert.equal(stored.result.needsReview, confirmedWrites > 0)
const { rows: [stop] } = await pool.query("SELECT payload FROM mcp_audit WHERE action='operation.stop' AND payload->>'operationId'=$1", [result.operation.id])
assert.equal(stop.payload.failedStepDispatched, false)
}
catalogs["/ip/route"] = structuredClone(routeBaseline)
const collapsed = await invokeMcpTool(principal, "mm_ecmp_plan", { ...ecmpArgs, hashPolicy: undefined }) as { planId: string }
afterVerified = async () => {
afterVerified = async () => { catalogs["/ip/route"][1]["immediate-gw"] = routeBaseline[0]["immediate-gw"] }
}
const collapsedResult = await apply(collapsed.planId)
assert.equal(collapsedResult.state, "partial")
assert.equal(collapsedResult.verification, "failed")
assert.equal(writes, beforeEqual + 9)
assert.equal(afterVerified, undefined)
catalogs["/ip/route"] = structuredClone(routeBaseline)
console.log("ECMP SQL/HTTP: shared adjacency refuses preparation and post-write collapse fails verification")
const v6Baseline = routeBaseline.map((row, index) => ({
...row, "dst-address": "2001:db8::/64", gateway: `2001:db8:1::${index + 1}`,
"immediate-gw": `fe80::${index + 1}%fixture`,
}))
catalogs["/ipv6/route"] = structuredClone(v6Baseline)
const v6Settings = [{ "multipath-hash-policy": "l3" }]
catalogs["/ipv6/settings"] = structuredClone(v6Settings)
const v6Mangle = "/ipv6/firewall/mangle"
catalogs[v6Mangle] = [{ ".id": "*v6mark", action: "mark-routing", disabled: "false", "new-routing-mark": "NEVER_NATIVE_POLICY_MARK", bytes: "0", packets: "0" }]
const v6Args = { ...ecmpArgs, family: "ipv6" }
readPaths.clear()
for (const [path, field] of [[v6Mangle, "new-routing-mark"], [rulePath, "min-prefix"]]) {
const stale = await invokeMcpTool(principal, "mm_ecmp_plan", v6Args) as { planId: string; changes: { snapshots: Record<string, unknown>[] } }
const policy = stale.changes.snapshots.find(snapshot => snapshot.resource === "routing_policy:ipv6_mangle")
assert.deepEqual(policy, { serverId, resource: "routing_policy:ipv6_mangle", rowCount: 1 })
assert.ok(!JSON.stringify(stale).includes("NEVER_NATIVE_POLICY_MARK"))
await approvePlan(stale.planId, actor, true)
const original = catalogs[path], before = writes
catalogs[path] = [{ ...original[0], [field]: "17" }, ...original.slice(1)]
assert.equal((await wait((await applyPlan(principal, stale.planId, randomUUID())).id)).state, "rejected")
assert.equal(writes, before)
catalogs[path] = original
}
const v6Plan = await invokeMcpTool(principal, "mm_ecmp_plan", v6Args) as { planId: string }
catalogs[v6Mangle][0].bytes = "1500"
catalogs[v6Mangle][0].packets = "1"
const v6Result = await apply(v6Plan.planId)
assert.equal(v6Result.state, "applied")
assert.ok(catalogs["/ipv6/route"].every(row => row.active === "true" && row.distance === "10"))
assert.equal(catalogs["/ipv6/settings"][0]["multipath-hash-policy"], "l4")
const inverseArgs = { operationId: v6Result.operation.id }
catalogs[v6Mangle][0]["new-routing-mark"] = "external-mark"
await assert.rejects(invokeMcpTool(principal, "mm_operation_recovery_plan", inverseArgs), { code: "STATE_CONFLICT" })
catalogs[v6Mangle][0]["new-routing-mark"] = "NEVER_NATIVE_POLICY_MARK"
const v6Inverse = await invokeMcpTool(principal, "mm_operation_recovery_plan", inverseArgs) as { planId: string }
assert.ok(!JSON.stringify(v6Inverse).includes("NEVER_NATIVE_POLICY_MARK"))
assert.equal((await apply(v6Inverse.planId)).state, "applied")
assert.deepEqual(catalogs["/ipv6/route"], v6Baseline)
assert.deepEqual(catalogs["/ipv6/settings"], v6Settings)
assert.ok(![...readPaths].some(path => path === "/ip/settings" || path.startsWith("/ip/firewall/")))
console.log("ECMP IPv6 SQL/HTTP: family-specific reads, policy drift refusal, route/hash apply and inverse")
const rpfArgs = { serverId, mode: "strict", acknowledgeAsymmetry: true }
const manglePath = "/ip/firewall/mangle"
const beforeRpf = writes, beforeIpv6 = polls
await assert.rejects(invokeMcpTool(principal, "mm_rpf_plan", { ...rpfArgs, family: "ipv6" }), { code: "UNSUPPORTED_CAPABILITY" })
assert.equal(polls, beforeIpv6)
catalogs[rulePath][0].disabled = "false"
await assert.rejects(invokeMcpTool(principal, "mm_rpf_plan", rpfArgs), { code: "STATE_CONFLICT" })
const strictSettings = { serverId, fields: { "rp-filter": "strict", "ipv4-multipath-hash-policy": "l4" } }
await assert.rejects(invokeMcpTool(principal, "mm_ip_settings_update_plan", strictSettings), { code: "STATE_CONFLICT" })
catalogs[rulePath][0].disabled = "true"
catalogs[manglePath] = [{ ".id": "*mark", chain: "prerouting", action: "mark-routing", disabled: "false", "new-routing-mark": "fixture-mark" }]
await assert.rejects(invokeMcpTool(principal, "mm_rpf_plan", rpfArgs), { code: "STATE_CONFLICT" })
const loose = await invokeMcpTool(principal, "mm_rpf_plan", { ...rpfArgs, mode: "loose" }) as { changes: { warnings: string[] } }
assert.ok(loose.changes.warnings.some(warning => warning.includes("policy routing")))
catalogs[manglePath][0].disabled = "true"
const strict = await invokeMcpTool(principal, "mm_rpf_plan", rpfArgs) as { planId: string }
const genericStrict = await invokeMcpTool(principal, "mm_ip_settings_update_plan", strictSettings) as { planId: string }
await approvePlan(strict.planId, actor, true)
await approvePlan(genericStrict.planId, actor, true)
catalogs[manglePath][0]["new-routing-mark"] = "changed-after-approval"
const staleRpf = await applyPlan(principal, strict.planId, randomUUID())
assert.equal((await wait(staleRpf.id)).state, "rejected")
assert.equal((await wait((await applyPlan(principal, genericStrict.planId, randomUUID())).id)).state, "rejected")
assert.equal(writes, beforeRpf)
catalogs[manglePath] = []
catalogs["/ip/settings"][0]["rp-filter"] = "loose"
const disableRpf = await invokeMcpTool(principal, "mm_rpf_plan", { ...rpfArgs, mode: "disabled" }) as { planId: string }
assert.equal((await apply(disableRpf.planId)).state, "applied")
assert.deepEqual(catalogs["/ip/settings"], originalSettings)
catalogs["/ip/settings"][0]["rp-filter"] = "strict"
const leaveStrict = await invokeMcpTool(principal, "mm_rpf_plan", { ...rpfArgs, mode: "no" }) as { planId: string }
const leftStrict = await apply(leaveStrict.planId)
assert.equal(leftStrict.state, "applied")
const inverseRpfArgs = { operationId: leftStrict.operation.id }
const beforeInverseRpf = writes
catalogs[manglePath] = [{ ".id": "*late-mark", action: "mark-routing", disabled: "false" }]
await assert.rejects(invokeMcpTool(principal, "mm_operation_recovery_plan", inverseRpfArgs), { code: "STATE_CONFLICT" })
catalogs[manglePath] = []
const legacyRpf = await invokeMcpTool(principal, "mm_operation_recovery_plan", inverseRpfArgs) as { planId: string }
const legacyRpfEvidence = await evidence(legacyRpf.planId)
legacyRpfEvidence.snapshots = legacyRpfEvidence.snapshots.filter(snapshot => !snapshot.resource.startsWith("routing_policy:"))
await replace(legacyRpf.planId, legacyRpfEvidence)
await approvePlan(legacyRpf.planId, actor, true)
await assert.rejects(applyPlan(principal, legacyRpf.planId, randomUUID()), { code: "PLAN_EXPIRED" })
const staleInverseRpf = await invokeMcpTool(principal, "mm_operation_recovery_plan", inverseRpfArgs) as { planId: string }
await approvePlan(staleInverseRpf.planId, actor, true)
catalogs[manglePath] = [{ ".id": "*late-mark", action: "mark-routing", disabled: "false" }]
assert.equal((await wait((await applyPlan(principal, staleInverseRpf.planId, randomUUID())).id)).state, "rejected")
assert.equal(writes, beforeInverseRpf)
catalogs[manglePath] = []
const safeInverseRpf = await invokeMcpTool(principal, "mm_operation_recovery_plan", inverseRpfArgs) as { planId: string }
assert.equal((await apply(safeInverseRpf.planId)).state, "applied")
assert.equal(catalogs["/ip/settings"][0]["rp-filter"], "strict")
catalogs["/ip/settings"] = structuredClone(originalSettings)
console.log("RPF inverse SQL/HTTP: active policy, legacy plans and post-approval drift refused; exact strict restoration under verified policy")
console.log("RPF SQL/HTTP: explicit IPv6 refusal, strict policy admission and drift, loose warning, disabled alias")
for (const [tool, args, path] of [
["mm_rpf_plan", { serverId, mode: "loose", acknowledgeAsymmetry: true }, "/ip/settings"],
["mm_ip_settings_update_plan", { serverId, fields: { "rp-filter": "loose" } }, "/ip/settings"],
@@ -229,6 +463,43 @@ export async function acceptNativeConservation(actor: AuthUser) {
const booleanResult = await apply(booleanPlan.planId); assert.equal(booleanResult.state, "applied")
const booleanInverse = await invokeMcpTool(principal, "mm_operation_recovery_plan", { operationId: booleanResult.operation.id }) as { planId: string }
assert.equal((await apply(booleanInverse.planId)).state, "applied"); assert.deepEqual(catalogs[filterPath], booleanBaseline)
catalogs["/ip/route"] = routeBaseline.map((row, index) => ({ ...row, gateway: `gre${index}`, "immediate-gw": `gre${index}` }))
const stableHopPlan = await invokeMcpTool(principal, "mm_ecmp_plan", { ...ecmpArgs, hashPolicy: undefined }) as { planId: string }
const beforeStableHops = writes
const stableHops = await apply(stableHopPlan.planId)
assert.equal(stableHops.state, "applied")
assert.equal(writes, beforeStableHops + 2)
assert.ok(catalogs["/ip/route"].every(row => row.distance === "10" && row.active === "true"))
console.log("ECMP interface-only next-hop: unchanged interface IDs verified before both writes and after apply")
const namedHopInverseArgs = { operationId: stableHops.operation.id }
const plansBeforeReplacement = (await query("SELECT count(*)::int AS count FROM mcp_plans WHERE connection_id=$1", [connectionId])).rows[0].count
catalogs["/interface"][0][".id"] = "*replaced-before-preparation"
await assert.rejects(invokeMcpTool(principal, "mm_operation_recovery_plan", namedHopInverseArgs), { code: "STATE_CONFLICT" })
assert.equal((await query("SELECT count(*)::int AS count FROM mcp_plans WHERE connection_id=$1", [connectionId])).rows[0].count, plansBeforeReplacement)
catalogs["/interface"][0][".id"] = "*gre0"
const oldHopInverse = await invokeMcpTool(principal, "mm_operation_recovery_plan", namedHopInverseArgs) as { planId: string }
const oldHopEvidence = await evidence(oldHopInverse.planId)
delete oldHopEvidence.healthChecks
await replace(oldHopInverse.planId, oldHopEvidence)
const beforeInverseHops = writes
const oldHopResult = await apply(oldHopInverse.planId)
assert.equal(oldHopResult.state, "rejected")
assert.equal(oldHopResult.error_code, "PLAN_EXPIRED")
assert.equal(writes, beforeInverseHops)
const staleHopInverse = await invokeMcpTool(principal, "mm_operation_recovery_plan", namedHopInverseArgs) as { planId: string; changes: unknown }
assert.ok(!JSON.stringify(staleHopInverse.changes).includes("nextHopInterface"))
afterIntent = async () => { catalogs["/interface"][0][".id"] = "*inverse-replacement" }
const refusedInverse = await apply(staleHopInverse.planId)
assert.equal(afterIntent, undefined)
assert.equal(refusedInverse.state, "rejected")
assert.equal(refusedInverse.error_code, "STATE_CONFLICT")
assert.equal(writes, beforeInverseHops)
catalogs["/interface"][0][".id"] = "*gre0"
const safeHopInverse = await invokeMcpTool(principal, "mm_operation_recovery_plan", namedHopInverseArgs) as { planId: string }
assert.equal((await apply(safeHopInverse.planId)).state, "applied")
assert.equal(writes, beforeInverseHops + 2)
assert.deepEqual(catalogs["/ip/route"].map(row => row.distance), ["10", "20"])
console.log("ECMP inverse: replaced interface refused before PATCH; intact interface restores unequal costs")
console.log("Native multi-catalog inverse SQL/HTTP: two route PATCHes plus singleton hash policy, exact reverse prefix, RPF/raw singleton, numeric zero, false and empty-comment originals preserved")
console.log("Native conservation SQL/HTTP: C/U/D and safe inverses; assigned-ID, membership, field and order failures never VERIFIED/replayed; honest two-step prefix stops before second intent; late source/dependency/protection/policy fence zero writes; stale operation revision refused. No CHR/opaque-field/forwarding claim.")
} finally {
@@ -32,8 +32,8 @@ assert.equal(rebuildReconcile.operation.effect, "read")
assert.equal(rebuildReconcile.operation.scope, "global")
assert.deepEqual(rebuildReconcile.operation.requiredPermissions, ["mm:traffic:write"])
assert.throws(() => rebuildReconcile.schema.parse({ jobId: "caller" }))
assert.equal(action.operation.version, 9)
assert.equal(findMcpOperation(action.operation.id)?.version, 9)
assert.equal(action.operation.version, 10)
assert.equal(findMcpOperation(action.operation.id)?.version, 10)
assert.throws(() => action.schema.parse({ serverId: 1, expectedServerProof: "forged-source" }))
assert.throws(() => action.schema.parse({ serverId: 1, version: 1 }))
assert.equal(action.schema.parse({ serverId: 1 }).family, "ipv4")
@@ -53,13 +53,13 @@ assert.equal(findAction("mm_interfaces_update_plan").operation.version, 5)
assert.equal(findAction("mm_ipsec_peer_delete_plan").operation.version, 3)
assert.equal(findAction("mm_ipsec_peer_update_plan").operation.version, 3)
assert.equal(findAction("mm_certificate_delete_plan").operation.version, 3)
for (const [tool, version] of [["mm_route_optimizer_plan", 7], ["mm_route_profile_save_plan", 4], ["mm_route_path_verify", 2], ["mm_operation_recovery_plan", 8], ["mm_ecmp_plan", 2], ["mm_rpf_plan", 2], ["mm_vrf_plan", 4], ["mm_firewall_rule_update_plan", 2]] as const) {
for (const [tool, version] of [["mm_route_optimizer_plan", 7], ["mm_route_profile_save_plan", 5], ["mm_route_path_verify", 2], ["mm_operation_recovery_plan", 8], ["mm_ecmp_plan", 7], ["mm_rpf_plan", 4], ["mm_ip_settings_update_plan", 3], ["mm_vrf_plan", 4], ["mm_firewall_rule_update_plan", 2]] as const) {
const routed = findAction(tool)
assert.equal(routed.operation.version, version)
assert.equal(findMcpOperation(routed.operation.id)?.version, version)
}
assert.throws(() => findAction("mm_routing_measurement_plan").schema.parse({ measurement: { srcServerId: 1, dstServerId: 2, srcInterface: "gre", dstInterface: "gre", family: "ipv4", expectedEndpoints: [] } }))
console.log("MCP operation revisions: OSPF v9 and template order revisions agree; clients cannot inject revision or private source proof")
console.log("MCP operation revisions: OSPF v10 and template order revisions agree; clients cannot inject revision or private source proof")
assert.equal(findAction("mm_ipsec_peer_create_plan").operation.version, 5)
assert.equal(findAction("mm_certificate_update_plan").operation.version, 4)
@@ -9,9 +9,10 @@ await prepareOwnedBandwidthState()
const {pool,closePool}=await import("../../db/index.js")
const {applySqlMigrations}=await import("../../db/migrate.js")
const {mcpConnectionCreateSchema}=await import("@mmapp/contracts/mcp")
const {createMcpConnection,authenticateMcpToken}=await import("./store.js")
const {createMcpConnection,authenticateMcpToken,getMcpSettings}=await import("./store.js")
const {invokeMcpTool,approvePlan,applyPlan}=await import("./runtime.js")
const {OrdinaryRetirementCommitUnknown,retireOrdinaryMcpOperations}=await import("./ordinary-operation-retirement.js")
const {PARTITION_WRITER_ADVISORY_KEY}=await import("../../services/partition-write-transaction.js")
const {tickMcpWorkers}=await import("./workers.js")
const {pauseSchedulerJobsAndWait,resumeSchedulerJobs}=await import("../../services/scheduler-running.js")
const {Client}=await import("pg")
@@ -20,9 +21,58 @@ const {touchGeoipRunMeta}=await import("../../services/geoip-settings.js")
const {touchBackupScheduleRunMeta}=await import("../../services/backup-service.js")
const errorIs=(code:string,status:number)=>(error:unknown)=>error instanceof McpError&&error.code===code&&error.statusCode===status
async function assertWorkerUsesCommittedRetention() {
const actorId=randomUUID()
const original=await getMcpSettings()
await pool.query("UPDATE mcp_settings SET requests_retention_days=1,audit_retention_days=30 WHERE id=1")
const settingsWriter=await pool.connect()
let tick:Promise<void>|undefined,settled=false,transactionOpen=false
try{
await pool.query(`INSERT INTO mcp_requests(id,actor_id,tool,status,created_at) VALUES
($1,$5,'protected','completed',now()-interval '2 days'),
($2,$5,'active','running',now()-interval '33 days'),
($3,$5,'recent','completed',now()-interval '1 hour'),
($4,$5,'expired','completed',now()-interval '33 days')`,
[randomUUID(),randomUUID(),randomUUID(),randomUUID(),actorId])
await pool.query(`INSERT INTO mcp_audit(actor_id,action,created_at) VALUES
($1,'protected',now()-interval '31 days'),($1,'recent',now()-interval '1 hour'),
($1,'expired',now()-interval '183 days')`,[actorId])
await settingsWriter.query("BEGIN")
transactionOpen=true
const {pid}=(await settingsWriter.query("SELECT pg_backend_pid() AS pid")).rows[0]
await settingsWriter.query("UPDATE mcp_settings SET requests_retention_days=30,audit_retention_days=180 WHERE id=1")
tick=tickMcpWorkers().finally(()=>{settled=true})
void tick.catch(()=>{})
let waiting=false
for(const deadline=performance.now()+3000;!settled&&!waiting&&performance.now()<deadline;){
waiting=(await pool.query(`SELECT EXISTS(SELECT 1 FROM pg_stat_activity
WHERE $1=ANY(pg_blocking_pids(pid)) AND query LIKE '%FROM mcp_settings WHERE id=1 FOR SHARE%') AS waiting`,[pid])).rows[0].waiting
if(!waiting)await delay(10)
}
assert(waiting,"retention worker must wait for the policy update before choosing candidates")
assert.deepEqual(await getMcpSettings(),{...original,requestsRetentionDays:1,auditRetentionDays:30})
await settingsWriter.query("COMMIT")
transactionOpen=false
await tick
assert.deepEqual(await getMcpSettings(),{...original,requestsRetentionDays:30,auditRetentionDays:180})
assert.deepEqual((await pool.query("SELECT tool,status FROM mcp_requests WHERE actor_id=$1 ORDER BY tool",[actorId])).rows,
[{tool:"active",status:"running"},{tool:"protected",status:"completed"},{tool:"recent",status:"completed"}])
assert.deepEqual((await pool.query("SELECT action FROM mcp_audit WHERE actor_id=$1 ORDER BY action",[actorId])).rows,
[{action:"protected"},{action:"recent"}])
}finally{
try{if(transactionOpen)await settingsWriter.query("ROLLBACK")}finally{settingsWriter.release()}
await tick?.catch(()=>{})
await pool.query("DELETE FROM mcp_requests WHERE actor_id=$1",[actorId])
await pool.query("DELETE FROM mcp_audit WHERE actor_id=$1",[actorId])
await pool.query("UPDATE mcp_settings SET requests_retention_days=$1,audit_retention_days=$2 WHERE id=1",[original.requestsRetentionDays,original.auditRetentionDays])
}
console.log("Owned MCP retention: worker waits for committed policy, preserves protected and running rows, and deletes expired rows")
}
try {
await applySqlMigrations(pool)
await pauseSchedulerJobsAndWait()
await assertWorkerUsesCommittedRetention()
await pool.query("UPDATE public.mcp_settings SET enabled=true WHERE id=1")
const serverId=Number((await pool.query("INSERT INTO servers(name,host,enabled) VALUES('owned logs retention','127.0.0.1',false) RETURNING id")).rows[0].id)
async function access(){
@@ -31,12 +81,18 @@ try {
issuedAt:Date.now()-1000,expiresAt:Date.now()+300000}
const issued=await createMcpConnection(mcpConnectionCreateSchema.parse({name:actor.name,mode:"approval",
permissions:actor.permissions,expiresAt:new Date(actor.expiresAt).toISOString(),
scope:{serverIds:[serverId],operationIds:["logs.settings.update","traffic.settings.update","uptime.settings.update",
scope:{serverIds:[serverId],operationIds:["logs.settings.read","logs.settings.update","traffic.settings.update","uptime.settings.update",
"servers.api-ping.settings.update","internet-path.settings.update","geoip.settings.update","backups.schedule.update","operations.read","changes.apply"],allowGlobal:true},
limits:{callsPerMinute:100,mutationsPerHour:20}}),actor)
return {actor,principal:await authenticateMcpToken(issued.token)}
}
const {actor,principal}=await access(),foreign=(await access()).principal
const initialLogs=await invokeMcpTool(principal,"mm_logs_settings_get",{})
assert.deepEqual(initialLogs,{enabled:false,intervalSec:10,retentionDays:14})
const shorterLogs=await invokeMcpTool(principal,"mm_logs_settings_update_plan",{settings:{retentionDays:7}}) as {state:string;changes:{warnings:string[]}}
assert.equal(shorterLogs.state,"awaiting-approval")
assert.match(shorterLogs.changes.warnings.join(" "),/с 14 до 7 суток.*полученные более 7 суток назад/)
assert.deepEqual(await invokeMcpTool(principal,"mm_logs_settings_get",{}),initialLogs)
async function operation(retentionDays:number,ageMs:number,tool="mm_logs_settings_update_plan",
settings:Record<string,unknown>={retentionDays}){
const plan=await invokeMcpTool(principal,tool,{settings}) as {planId:string;state:string}
@@ -79,6 +135,38 @@ try {
LEFT JOIN mcp_operation_steps s ON s.operation_id=i.id LEFT JOIN mcp_operation_checkpoints k ON k.operation_id=i.id
LEFT JOIN mcp_operation_replay_tombstones t ON t.operation_id=i.id
LEFT JOIN mcp_ordinary_retirement_claims c ON c.operation_id=i.id ORDER BY i.id`,[ids])).rows
const beforeFence=await projection()
const exclusive=await pool.connect()
let blockedAttempt:ReturnType<typeof retireOrdinaryMcpOperations>|undefined
try{
await exclusive.query("SELECT pg_advisory_lock($1::bigint)",[PARTITION_WRITER_ADVISORY_KEY])
await exclusive.query("BEGIN")
blockedAttempt=retireOrdinaryMcpOperations()
void blockedAttempt.catch(()=>{})
let waiting=false
for(const deadline=Date.now()+3000;!waiting&&Date.now()<deadline;){
waiting=(await exclusive.query(`SELECT EXISTS(SELECT 1 FROM pg_catalog.pg_locks
WHERE locktype='advisory' AND database=(SELECT oid FROM pg_catalog.pg_database WHERE datname=current_database())
AND classid=0 AND objid=$1::oid AND objsubid=1 AND mode='ShareLock' AND NOT granted) AS waiting`,[PARTITION_WRITER_ADVISORY_KEY])).rows[0].waiting
if(!waiting)await delay(10)
}
assert.equal(waiting,true,"the retiring checkout must wait at the common fence")
assert.equal((await exclusive.query("SELECT id FROM public.mcp_connections WHERE id=$1 FOR UPDATE NOWAIT",[principal.connectionId])).rows.length,1)
for(const candidate of [spent,due]){
const {rows:[row]}=await exclusive.query("SELECT pg_try_advisory_xact_lock(hashtextextended($1,0)) AS locked",["mcp-operation:"+candidate.id])
assert.equal(row.locked,true,"the retiring checkout must not hold a secondary operation key")
}
const blocked=await blockedAttempt
assert(blocked.scanned>0);assert.equal(blocked.retired,0);assert.equal(blocked.automaticReplay,false)
assert.deepEqual(await projection(),beforeFence)
}finally{
try{await blockedAttempt}finally{
try{
await exclusive.query("ROLLBACK")
assert.equal((await exclusive.query("SELECT pg_advisory_unlock($1::bigint) AS unlocked",[PARTITION_WRITER_ADVISORY_KEY])).rows[0].unlocked,true)
}finally{exclusive.release(true)}
}
}
const originalQuery=Client.prototype.query
let commits=0
try{
@@ -5,6 +5,7 @@ import { backupScheduleSettingsDtoSchema, putBackupScheduleSettingsSchema } from
import { z } from "zod"
import { connectProbeStorage } from "../../services/server-probe-storage-connection.js"
import type { ProbeStorageSql } from "../../services/server-probe-journal-maintenance.js"
import { admitPartitionWriter } from "../../services/partition-write-transaction.js"
import { sharedDatabaseGate } from "../../services/shared-database-gate.js"
import { operationIdempotencyDigest } from "./operation-replay-tombstones.js"
import { decryptSecretExisting, fingerprint, privateFingerprinterExisting, redact } from "./security.js"
@@ -110,11 +111,13 @@ const SOURCE = `SELECT to_jsonb(o) AS operation,to_jsonb(p) AS plan,to_jsonb(s)
AND octet_length(to_jsonb(o)::text)+octet_length(to_jsonb(p)::text)+octet_length(to_jsonb(s)::text)+octet_length(to_jsonb(k)::text)<=1048576
FOR UPDATE OF o,p,s,k NOWAIT`
async function transaction<T>(deadlineAt:number, work:(sql:ProbeStorageSql)=>Promise<T>):Promise<T> {
async function transaction<T>(deadlineAt:number, mode:"scan"|"write", work:(sql:ProbeStorageSql)=>Promise<T>):Promise<T> {
const sql = await connectProbeStorage({deadlineAt})
let committing=false,committed=false
try {
await sql.query("BEGIN")
// Preserve the one-second lock budget and admit before any target row or operation lock.
if(mode==="write")await admitPartitionWriter(sql,{deadlineAt:Math.min(deadlineAt,Date.now()+1000)})
await sql.query("SELECT set_config('TimeZone','UTC',true),set_config('statement_timeout','5000ms',true),set_config('lock_timeout','1000ms',true)")
const result=await work(sql)
committing=true
@@ -173,13 +176,13 @@ async function prepare(sql:ProbeStorageSql, operationId:string, connectionId:str
async function retire(operationId:string,connectionId:string,deadlineAt:number) {
const token=randomUUID()
await transaction(deadlineAt,async sql=>{
await transaction(deadlineAt,"write",async sql=>{
const source=await prepare(sql,operationId,connectionId)
const claim=await sql.query(`INSERT INTO public.mcp_ordinary_retirement_claims(operation_id,plan_id,connection_id,source_proof,claim_token)
VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING RETURNING operation_id`,[operationId,source.planId,connectionId,source.proof,token])
requireThat(claim.rows.length===1&&claim.rows[0].operation_id===operationId)
})
await transaction(deadlineAt,async sql=>{
await transaction(deadlineAt,"write",async sql=>{
const claim=(await sql.query(`SELECT source_proof,plan_id FROM public.mcp_ordinary_retirement_claims
WHERE operation_id=$1 AND connection_id=$2 AND claim_token=$3 AND claim_xid<>pg_current_xact_id()`,[operationId,connectionId,token])).rows
requireThat(claim.length===1)
@@ -203,7 +206,7 @@ let cursor:{finishedAt:string;operationId:string}|undefined
export function retireOrdinaryMcpOperations() {
return sharedDatabaseGate.request(async()=>{
const deadlineAt=Date.now()+15000
const rows=await transaction(deadlineAt,async sql=>(await sql.query(CANDIDATES,[cursor?.finishedAt??null,cursor?.operationId??null])).rows)
const rows=await transaction(deadlineAt,"scan",async sql=>(await sql.query(CANDIDATES,[cursor?.finishedAt??null,cursor?.operationId??null])).rows)
requireThat(rows.length<=8)
let scanned=0,retired=0
for(const row of rows){
+28 -1
View File
@@ -14,6 +14,8 @@ import { loadScopedRecoverySource } from "./routing-community-scoped-recovery-so
import { prepareScopedRecovery, registerScopedRecoveryService } from "./routing-community-scoped-recovery.js"
import { reconcileVrfProductionStep } from "../../services/vrf-production-execution.js"
import { needsBridgeVlanRecovery, prepareBridgeVlanRecovery } from "./bridge-vlan-recovery.js"
import { readRpfDependencies } from "./routing-controls.js"
import { verifyHealthChecks } from "./routing-health-checks.js"
async function source(operationId: string, context: ActionContext, allowServices = false) {
const { rows: [row] } = await pool.query("SELECT o.*,p.operation_id AS source_action,p.operation_version AS source_version,p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id WHERE o.id=$1 AND o.connection_id=$2", [operationId, context.connection.id])
@@ -109,6 +111,7 @@ export function registerRecoveryActions(register: typeof add) {
if (!intended.length || intended.some(step => step.state !== "verified") || intended.length !== checkpoints.length || checkpoints.some(checkpoint => !checkpoint.after_sealed)) throw new McpError("STATE_CONFLICT", "Нет полного набора подтверждённых снимков; требуется ручная сверка", 409)
if (needsBridgeVlanRecovery(row.source_action, evidence)) return prepareBridgeVlanRecovery({ operationId: row.id, evidence, checkpoints, intended }, context)
const snapshots = new Map<string, RosSnapshot>(), steps: ChangeStep[] = [], objects: ChangeEvidence["objects"] = [...evidence.objects]
const warnings = [`Обратный план операции ${row.id}; не является повтором исходного действия.`, "Восстановление удалённых объектов и сервисов требует их собственного механизма ревизий.", "Сохраняются инварианты Full OSPF/Up BFD и привязка соседств к устройствам, VRF, адресам и интерфейсам; достижимость восстановленной сетевой конфигурации проверяется контрольными пробами."]
for (const checkpoint of checkpoints) {
const original = evidence.steps[checkpoint.step_index]
if (!original || original.kind !== "ros" || original.step.action === "delete") throw new McpError("UNSUPPORTED_CAPABILITY", "Удалённый объект может иметь новые ID и зависимости; используйте проверенную ревизию", 409)
@@ -148,8 +151,32 @@ export function registerRecoveryActions(register: typeof add) {
if (current.fingerprint !== dependency.fingerprint || current.sourceProof !== dependency.sourceProof || dependency.projectionVersion !== rosSnapshotVersion(dependency.resource)) throw new McpError("STATE_CONFLICT", "Зависимость или источник исходного плана изменились", 409)
snapshots.set(key, current)
}
for (const change of steps) {
if (change.kind !== "ros" || change.step.resource !== "ip_settings" || !change.step.fields["rp-filter"]) continue
const settings = snapshots.get(`${change.step.serverId}:ip_settings`)
const rpf = await readRpfDependencies(change.step.serverId, change.step.fields["rp-filter"], settings?.sourceProof)
for (const snapshot of rpf.snapshots) {
const key = `${snapshot.serverId}:${snapshot.resource}`, previous = snapshots.get(key)
if (previous && (previous.fingerprint !== snapshot.fingerprint || previous.sourceProof !== snapshot.sourceProof)) {
throw new McpError("STATE_CONFLICT", "Зависимость RPF изменилась при подготовке восстановления", 409)
}
snapshots.set(key, snapshot)
}
warnings.push(...rpf.warnings)
}
objects.push(...[...new Set([...snapshots.values()].map(snapshot => snapshot.serverId))].map(serverId => ({ kind: "server", id: String(serverId), serverId })))
return { target: { scope: "servers", serverIds: [...new Set([...snapshots.values()].map(snapshot => snapshot.serverId))] }, objects, snapshots: [...snapshots.values()], steps, ospfOwnerGraph: evidence.ospfOwnerGraph, healthChecks: evidence.healthChecks?.filter(check => check.field === "state" || check.field === "bfd-up"), requiresHuman: true, warnings: [`Обратный план операции ${row.id}; не является повтором исходного действия.`, "Восстановление удалённых объектов и сервисов требует их собственного механизма ревизий.", "Сохраняются инварианты Full OSPF/Up BFD и привязка соседств к устройствам, VRF, адресам и интерфейсам; достижимость восстановленной сетевой конфигурации проверяется контрольными пробами."] }
const healthChecks = evidence.healthChecks?.filter(check => check.field === "state" || check.field === "bfd-up" || check.field === "resolved")
const currentSnapshots = [...snapshots.values()]
const serverIds = [...new Set(currentSnapshots.map(snapshot => snapshot.serverId))]
const nextHopChecks = healthChecks?.filter(check => check.field === "resolved")
if (nextHopChecks?.length) await verifyHealthChecks({ healthChecks: nextHopChecks, snapshots: currentSnapshots }, serverIds, false, async () => {
if (context.signal?.aborted) throw new McpError("READ_CANCELLED", "Подготовка обратного плана отменена", 409)
const current = await effectiveMcpConnection(context.principal)
if (fingerprint(current.policy) !== fingerprint(context.connection.policy) || fingerprint(current.owner_permissions) !== fingerprint(context.connection.owner_permissions)) {
throw new McpError("RESET_REQUIRED", "Допуск восстановления изменился", 409)
}
})
return { target: { scope: "servers", serverIds }, objects, snapshots: currentSnapshots, steps, ospfOwnerGraph: evidence.ospfOwnerGraph, healthChecks, requiresHuman: true, warnings }
},
})
}
+32 -5
View File
@@ -6,7 +6,7 @@ import { db } from "../../db/index.js"
import { servers } from "../../db/schema.js"
import { MikrotikClient } from "../../services/mikrotik.js"
import { readRouterOsSnapshotGet } from "../../services/routeros-snapshot-read.js"
import { McpError, fingerprint, privateSecretFingerprint } from "./security.js"
import { McpError, fingerprint, privateFingerprinter, privateSecretFingerprint } from "./security.js"
import { readOspfEffectiveDisabled } from "../../services/ospf-effective-disabled.js"
import { greNativeFieldsSchema } from "../../services/gre-mutation-input.js"
import { canonicalVrfMembers } from "../../services/vrf-members.js"
@@ -50,7 +50,7 @@ export const rosResources: RosResourceSpec[] = [
{ id: "ipv6_address", path: "/ipv6/address", title: "IPv6-адреса", permission: "mm:network", fields: z.object({ address: name.optional(), interface: name.optional(), advertise: bool.optional(), comment: text.optional(), disabled: bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["address", "interface"] },
{ id: "ipv4_route", path: "/ip/route", title: "Маршруты IPv4", permission: "mm:network", fields: z.object(route).strict(), create: true, remove: true, requiredOnCreate: ["dst-address"] },
{ id: "ipv6_route", path: "/ipv6/route", title: "Маршруты IPv6", permission: "mm:network", fields: z.object(route).strict(), create: true, remove: true, requiredOnCreate: ["dst-address"] },
{ id: "routing_table", path: "/routing/table", title: "Таблицы маршрутизации", permission: "mm:network", fields: z.object({ name: name.optional(), fib: bool.optional(), disabled: bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["name"] },
{ id: "routing_table", path: "/routing/table", title: "Таблицы маршрутизации", permission: "mm:network", fields: z.object({ name: name.optional(), fib: bool.optional(), disabled: bool.optional() }).strict(), observeFields: ["invalid"], create: true, remove: true, requiredOnCreate: ["name"] },
{ id: "routing_rule", path: "/routing/rule", title: "Правила маршрутизации", permission: "mm:network", fields: z.object({ "src-address": text.optional(), "dst-address": text.optional(), interface: text.optional(), action: z.enum(["lookup", "lookup-only-in-table", "drop", "unreachable"]).optional(), table: name.optional(), disabled: bool.optional(), comment: text.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["action"] },
{ id: "vrf", path: "/ip/vrf", title: "VRF", permission: "mm:network", fields: z.object({ name: name.optional(), interfaces: text.optional(), disabled: bool.optional(), comment: text.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["name", "interfaces"] },
{ id: "ip_settings", path: "/ip/settings", title: "RPF и IPv4 hash-policy", permission: "mm:network", fields: z.object({ "rp-filter": z.enum(["no", "loose", "strict"]).optional(), "ipv4-multipath-hash-policy": z.enum(["l3", "l4", "l3-inner"]).optional() }).strict(), create: false, remove: false },
@@ -77,8 +77,34 @@ export const rosResources: RosResourceSpec[] = [
export type ConfigurationRow = Record<string, unknown>
export interface RosStep { serverId: number; resource: string; action: "create" | "update" | "delete"; id?: string; fields: Record<string, string>; placeBeforeId?: string; expectedCost?: number; expectedServerProof?: string }
export const ROS_SNAPSHOT_VERSION = 3
export const rosSnapshotVersion = (resource: string): number => ["bridge", "bridge_port", "bridge_vlan"].includes(resource) ? 4 : ROS_SNAPSHOT_VERSION
export const rosSnapshotVersion = (resource: string): number => ["bridge", "bridge_port", "bridge_vlan", "routing_table"].includes(resource) ? 4 : ROS_SNAPSHOT_VERSION
export interface RosSnapshot { serverId: number; resource: string; rows: ConfigurationRow[]; fingerprint: string; projectionVersion?: number; sourceProof?: string }
const routingPolicyResources = new Set(["routing_rule", "ip_filter", "ip_nat", "ip_mangle", "ip_raw", "ipv6_filter", "ipv6_nat", "ipv6_mangle", "ipv6_raw"])
const routingPolicyPrefix = "routing_policy:"
const routingPolicyCounters = new Set(["bytes", "packets"])
export function isRoutingPolicySnapshot(resource: string) {
return resource.startsWith(routingPolicyPrefix) && routingPolicyResources.has(resource.slice(routingPolicyPrefix.length))
}
export function readRoutingPolicySnapshot(serverId: number, resource: string, expectedSourceProof?: string) {
if (!routingPolicyResources.has(resource)) throw new McpError("UNSUPPORTED_CAPABILITY", "Неизвестный каталог политики маршрутизации", 409)
return readRosSnapshot(serverId, routingPolicyPrefix + resource, undefined, expectedSourceProof)
}
async function routingPolicyRows(resource: string, payload: unknown): Promise<ConfigurationRow[]> {
if (!Array.isArray(payload)) throw new McpError("STATE_CONFLICT", "Каталог политики маршрутизации не подтверждён", 409)
const identify = await privateFingerprinter(`mcp-routing-policy-v1:${resource}`)
const ids = new Set<string>()
return payload.map(row => {
const id = row?.[".id"]
if (!row || typeof row !== "object" || Array.isArray(row) || typeof id !== "string" || !id.trim() || ids.has(id)) {
throw new McpError("STATE_CONFLICT", "Неоднозначные ID каталога политики маршрутизации", 409)
}
ids.add(id)
// Seal the native rule, including selectors outside the write schema.
const configuration = Object.fromEntries(Object.entries(row).filter(([name]) => resource === routingPolicyPrefix + "routing_rule" || !routingPolicyCounters.has(name)))
return { ".id": id, action: row.action ?? null, disabled: row.disabled ?? null, proof: identify(fingerprint(configuration)) }
})
}
export function resourceById(id: string): RosResourceSpec {
const resource = rosResources.find(item => item.id === id)
if (!resource) throw new McpError("UNSUPPORTED_CAPABILITY", "Неизвестный тип сетевого объекта", 400)
@@ -123,7 +149,8 @@ export function configurationRows(resource: RosResourceSpec, payload: unknown):
}
export async function readRosSnapshot(serverId: number, resourceId: string, beforeRead: () => Promise<unknown> = async () => {}, expectedSourceProof?: string): Promise<RosSnapshot> {
await beforeRead()
const resource = resourceById(resourceId), { client, server } = await routerClient(serverId)
const policy = isRoutingPolicySnapshot(resourceId)
const resource = resourceById(policy ? resourceId.slice(routingPolicyPrefix.length) : resourceId), { client, server } = await routerClient(serverId)
const sourceProof = await rosSnapshotSourceProof(server)
if (expectedSourceProof !== undefined && expectedSourceProof !== sourceProof) throw new McpError("STATE_CONFLICT", "Источник сетевого снимка изменился до опроса", 409)
const checkSource = async () => {
@@ -136,7 +163,7 @@ export async function readRosSnapshot(serverId: number, resourceId: string, befo
return client.post(resource.path + "/get", { ".id": id, "value-name": "disabled" })
}) : await readRouterOsSnapshotGet<unknown>({ client, path: resource.path, assertCurrent: checkSource })
await checkSource()
const rows = configurationRows(resource, payload)
const rows = policy ? await routingPolicyRows(resourceId, payload) : configurationRows(resource, payload)
return { serverId, resource: resourceId, rows, fingerprint: fingerprint(rows), projectionVersion: rosSnapshotVersion(resourceId), sourceProof }
}
export async function rosSnapshotSourceProof(server: Awaited<ReturnType<typeof routerClient>>["server"]): Promise<string> {
@@ -60,6 +60,16 @@ test("non-JSON detail cannot overwrite the already applied receipt", () => {
output.result = output
assert.equal(projected(accepted(output, "operation")).state, "applied")
})
test("compact receipt keeps a known pre-dispatch stop and its verified prefix", () => {
for (const [state, confirmedWrites] of [["rejected", 0], ["partial", 2]] as const) {
const result = { failedStepDispatched: false, confirmedWrites }
const output = { id, plan_id: plan, state, verification: "inconclusive", error_code: "STATE_CONFLICT" }
assert.deepEqual(projected(accepted({ ...output, result: { ...result, detail: 1n } }, "operation")), { ...output, result })
for (const invalid of [{ ...result, confirmedWrites: -1 }, { ...result, failedStepDispatched: true }]) {
assert.deepEqual(projected(accepted({ ...output, result: { ...invalid, detail: 1n } }, "operation")), output)
}
}
})
test("prepare omission keeps real plan/state, requests review, never invents executionAvailable", () => {
const result = accepted({ planId: plan, state: "approved", expiresAt: "2026-10-31T12:00:00Z", executionAvailable: true, changes: "x".repeat(MCP_CALL_RESULT_BYTES) }, "plan")
assert.equal(result.isError, undefined)
+7
View File
@@ -36,6 +36,13 @@ function project(output: unknown, projection: ResultProjection): Record<string,
const receipt: Record<string, unknown> = { id, plan_id: planId, state, verification }
const errorCode = data(output, "error_code")
if (errorCode === null || short(errorCode)) receipt.error_code = errorCode
const outcome = data(output, "result")
const confirmedWrites = data(outcome, "confirmedWrites")
if ((state === "partial" || state === "rejected") && data(outcome, "failedStepDispatched") === false &&
typeof confirmedWrites === "number" && Number.isSafeInteger(confirmedWrites) &&
(state === "partial" ? confirmedWrites > 0 : confirmedWrites === 0)) {
receipt.result = { failedStepDispatched: false, confirmedWrites }
}
return receipt
}
const planId = data(output, "planId"), state = data(output, "state")
+3 -2
View File
@@ -1,9 +1,9 @@
import assert from "node:assert/strict"
import { createRosPrefix } from "./ros-prefix.js"
import { fingerprint, McpError } from "./security.js"
import { ROS_SNAPSHOT_VERSION, type RosSnapshot, type RosStep, type ConfigurationRow } from "./resources.js"
import { rosSnapshotVersion, type RosSnapshot, type RosStep, type ConfigurationRow } from "./resources.js"
const snapshot = (resource: string, rows: ConfigurationRow[], sourceProof = "a".repeat(64)): RosSnapshot => ({ serverId: 1, resource, rows, fingerprint: fingerprint(rows), projectionVersion: ROS_SNAPSHOT_VERSION, sourceProof })
const snapshot = (resource: string, rows: ConfigurationRow[], sourceProof = "a".repeat(64)): RosSnapshot => ({ serverId: 1, resource, rows, fingerprint: fingerprint(rows), projectionVersion: rosSnapshotVersion(resource), sourceProof })
const old = snapshot("ip_filter", [{ ".id": "*a", chain: "forward", action: "accept", comment: "old" }, { ".id": "*b", chain: "forward", action: "drop", comment: "keep" }])
const dependency = snapshot("routing_table", [{ ".id": "*main", name: "main", fib: "true" }])
const changed = snapshot("ip_filter", [{ ...old.rows[0], comment: "new" }, old.rows[1]])
@@ -15,6 +15,7 @@ const isFailed = (error: unknown) => error instanceof McpError && error.code ===
for (const invalid of [{ ...old, sourceProof: undefined }, { ...old, projectionVersion: 1 }, { ...old, projectionVersion: 2 }, { ...old, fingerprint: "forged" }]) assert.throws(() => createRosPrefix([invalid]), error => error instanceof McpError && error.code === "PLAN_EXPIRED")
assert.throws(() => createRosPrefix([old, changed]), isConflict)
assert.throws(() => createRosPrefix([{ ...dependency, projectionVersion: 3 }]), { code: "PLAN_EXPIRED" })
const prefix = createRosPrefix([old, dependency, structuredClone(old)], read)
await prefix.check()
actual.set(old.resource, changed)
@@ -1,7 +1,7 @@
import assert from "node:assert/strict"
import { configurationRows, isOrderedRosResource, resourceById, type ConfigurationRow, type RosSnapshot, type RosStep } from "./resources.js"
import { configurationRows, isOrderedRosResource, resourceById, rosSnapshotVersion, type ConfigurationRow, type RosSnapshot, type RosStep } from "./resources.js"
import { fingerprint } from "./security.js"
import { verifyRosStepSnapshots } from "./ros-step-verification.js"
import { proveRosDriftExpectation, verifyRosStepSnapshots } from "./ros-step-verification.js"
import { readOspfTemplateFlags } from "../../services/ospf-template-flags.js"
function snapshot(resource: string, rows: ConfigurationRow[]): RosSnapshot {
@@ -106,3 +106,55 @@ assert.equal(raw[0].opaque, "outside-projection")
assert.equal(verifyRosStepSnapshots(update, {}, { ...snapshot("ip_filter", [a]), serverId: 2 }, snapshot("ip_filter", [updated])), false)
assert.equal(verifyRosStepSnapshots(update, {}, snapshot("bridge", [a]), snapshot("ip_filter", [updated])), false)
console.log("ROS projected whole-catalog verifier: acknowledged assigned ID, exact membership/field conservation, 12 ordered catalogs including OSPF templates, singleton settings, schema booleans, effective OSPF unset, immutable inputs")
const sourceProof = "a".repeat(64)
function driftSnapshot(resource: string, rows: ConfigurationRow[]): RosSnapshot {
return { ...snapshot(resource, rows), projectionVersion: rosSnapshotVersion(resource), sourceProof }
}
function expectDrift(step: RosStep, receipt: unknown, before: ConfigurationRow[], after: ConfigurationRow[]) {
const expectation = proveRosDriftExpectation(step, receipt, driftSnapshot(step.resource, before), driftSnapshot(step.resource, after))
assert.ok(expectation)
assert.equal(expectation.sourceProof, sourceProof)
return expectation
}
const settingsDrift = expectDrift(settings, {}, [s0], [s1])
assert.equal(settingsDrift.nativeId, undefined)
assert.equal(settingsDrift.matches(driftSnapshot("ip_settings", [{ ...s1, "ipv4-multipath-hash-policy": "l4" }])), true)
assert.equal(settingsDrift.matches(driftSnapshot("ip_settings", [s0])), false)
for (const rows of [[], [s1, s1], [{}], [{ "rp-filter": "unknown" }], [{ "rp-filter": null }], [{ "rp-filter": ["loose"] }]]) {
assert.equal(settingsDrift.matches(driftSnapshot("ip_settings", rows)), undefined)
}
const verifiedSettings = driftSnapshot("ip_settings", [s1])
assert.equal(settingsDrift.matches({ ...verifiedSettings, sourceProof: "b".repeat(64) }), undefined)
assert.equal(settingsDrift.matches({ ...verifiedSettings, serverId: 2 }), undefined)
assert.equal(settingsDrift.matches({ ...verifiedSettings, fingerprint: "b".repeat(64) }), undefined)
assert.equal(proveRosDriftExpectation(settings, {}, snapshot("ip_settings", [s0]), verifiedSettings), undefined, "Legacy evidence without a source/version cannot prove drift")
assert.equal(proveRosDriftExpectation(settings, {}, driftSnapshot("ip_settings", [s0]), { kind: "vrf-production-private-checkpoint-v1" }), undefined)
assert.equal(proveRosDriftExpectation(settings, {}, driftSnapshot("ip_settings", [s0]), { ...verifiedSettings, sourceProof: "b".repeat(64) }), undefined)
assert.equal(proveRosDriftExpectation(settings, undefined, driftSnapshot("ip_settings", [s0]), verifiedSettings), undefined)
const createDrift = expectDrift(create, ack, [a, b], [a, b, c])
assert.equal(createDrift.nativeId, "*C")
assert.equal(createDrift.matches(driftSnapshot("ip_filter", [{ ...b, comment: "unrelated" }, c])), true)
assert.equal(createDrift.matches(driftSnapshot("ip_filter", [a, b])), false)
assert.equal(createDrift.matches(driftSnapshot("ip_filter", [a, b, c, c])), undefined)
assert.equal(proveRosDriftExpectation(create, {}, driftSnapshot("ip_filter", [a, b]), driftSnapshot("ip_filter", [a, b, c])), undefined)
const updateDrift = expectDrift(update, {}, [a, b], [updated, b])
assert.equal(updateDrift.nativeId, "*A")
assert.equal(updateDrift.matches(driftSnapshot("ip_filter", [{ ...updated, comment: "unrelated" }, c])), true)
assert.equal(updateDrift.matches(driftSnapshot("ip_filter", [a, c])), false)
assert.equal(updateDrift.matches(driftSnapshot("ip_filter", [b, c])), false)
const booleanDrift = expectDrift(booleanStep, {}, [a], [a])
assert.equal(booleanDrift.matches(driftSnapshot("ip_filter", [{ ...a, disabled: "no" }])), true)
assert.equal(booleanDrift.matches(driftSnapshot("ip_filter", [{ ...a, disabled: "yes" }])), false)
assert.equal(booleanDrift.matches(driftSnapshot("ip_filter", [{ ...a, disabled: "" }])), undefined)
const deleteDrift = expectDrift(remove, null, [a, b], [b])
assert.equal(deleteDrift.nativeId, "*A")
assert.equal(deleteDrift.matches(driftSnapshot("ip_filter", [{ ...b, comment: "unrelated" }, c])), true)
assert.equal(deleteDrift.matches(driftSnapshot("ip_filter", [{ ...a, disabled: "no" }, c])), false)
assert.equal(deleteDrift.matches(driftSnapshot("ip_filter", [{ ...a, chain: "output", comment: "a different rule reused this ID" }, b])), undefined)
console.log("ROS drift proof: bound source and verified transition, scoped typed fields, unknown observations and delete ID reuse")
@@ -1,7 +1,8 @@
import { isDeepStrictEqual } from "node:util"
import { fingerprint } from "./security.js"
import { acknowledgedCreateId } from "./native-create-identity.js"
import { canonicalBridgeVlanValue } from "../../services/bridge-vlan-values.js"
import { isOrderedRosResource, resourceById, type ConfigurationRow, type RosResourceSpec, type RosSnapshot, type RosStep } from "./resources.js"
import { isOrderedRosResource, resourceById, rosSnapshotVersion, type ConfigurationRow, type RosResourceSpec, type RosSnapshot, type RosStep } from "./resources.js"
function booleanValue(value: unknown): unknown {
if ([true, "true", "yes"].includes(value as boolean | string)) return true
@@ -92,3 +93,66 @@ export function verifyRosStepSnapshots(step: RosStep, result: unknown, before: R
// already prove exact membership. Ordered catalogs must also retain position.
return !isOrderedRosResource(step.resource) || isDeepStrictEqual(expectedIds, [...current.keys()])
}
const singletonRosResource = (resource: string) => resource === "ip_settings" || resource === "ipv6_settings"
function projectedSnapshot(value: unknown, step: RosStep): value is RosSnapshot & { sourceProof: string } {
if (!value || typeof value !== "object" || Array.isArray(value)) return false
const snapshot = value as RosSnapshot
if (snapshot.serverId !== step.serverId || snapshot.resource !== step.resource
|| snapshot.projectionVersion !== rosSnapshotVersion(step.resource)
|| typeof snapshot.sourceProof !== "string" || !/^[a-f0-9]{64}$/.test(snapshot.sourceProof)
|| !Array.isArray(snapshot.rows) || snapshot.fingerprint !== fingerprint(snapshot.rows)) return false
if (!singletonRosResource(step.resource)) return indexed(snapshot.rows) !== undefined
const row = snapshot.rows[0]
return snapshot.rows.length === 1 && !!row && typeof row === "object" && !Array.isArray(row)
}
interface RosDriftExpectation {
nativeId: string | undefined
sourceProof: string
matches(current: unknown): boolean | undefined
}
/** Bind the drift expectation to an acknowledged and verified transition.
* The caller distinguishes a stored JSON null (native 204) from SQL NULL. */
export function proveRosDriftExpectation(step: RosStep, receipt: unknown, before: unknown, after: unknown): RosDriftExpectation | undefined {
if (receipt === undefined || !projectedSnapshot(before, step) || !projectedSnapshot(after, step)
|| before.sourceProof !== after.sourceProof || !verifyRosStepSnapshots(step, receipt, before, after)) return undefined
const resource = resourceById(step.resource)
const nativeId = step.action === "create" ? acknowledgedCreateId(receipt) : step.id
const singleton = singletonRosResource(step.resource)
const deleted = step.action === "delete" ? before.rows.find(row => row[".id"] === nativeId) : undefined
const sourceProof = after.sourceProof
return {
nativeId,
sourceProof,
matches(current) {
if (!projectedSnapshot(current, step) || current.sourceProof !== sourceProof) return undefined
const row = singleton ? current.rows[0] : current.rows.find(item => item[".id"] === nativeId)
if (deleted) {
if (!row) return true
// A different row at a reused ID does not prove the original row returned.
return conserved(resource, deleted, row) ? false : undefined
}
if (!row) return false
let complete = true
for (const [key, expected] of Object.entries(step.fields)) {
const schema = resource.fields.shape[key]
const raw = row[key]
if (!schema || !Object.hasOwn(row, key) || raw === undefined || raw === null) {
complete = false
continue
}
const boolean = schema.safeParse(true).success
const valid = boolean ? typeof booleanValue(raw) === "boolean"
: (typeof raw === "string" || typeof raw === "number") && (
schema.safeParse(raw).success || typeof raw === "string" && /^\d+$/.test(raw) && schema.safeParse(Number(raw)).success
)
if (!valid) complete = false
else if (!requested(resource, { [key]: expected }, row)) return false
}
return complete ? true : undefined
},
}
}
+60 -13
View File
@@ -1,5 +1,5 @@
import { prepareRoutingMainTransition, beginRoutingMainTransition, finishRoutingMainTransition, type RoutingMainTransitionAttempt } from "./routing-main-transition.js"
import { readRoutingObservations } from "./routing-observations.js"
import { queryRoutingObservations, readRoutingObservations } from "./routing-observations.js"
import { isIP } from "node:net"
import { hasPermission } from "../../lib/permissions.js"
import { effectiveMcpConnection } from "./store.js"
@@ -25,10 +25,45 @@ import { resolveRoutingProfileAuthority, assertRoutingProfileAuthority, type Rou
import { createRoutingSourceContext, readRoutingEndpoints } from "./routing-observation-source.js"
const sid = z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
const historySelector = z.string().trim().min(1).max(100)
const routingHistorySchema = z.object({
serverId: sid.optional(), srcServerId: sid.optional(), dstServerId: sid.optional(),
srcInterface: historySelector.optional(), dstInterface: historySelector.optional(),
family: z.enum(["ipv4", "ipv6"]).optional(), table: historySelector.optional(),
limit: z.number().int().min(1).max(100).default(50),
cursor: z.object({ recordedAt: z.string().datetime({ precision: 6 }), id: z.string().uuid() }).strict().optional(),
}).strict()
const ospfOptimizerSchema = z.object({ serverId: sid, family: z.enum(["ipv4", "ipv6"]).default("ipv4"), table: z.string().min(1).default("main"), ecmpPaths: z.number().int().min(1).max(64).default(1), pingWeight: z.number().min(0).max(100).default(60), maxLossPct: z.number().min(0).max(100).default(1) }).strict()
export { readRoutingObservations } from "./routing-observations.js"
const profileTarget = (profile: z.infer<typeof routeAiProfileSchema>) => [...new Set([profile.serverId, ...profile.candidates.flatMap(candidate => [candidate.jumpHostId, candidate.exitNodeId])])]
const routeAnalysisSchema = z.object({
profile: routeAiProfileSchema,
offset: z.number().int().min(0).max(100).default(0),
limit: z.number().int().min(1).max(100).default(20),
eligibility: z.enum(["all", "eligible", "ineligible"]).default("all"),
}).strict()
const routeComparisonSchema = z.object({ profile: routeAiProfileSchema, candidateIds: z.array(z.string().min(1).max(100)).min(2).max(8) }).strict().superRefine(({ profile, candidateIds }, context) => {
const known = new Set(profile.candidates.map(candidate => candidate.id))
if (new Set(candidateIds).size !== candidateIds.length || candidateIds.some(id => !known.has(id))) {
context.addIssue({ code: "custom", path: ["candidateIds"], message: "Укажите разные ID путей из профиля" })
}
})
async function analyzeCommunityProfile(profile: z.infer<typeof routeAiProfileSchema>, context: ActionContext) {
const ids = profileTarget(profile)
if (ids.some(id => !context.connection.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Профиль выходит за область доступа")
const authorize = routeContextAccess(context)
await authorize()
const ranking = rankRoutingCandidates(profile, await readRoutingObservations(ids))
const routeContext = await readRouteCommunityContext(profile, { authorize, signal: context.signal })
for (const row of ranking) if (routeContext.before.candidateDiagnostics.find(diagnostic => diagnostic.candidateId === row.candidate.id)?.status !== "available") {
row.eligible = false; row.reasons.push("CANDIDATE_CONTEXT_UNAVAILABLE")
}
ranking.sort((a, b) => Number(b.eligible) - Number(a.eligible))
return { profile, ranking, ...routeContext.before, observedPath: false as const, returnPathVerified: false as const, observedAt: new Date().toISOString() }
}
async function assertCandidateEligible(profile: z.infer<typeof routeAiProfileSchema>, candidateId: string) {
const candidate = rankRoutingCandidates(profile, await readRoutingObservations(profileTarget(profile))).find(row => row.candidate.id === candidateId)
if (!candidate?.eligible) throw new McpError("MEASUREMENT_UNKNOWN", "Выбранный путь не удовлетворяет свежести измерений, ограничениям и SLA: " + (candidate?.reasons.join("; ") ?? "путь отсутствует"), 409)
@@ -65,21 +100,33 @@ async function filtersBefore(input: Record<string, unknown>, afterWrite = false)
export function registerRoutingActions(register: typeof add) {
registerRoutingMeasurement(register)
registerScopedCommunityService()
register("routing.measurements.read", "mm_routing_measurements_query", "История измерений со статусом проверки источника", "network", "mm:network:read", "read", "servers", z.object({ serverId: sid.optional() }).strict(), {
register("routing.measurements.read", "mm_routing_measurements_query", "История измерений со статусом проверки источника", "network", "mm:network:read", "read", "servers", routingHistorySchema, {
target: (_input, context) => ({ scope: "servers", serverIds: context.connection.policy.scope.serverIds }),
read: async (input, context) => ({ observations: await readRoutingObservations(context.connection.policy.scope.serverIds, input.serverId as number | undefined) }),
read: async (input, context) => queryRoutingObservations(context.connection.policy.scope.serverIds, routingHistorySchema.parse(input)),
})
register("routing.analyze", "mm_route_optimizer_analyze", "Анализ профиля Community", "network", "mm:network:read", "read", "servers", z.object({ profile: routeAiProfileSchema }).strict(), {
register("routing.analyze", "mm_route_optimizer_analyze", "Анализ профиля Community", "network", "mm:network:read", "read", "servers", routeAnalysisSchema, {
target: input => ({ scope: "servers", serverIds: profileTarget(routeAiProfileSchema.parse(input.profile)) }),
read: async (input, context) => {
const profile = routeAiProfileSchema.parse(input.profile), ids = profileTarget(profile)
if (ids.some(id => !context.connection.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Профиль выходит за область доступа")
const ranking = rankRoutingCandidates(profile, await readRoutingObservations(ids))
const routeContext = await readRouteCommunityContext(profile, { authorize: routeContextAccess(context), signal: context.signal })
for (const row of ranking) if (routeContext.before.candidateDiagnostics.find(diagnostic => diagnostic.candidateId === row.candidate.id)?.status !== "available") {
row.eligible = false; row.reasons.push("CANDIDATE_CONTEXT_UNAVAILABLE")
}
return { profile, ranking, ...routeContext.before, observedPath: false, returnPathVerified: false, observedAt: new Date().toISOString() }
const { profile, offset, limit, eligibility } = routeAnalysisSchema.parse(input)
const { ranking, candidateDiagnostics, ...analysis } = await analyzeCommunityProfile(profile, context)
const filtered = eligibility === "all" ? ranking : ranking.filter(row => row.eligible === (eligibility === "eligible"))
const page = filtered.slice(offset, offset + limit)
const candidateIds = new Set(page.map(row => row.candidate.id))
return { ...analysis, ranking: page, candidateDiagnostics: candidateDiagnostics.filter(row => candidateIds.has(row.candidateId)),
total: filtered.length, offset, limit, nextOffset: offset + limit < filtered.length ? offset + limit : null }
},
})
register("routing.compare", "mm_route_optimizer_compare", "Сравнение выбранных путей Community", "network", "mm:network:read", "read", "servers", routeComparisonSchema, {
target: input => ({ scope: "servers", serverIds: profileTarget(routeAiProfileSchema.parse(input.profile)) }),
read: async (input, context) => {
const { profile, candidateIds } = routeComparisonSchema.parse(input)
const analysis = await analyzeCommunityProfile(profile, context)
const ranking = analysis.ranking.flatMap(({ candidate, ...row }, index) => candidateIds.includes(candidate.id)
? [{ candidateId: candidate.id, profileRank: index + 1, ...row }]
: [])
return { serverId: profile.serverId, family: profile.family, table: profile.table, community: profile.community,
ranking, bestEligibleCandidateId: ranking.find(row => row.eligible)?.candidateId ?? null,
observedPath: analysis.observedPath, returnPathVerified: analysis.returnPathVerified, observedAt: analysis.observedAt }
},
})
@@ -216,7 +263,7 @@ export function registerRoutingActions(register: typeof add) {
warnings: evidence.warnings, criteria: { maxLossPct: input.maxLossPct, pingWeight: input.pingWeight, ecmpPaths: input.ecmpPaths }, executionAvailable: false, proposalOnly: true, forwardingVerified: false, newApprovedPlanRequired: true }
},
})
register("routing.ospf.optimize", "mm_ospf_optimizer_plan", "Оптимизация стоимости OSPF без пересчёта при применении", "network", "mm:network:write", "write", "servers", ospfOptimizerSchema, { target: (_input, context) => ({ scope: "servers", serverIds: context.connection.policy.scope.serverIds }), prepare: buildOspfOptimizationEvidence, version: 9 })
register("routing.ospf.optimize", "mm_ospf_optimizer_plan", "Оптимизация стоимости OSPF без пересчёта при применении", "network", "mm:network:write", "write", "servers", ospfOptimizerSchema, { target: (_input, context) => ({ scope: "servers", serverIds: context.connection.policy.scope.serverIds }), prepare: buildOspfOptimizationEvidence, version: 10 })
}
async function buildOspfOptimizationEvidence(input: Record<string, unknown>, context: ActionContext, mode: "read" | "write" = "write"): Promise<ChangeEvidence & { before: { ranking: OspfRank[] } }> {
@@ -151,6 +151,46 @@ export async function acceptRouteCommunityContextMcp(actor: AuthUser) {
const legacyComment = policyComment
const withoutProbe = (({ controlProbe: _probe, ...candidate }) => candidate)(base.candidates[0])
const mainProfile = routeAiProfileSchema.parse({ ...base, table: "main", candidates: [withoutProbe, { ...withoutProbe, id: "b", gateway: "192.0.2.9", gatewayRef: { resource: "ipv4_route", id: "*r2" } }] })
const compareAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Community comparison", mode: "observer", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read"], scope: { serverIds: [sid], operationIds: ["routing.compare"], allowGlobal: false } }), actor)
const analyzeAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Community analysis only", mode: "observer", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read"], scope: { serverIds: [sid], operationIds: ["routing.analyze"], allowGlobal: false } }), actor)
try {
const comparePrincipal = await authenticateMcpToken(compareAccess.token)
const analyzePrincipal = await authenticateMcpToken(analyzeAccess.token)
const compare = (candidateIds: string[], profile = mainProfile) => invokeMcpTool(comparePrincipal, "mm_route_optimizer_compare", { profile, candidateIds })
const beforeDenied = requests
await assert.rejects(invokeMcpTool(analyzePrincipal, "mm_route_optimizer_compare", { profile: mainProfile, candidateIds: ["a", "b"] }))
await assert.rejects(compare(["a", "a"]))
await assert.rejects(compare(["a", "missing"]))
await assert.rejects(compare(["a", "b"], routeAiProfileSchema.parse({ ...mainProfile, candidates: [mainProfile.candidates[0], { ...mainProfile.candidates[1], exitNodeId: sid + 1 }] })))
assert.equal(requests, beforeDenied)
const beforeWrites = writes
const analyzed = await invokeMcpTool(analyzePrincipal, "mm_route_optimizer_analyze", { profile: mainProfile }) as { ranking: Array<{ candidate: { id: string }; eligible: boolean; score: number | null }> }
const result = await compare(["b", "a"]) as { ranking: Array<{ candidateId: string; profileRank: number; eligible: boolean; score: number | null }>; observedPath: boolean; returnPathVerified: boolean }
assert.deepEqual(result.ranking.map(({ candidateId, profileRank, eligible, score }) => ({ candidateId, profileRank, eligible, score })), analyzed.ranking.map(({ candidate, eligible, score }, index) => ({ candidateId: candidate.id, profileRank: index + 1, eligible, score })))
assert.equal(result.observedPath, false)
assert.equal(result.returnPathVerified, false)
const pagedProfile = routeAiProfileSchema.parse({ ...mainProfile, candidates: [...mainProfile.candidates, ...Array.from({ length: 20 }, (_, index) => ({ ...mainProfile.candidates[1], id: `missing-${String(index).padStart(2, "0")}`, gatewayRef: { resource: "ipv4_route", id: `*absent${index}` } }))] })
type AnalysisPage = { ranking: Array<{ candidate: { id: string }; eligible: boolean }>; candidateDiagnostics: Array<{ candidateId: string }>; total: number; nextOffset: number | null; currentCandidateId: string | null }
const page = (options = {}) => invokeMcpTool(analyzePrincipal, "mm_route_optimizer_analyze", { profile: pagedProfile, ...options }) as Promise<AnalysisPage>
const firstPage = await page(), secondPage = await page({ offset: 20 }), full = await page({ limit: 100 })
assert.equal(firstPage.total, 22)
assert.equal(firstPage.ranking.length, 20)
assert.equal(firstPage.candidateDiagnostics.length, 20)
assert.equal(firstPage.nextOffset, 20)
assert.equal(secondPage.nextOffset, null)
assert.deepEqual([...firstPage.ranking, ...secondPage.ranking], full.ranking)
assert.equal(secondPage.currentCandidateId, full.currentCandidateId)
const eligible = await page({ eligibility: "eligible" }), ineligible = await page({ eligibility: "ineligible", limit: 100 })
assert.deepEqual(eligible.ranking, full.ranking.filter(row => row.eligible))
assert.deepEqual(ineligible.ranking, full.ranking.filter(row => !row.eligible))
await assert.rejects(page({ limit: 101 }))
assert.equal(writes, beforeWrites)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_plans WHERE connection_id=$1", [compareAccess.connection.id])).rows[0].n, 0)
assert.deepEqual(privateMarkerPaths(result), [])
console.log("✓ Route AI comparison: explicit observer scope, invalid IDs and hidden dependencies rejected before RouterOS, analysis parity and no plan or write")
} finally {
await pool.query("DELETE FROM mcp_connections WHERE id=ANY($1::uuid[])", [[compareAccess.connection.id, analyzeAccess.connection.id]])
}
for (const iface of ["wan", "hop"]) {
const observationId = randomUUID()
await pool.query("INSERT INTO mcp_routing_observations(id,src_server_id,dst_server_id,src_interface,dst_interface,family,routing_table,ping_ms,loss_pct,dl_mbps,ul_mbps,ping_at,bandwidth_at,context_verified,source) VALUES($1,$2,$2,$3,'peer','ipv4','main',1,0,100,100,now(),now(),true,'fixture')", [observationId, sid, iface])
@@ -17,6 +17,7 @@ import { routeContextClients } from "./routing-community-context-native.js"
import { pathProofTag } from "./path-proof-purpose.js"
import { assertManagementTransport, managementTransportClient, type ManagementTransportCapability } from "./management-transport.js"
import { assertRoutingNetworkSharedContextRead, inspectRoutingNetworkSharedContextRead, readRoutingNetworkSharedResource, routingNetworkSharedContextPublicationFence, type RoutingNetworkSharedContextRead } from "./routing-network-native-session.js"
import { scopedTargetRuleMatches } from "./routing-community-scoped-model.js"
export function routeContextAccess(context: ActionContext) {
return async () => {
@@ -27,7 +28,7 @@ export function routeContextAccess(context: ActionContext) {
}
}
export type RouteContextOptions = { authorize?: () => Promise<void>; signal?: AbortSignal; expectedSources?: Array<{ serverId: number; proof: string }>; selectedCandidateId?: string; managementTransport?: ManagementTransportCapability }
export type OwnedRouteCommunityTarget = { nativeId: string; rawRow: NativeBgpRow; endpoint: Readonly<RoutingEndpoint>; desiredTargetProof: (ruleBody: string) => string }
export type OwnedRouteCommunityTarget = { nativeId: string; rawRow: NativeBgpRow; endpoint: Readonly<RoutingEndpoint>; desiredTargetProof: (ruleBody: string) => string; matchesRuleProof: (ruleBody: string, proof: unknown) => boolean }
/** Fixed endpoint clients and private proofs are retained in closures. Public
* results contain no credentials, native rules, arbitrary comments or SQL rows. */
@@ -234,13 +235,16 @@ async function readContext(profile: RouteAiProfile, options: RouteContextOptions
// untouched dependencies after its deliberate, exact body replacement.
const targetRuleProof = targetRule ? sign("target-rule:" + fingerprint(stableBgpPolicyRow(targetRule))) : null
const immutableTarget = targetRule ? structuredClone(targetRule) : null
const ownedTarget: OwnedRouteCommunityTarget | undefined = immutableTarget ? {
nativeId: immutableTarget[".id"], rawRow: structuredClone(immutableTarget), endpoint: Object.freeze({ ...endpoints.find(endpoint => endpoint.id === sourceId)! }),
desiredTargetProof(ruleBody) {
const ownedTarget: OwnedRouteCommunityTarget | undefined = immutableTarget ? (() => {
const desiredTargetProof = (ruleBody: string) => {
if (typeof ruleBody !== "string" || Buffer.byteLength(ruleBody, "utf8") > 65536) throw new McpError("LIMIT_EXCEEDED", "Тело scoped Community фильтра превышает лимит", 409)
return sign("target-rule:" + fingerprint(stableBgpPolicyRow({ ...immutableTarget, rule: ruleBody })))
},
} : undefined
}
return {
nativeId: immutableTarget[".id"], rawRow: structuredClone(immutableTarget), endpoint: Object.freeze({ ...endpoints.find(endpoint => endpoint.id === sourceId)! }),
desiredTargetProof, matchesRuleProof: (ruleBody: string, proof: unknown) => scopedTargetRuleMatches({ rawRow: immutableTarget, desiredTargetProof }, ruleBody, proof),
}
})() : undefined
const dependencies = rawProof.map(value => {
const snapshot = value as { path?: string; rows?: Record<string, unknown>[] }
return snapshot.path === "/routing/filter/rule" && snapshot.rows && targetRule ? { ...snapshot, rows: snapshot.rows.map(row => row[".id"] === targetRule[".id"] ? { ...row, rule: "[separately-sealed-owned-body]" } : row) } : value
@@ -284,5 +288,5 @@ export async function readRouteCommunityContextWithSharedNativeSession(sharedCon
// The acquisition ends when this function returns. Later publication fences
// must use the owner rather than retain a completed transport-read permit.
return { before: result.before, fence: routingNetworkSharedContextPublicationFence(sharedContext),
ownedTarget: result.ownedTarget ? { nativeId: result.ownedTarget.nativeId, rawRow: structuredClone(result.ownedTarget.rawRow) } : undefined }
ownedTarget: result.ownedTarget ? { nativeId: result.ownedTarget.nativeId, rawRow: structuredClone(result.ownedTarget.rawRow), matchesRuleProof: result.ownedTarget.matchesRuleProof } : undefined }
}
@@ -1,4 +1,5 @@
import type { ChangeEvidence } from "./action-types.js"
import { isRoutingPolicySnapshot } from "./resources.js"
const record = (value: unknown): Record<string, unknown> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {}
const rules = (value: unknown) => Array.isArray(value) ? value.map(item => {
@@ -33,8 +34,13 @@ export function publicRoutingCommunityChanges(evidence: ChangeEvidence) {
/** Service recovery has the same private native bodies as its forward plan. */
export function publicMcpChangeEvidence(operationId: string, evidence: ChangeEvidence) {
const scopedRecovery = operationId === "operations.recover" && evidence.steps.length === 1 && evidence.steps[0].kind === "service" && evidence.steps[0].service === "routing.community.scoped.recover"
if (operationId === "routing.profiles.save") return {...evidence,steps:evidence.steps.map(step=>{if(step.kind!=="service")return step;const {baseline:_privateBaseline,...input}=step.input;return {...step,input}})}
return operationId === "routing.community.switch" || scopedRecovery ? publicRoutingCommunityChanges(evidence) : evidence
if (operationId === "routing.profiles.save" || operationId === "routing.controller.update") return {...evidence,steps:evidence.steps.map(step=>{if(step.kind!=="service")return step;const {baseline:_privateBaseline,...input}=step.input;return {...step,input}})}
if (operationId === "routing.community.switch" || scopedRecovery) return publicRoutingCommunityChanges(evidence)
if (evidence.healthChecks?.some(check => check.nextHopInterface)) evidence = { ...evidence, healthChecks: evidence.healthChecks.map(({ nextHopInterface: _privateInterface, ...check }) => check) }
if (!evidence.snapshots.some(snapshot => isRoutingPolicySnapshot(snapshot.resource))) return evidence
return { ...evidence, snapshots: evidence.snapshots.map(snapshot => isRoutingPolicySnapshot(snapshot.resource)
? { serverId: snapshot.serverId, resource: snapshot.resource, rowCount: snapshot.rows.length }
: snapshot) }
}
/** Public controller DTO is explicit. Private baseline/native proofs, seal
@@ -1,6 +1,6 @@
import { createRoutingReservationStore } from "./routing-reservation-store.js"
import { observeCommittedScopedCheckpoint } from "../../test/committed-scoped-checkpoint-observer.js"
import { prepareRoutingControllerBaseline, advanceRoutingControllerBaseline } from "./routing-controller-baseline.js"
import { prepareRoutingControllerBaseline, advanceRoutingControllerBaseline, assertRoutingControllerBaseline } from "./routing-controller-baseline.js"
import { loadScopedConfigurationPacket, loadScopedNetworkCheckpoint } from "./routing-community-scoped-checkpoint.js"
import { assertScopedCommunityRequest } from "./routing-community-scoped-request.js"
import { originalEffectReportSchema, originalEffectSelectionSchema } from "./routing-original-effects.js"
@@ -33,12 +33,25 @@ import { requireRecentPath } from "./path-verification.js"
import { registerManagementChannel } from "./management.js"
import { isSchedulerJobsPaused, pauseSchedulerJobsAndWait, resumeSchedulerJobs } from "../../services/scheduler-running.js"
type BrowserRoutingTransport = {
call(tool: string, input: Record<string, unknown>): Promise<Record<string, unknown>>
approve(plan: Awaited<ReturnType<typeof publicPlan>>): Promise<void>
verifyOperation(operation: { id: string; planId: string }): Promise<void>
}
type BrowserRoutingJourney = <T>(access: { token: string; connectionId: string; serverId: number },
run: (transport: BrowserRoutingTransport) => Promise<T>) => Promise<T>
/** Disposable SQL + loopback HTTP only. Does not access or configure CHR. */
export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options: { originalEffectsOnly?: boolean; controllerFailoverOnly?: boolean; originalCredentialRevocationOnly?: boolean; forwardRoundsOnly?: boolean; manualInverseChargeOnly?: boolean; manualInverseChargeCase?: "positive" | "quota" | "concurrent" | "unknown" | "revoked" | "nonpersistent" | "mixed-forward"; lateInverseAuthorityChange?: "edit" | "disable" | "grant" } = {}) {
export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options: { originalEffectsOnly?: boolean; controllerFailoverOnly?: boolean; originalCredentialRevocationOnly?: boolean; forwardRoundsOnly?: boolean; manualInverseChargeOnly?: boolean; manualInverseChargeCase?: "positive" | "quota" | "concurrent" | "unknown" | "revoked" | "nonpersistent" | "mixed-forward"; lateInverseAuthorityChange?: "edit" | "disable" | "grant"; browserJourney?: BrowserRoutingJourney; bareReadbackOnly?: boolean } = {}) {
if (options.browserJourney || options.bareReadbackOnly) assert.equal(process.env.NODE_ENV, "test")
const sid = options.lateInverseAuthorityChange ? { edit: 131, disable: 132, grant: 133 }[options.lateInverseAuthorityChange] : 131
const writer: AuthUser = { ...actor, permissions: [...new Set([...actor.permissions, "mm:filters:write"])] }
const initial = compileManagedFilterRules([{ community: "65000:10", gateway: "192.0.2.1", outIface: "", action: "route", description: "" }, { community: "65000:20", gateway: "192.0.2.5", outIface: "", action: "route", description: "" }])
const filters = ["a", "b"].map(key => ({ ".id": "*f" + key, chain: "custom-" + key, rule: initial, disabled: "false", comment: managedComment("NEVER_SCOPED_COMMENT_" + key), opaque: "NEVER_SCOPED_OPAQUE" }))
const compileFixture = (outIface: string) => compileManagedFilterRules([{ community: "65000:10", gateway: "192.0.2.1", outIface, action: "route", description: "" }, { community: "65000:20", gateway: "192.0.2.5", outIface: "", action: "route", description: "" }])
const initial = compileFixture(options.bareReadbackOnly ? "gre-a" : ""), foreign = compileFixture("")
const filters = ["a", "b"].map(key => ({ ".id": "*f" + key, chain: "custom-" + key, rule: key === "a" ? initial : foreign, disabled: "false", comment: managedComment("NEVER_SCOPED_COMMENT_" + key), opaque: "NEVER_SCOPED_OPAQUE" }))
const bareRule = (rule: string) => rule.replace(/^ set gw-interface "(gre-[ab])";$/gm, " set gw-interface $1;")
const patchBodies: string[] = []
let mode: "normal" | "ignored" | "lost" | "collateral" | "post-failed" | "post-unknown" = "normal", writes = 0, requests = 0
let controllerProbeUnknown = false
let controllerFailoverProbeSources: string[] | undefined
@@ -55,13 +68,14 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
let originalEffectCase: "transition" | "no-op" | "ambiguous" = "transition"
let exposeEffectCatalogue = false
const originalEffectWire: Array<{ method: string; path: string; afterPatch: boolean }> = []
const originalEffectChanged = () => originalEffectCase === "no-op" || filters[0].rule !== initial
const originalEffectChanged = () => originalEffectCase === "no-op" || (options.bareReadbackOnly ? bareRule(filters[0].rule) !== bareRule(initial) : filters[0].rule !== initial)
const effectRoutes = () => {
const changed = originalEffectChanged()
const row = { ".id": "*effect1", afi: "ip4", "routing-table": "tenant-a", "dst-address": "203.0.113.0/24",
bgp: "true", connect: "false", static: "false", active: "true", contribution: "active", ecmp: "false",
disabled: "false", filtered: "false", unreachable: "false", blackhole: "false", "bgp.communities": "65000:10",
gateway: changed ? "192.0.2.9" : "192.0.2.1", "immediate-gw": (changed ? "203.0.113.9" : "203.0.113.1") + "%wan-a" }
gateway: changed ? "192.0.2.9" : "192.0.2.1",
"immediate-gw": options.bareReadbackOnly ? changed ? "192.0.2.9%gre-b" : "192.0.2.1%gre-a" : (changed ? "203.0.113.9" : "203.0.113.1") + "%wan-a" }
return originalEffectCase === "ambiguous" ? [row, { ...row, ".id": "*effect2" }] : [row]
}
const effectConnection = { ".id": "*ca", name: "peer-a", instance: "default", vrf: "main", "routing-table": "tenant-a", afi: "ip",
@@ -80,6 +94,7 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
"/ip/route": (controllerFailoverProbeSources ? [1, 9, 10, 11] : [1, 9]).map(value => ({ ".id": "*r" + value, "dst-address": "192.0.2." + value + "/32", "routing-table": "main", disabled: "false",
"immediate-gw": "203.0.113." + value + "%wan-a", comment: managedComment("recursive"), opaque: "NEVER_SCOPED_ROUTE" })),
"/interface": ["wan-a", "wan-b", "hop", "target"].map((name, index) => ({ ".id": "*if" + index, name, disabled: "false", running: "true" })),
...(options.bareReadbackOnly ? { "/interface/gre": ["a", "b"].map(key => ({ ".id": "*e" + key, name: "gre-" + key, disabled: "false", running: "true" })) } : {}),
"/ip/address": [
...["wan-a", "wan-b", "hop", "target"].map((name, index) => ({ ".id": "*ad" + index, interface: name, address: "198.51.100." + (index + 1) + "/32", disabled: "false", invalid: "false" })),
...(controllerFailoverProbeSources ? [11, 12, 13].map(index => ({ ".id": "*ad" + index, interface: "wan-a", address: "198.51.100." + index + "/32", disabled: "false", invalid: "false" })) : []),
@@ -133,7 +148,7 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
const packet = JSON.parse(await decryptSecret(checkpoint[0].configuration_sealed))
assert.equal(packet.phase, "CONFIG_APPLIED"); assert.equal(packet.originalRule, initial)
assert.equal(packet.originalCandidateId, "a"); assert.equal(packet.desiredCandidateId, controllerFailoverProbeSources ? "d" : "b")
assert.equal(packet.desiredRule, filters.find(row => row[".id"] === packet.nativeId)!.rule)
assert.equal(options.bareReadbackOnly ? bareRule(packet.desiredRule) : packet.desiredRule, filters.find(row => row[".id"] === packet.nativeId)!.rule)
assert.ok(checkpoint[0].configuration_proof); pendingPostCheckpoint = false; postCheckpointsObserved++
const sharedCheck = afterConfigurationCheckpoint; afterConfigurationCheckpoint = undefined
await sharedCheck?.(packet.operationId)
@@ -147,7 +162,8 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
if (req.method === "POST" && path === "/tool/traceroute") { observedVrfs.add(args.vrf); res.end(JSON.stringify([3, 4].map(index => ({ address: "198.51.100." + index, sent: "1", loss: "0" })))); return }
if (req.method === "PATCH") {
assert.deepEqual(Object.keys(args), ["rule"]); const target = filters.find(row => path === "/routing/filter/rule/" + row[".id"]); assert.ok(target)
writes++; if (mode !== "ignored") target.rule = args.rule
if (options.bareReadbackOnly) { assert.equal(target, filters[0]); patchBodies.push(args.rule) }
writes++; if (mode !== "ignored") target.rule = options.bareReadbackOnly ? bareRule(args.rule) : args.rule
pendingPostCheckpoint = args.rule !== initial; pendingInversePost = args.rule === initial
if (mode === "collateral") filters.find(row => row !== target)!.opaque = "changed collateral"
if (mode === "lost") { req.socket.destroy(); return }
@@ -199,7 +215,10 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
} while(!workerLocked)
await pool.query("INSERT INTO servers(id,name,host,port,use_ssl,password) VALUES($1,'scoped-route','127.0.0.1',$2,false,'NEVER_SCOPED_PASSWORD')", [sid, (router.address() as { port: number }).port])
insertedServer = true
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Scoped manual", mode: "approval", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read", "mm:network:write", "mm:filters:write"], scope: { serverIds: [sid], operationIds: ["routing.analyze", "routing.path.verify", "routing.community.switch", "routing.profiles.save", "operations.reconcile", "operations.recover"], allowGlobal: false }, limits: { callsPerMinute: 1000, mutationsPerHour: 300 } }), writer)
const operationIds = options.browserJourney
? ["routing.path.verify", "routing.community.switch", "changes.apply", "operations.read", "operations.recover"]
: ["routing.analyze", "routing.path.verify", "routing.community.switch", "routing.profiles.save", "operations.reconcile", "operations.recover"]
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Scoped manual", mode: "approval", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read", "mm:network:write", "mm:filters:write"], scope: { serverIds: [sid], operationIds, allowGlobal: false }, limits: { callsPerMinute: 1000, mutationsPerHour: 300 } }), writer)
connectionId = access.connection.id; const principal = await authenticateMcpToken(access.token)
const profile = (key: string) => routeAiProfileSchema.parse({ name: "independent " + key, maxLossPct: 5, serverId: sid, family: "ipv4", community: "65000:10", table: "tenant-" + key, probeVrf: "probe-" + key, bgpContext: { connectionId: "*c" + key, instance: "default", vrf: "main", inputChain: "custom-" + key }, candidates: [1, 9].map(value => ({ id: value === 1 ? "a" : "b", jumpHostId: sid, exitNodeId: sid, wanInterface: "wan-" + key, downstreamInterface: "hop", gateway: "192.0.2." + value, gatewayRef: { resource: "ipv4_route", id: "*r" + value }, controlProbe: { targetInterface: "target", sourceAddress: key === "a" ? "198.51.100.1" : "198.51.100.2", expectedHops: [{ serverId: sid, interface: "hop" }, { serverId: sid, interface: "target" }] } })) })
for (const key of ["a", "b"]) for (const iface of ["wan-" + key, "hop"]) {
@@ -240,6 +259,68 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
assert.ok(!["pending", "running"].includes(state), "The bounded executor must reach a terminal state before assertions or fixture cleanup")
return state
}
if (options.browserJourney) {
exposeEffectCatalogue = true
const originalFilters = structuredClone(filters)
const origin = new URL(`http://127.0.0.1:${(router.address() as { port: number }).port}`)
const checkGateway = async (expected: string) => {
const response = await fetch(new URL("/rest/routing/route", origin), { redirect: "error", signal: AbortSignal.timeout(5000) })
assert.equal(response.status, 200)
const rows = await response.json()
assert(Array.isArray(rows))
assert.equal(rows.length, 1)
assert.equal(rows[0]["routing-table"], "tenant-a")
assert.equal(rows[0].active, "true")
assert.equal(rows[0].gateway, expected)
}
return await options.browserJourney({ token: access.token, connectionId: access.connection.id, serverId: sid }, async transport => {
const approveAndApply = async (prepared: Record<string, unknown>, operationId: string) => {
const planId = prepared.planId
assert.equal(typeof planId, "string")
assert.equal(prepared.state, "awaiting-approval")
const review = await publicPlan(await loadPlan(planId as string))
assert.equal(review.operationId, operationId)
assert.equal(review.connectionId, connectionId)
assert.equal(review.approvedBy, null)
assert(!JSON.stringify(review).includes("NEVER_"))
const unchanged = { writes, requests }
await assert.rejects(transport.call("mm_change_apply", { planId, idempotencyKey: randomUUID() }), { code: "APPROVAL_REQUIRED" })
assert.deepEqual({ writes, requests }, unchanged)
await transport.approve(review)
assert.equal((await publicPlan(await loadPlan(planId as string))).approvedBy, writer.id)
const accepted = await transport.call("mm_change_apply", { planId, idempotencyKey: randomUUID() })
const id = accepted.id
assert.equal(typeof id, "string")
assert.equal(await waitForOperation(id as string), "applied")
const result = await transport.call("mm_operation_get", { operationId: id })
assert.equal(result.id, id)
assert.equal(result.state, "applied")
assert.equal(result.verification, "passed")
await transport.verifyOperation({ id: id as string, planId: planId as string })
return { id: id as string, planId: planId as string }
}
const current = profile("a")
await checkGateway("192.0.2.1")
assert.equal((await transport.call("mm_route_path_verify", { profile: current, candidateId: "b" })).status, "passed")
const forward = await approveAndApply(await transport.call("mm_route_optimizer_plan", { profile: current, candidateId: "b" }), "routing.community.switch")
assert.equal(writes, 1)
assert.notEqual(filters[0].rule, originalFilters[0].rule)
assert.deepEqual({ ...filters[0], rule: originalFilters[0].rule }, originalFilters[0])
assert.deepEqual(filters[1], originalFilters[1])
await checkGateway("192.0.2.9")
const inverse = await approveAndApply(await transport.call("mm_operation_recovery_plan", { operationId: forward.id }), "operations.recover")
assert.notEqual(inverse.planId, forward.planId)
assert.notEqual(inverse.id, forward.id)
await checkGateway("192.0.2.1")
assert.deepEqual(filters, originalFilters)
assert.deepEqual((await pool.query("SELECT * FROM filter_rules WHERE server_id=$1 ORDER BY id", [sid])).rows, dbBefore)
assert.equal(writes, 2)
assert.equal(postCheckpointsObserved, 1)
assert.equal(inversePostChecks, 1)
return { accepted: true, persistentAuthorityCases: 0, officialSdk: true, browserApprovals: 2, routerWrites: 2, syntheticGatewayRoundTrip: true,
originalFiltersRestored: true, externalNetwork: false, forwardingVerified: false }
})
}
const execute = async (planId: string, executingPrincipal: McpPrincipal = principal) => {
const op = await applyPlan(executingPrincipal, planId, randomUUID())
return { ...op, state: await waitForOperation(op.id) }
@@ -344,6 +425,63 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
} finally { await disablePersistent(profileId); controllerFailoverProbeSources = undefined; controllerFailoverReserveHealthy = true }
}
if (options.controllerFailoverOnly) return { ...await acceptControllerFailover(), persistentAuthorityCases: 0 }
if (options.bareReadbackOnly) {
const config = profile("a")
config.candidates = config.candidates.map(candidate => ({ ...candidate, gatewayRef: { resource: "gre", id: "*e" + candidate.id } }))
const persistent = await persistentFixture(config), context = { principal, connection: await effectiveMcpConnection(principal) }
const readProfile = async () => (await pool.query("SELECT enabled,state,grant_expires_at FROM mcp_routing_profiles WHERE id=$1", [persistent.id])).rows[0]
const enrolled = await readProfile(), grant = enrolled.grant_expires_at.toISOString()
const previous = await assertRoutingControllerBaseline(enrolled.state.baseline, persistent.id, persistent.version, config, grant, context)
const { plan, input } = await prepare("a", persistent, config, true)
assert.equal(originalEffectSelectionSchema.parse(input.originalEffectSelection).status, "prepared")
const forward = await execute(plan.planId)
assert.equal(forward.state, "applied")
const seal = await assertScopedCommunityRequest(input)
const checkpoint = await loadScopedConfigurationPacket(input, seal, { operationId: forward.id, stepIndex: 0 })
const network = await loadScopedNetworkCheckpoint(input, seal)
assert.equal(network?.originalEffects?.status, "observed-expected")
assert.ok(checkpoint)
assert.match(checkpoint.packet.desiredRule, / set gw-interface "gre-b";/)
assert.equal(filters[0].rule, bareRule(checkpoint.packet.desiredRule))
assert.deepEqual(patchBodies, [checkpoint.packet.desiredRule])
// Link the operation to the tick as for the existing effect fixtures.
// The worker still validates the signed plan, charge and native readback.
await pool.query("UPDATE mcp_routing_profiles SET state=state || $2::jsonb WHERE id=$1", [persistent.id, JSON.stringify({ planId: plan.planId, candidateId: "b" })])
await tickRoutingProfiles()
const published = await readProfile()
assert.equal(published.enabled, true, String(published.state.pausedReason))
assert.equal(published.state.planId, undefined)
const advanced = await assertRoutingControllerBaseline(published.state.baseline, persistent.id, persistent.version, config, grant, context)
assert.equal(advanced.packet.candidateId, "b")
assert.equal(advanced.packet.previousProof, previous.proof)
assert.notEqual(advanced.packet.targetProof, checkpoint.packet.desiredTargetProof)
assert.deepEqual(advanced.packet.transition, { planId: plan.planId, operationId: forward.id, checkpointProof: checkpoint.checkpointProof,
sourceDesiredTargetProof: checkpoint.packet.desiredTargetProof })
const inverse = await invokeMcpTool(principal, "mm_operation_recovery_plan", { operationId: forward.id }) as { planId: string }
assert.notEqual(inverse.planId, plan.planId)
await assert.rejects(applyPlan(principal, inverse.planId, randomUUID()), { code: "APPROVAL_REQUIRED" })
assert.equal(writes, 1)
await approvePlan(inverse.planId, writer, true)
const recovered = await execute(inverse.planId)
assert.equal(recovered.state, "applied")
assert.equal(filters[0].rule, bareRule(initial))
assert.equal(filters[1].rule, foreign)
assert.deepEqual(patchBodies, [checkpoint.packet.desiredRule, initial])
assert.equal(writes, 2)
const inverseIntents = (await pool.query("SELECT state,recovery_operation_id FROM mcp_scoped_recovery_intents WHERE source_operation_id=$1", [forward.id])).rows
assert.deepEqual(inverseIntents, [{ state: "verified", recovery_operation_id: recovered.id }])
assert.equal(inversePostChecks, 1)
await tickRoutingProfiles()
const paused = await readProfile()
assert.equal(paused.enabled, false)
assert.equal(paused.state.pausedReason, "STATE_CONFLICT")
assert.deepEqual(paused.state.baseline, advanced)
assert.deepEqual((await pool.query("SELECT * FROM filter_rules WHERE server_id=$1 ORDER BY id", [sid])).rows, dbBefore)
return { accepted: true, realSql: true, loopbackHttp: true, signedBaselinePublished: true,
sameSourceApprovedInverseVerified: true, controllerPausedAfterInverse: true, nativeWrites: writes }
}
if (options.originalEffectsOnly) {
const cases: Array<Record<string, unknown>> = []
let publicManualRevision2: Record<string, unknown> | undefined
@@ -1,7 +1,7 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { compileManagedFilterRules } from "../../services/filter-rules-write-model.js"
import { compileScopedCommunitySwitch, scopedConfigurationMatches } from "./routing-community-scoped-model.js"
import { compileScopedCommunitySwitch, scopedConfigurationMatches, scopedTargetRuleMatches } from "./routing-community-scoped-model.js"
const rules = [{ community: "65000:10", action: "route" as const, gateway: "192.0.2.1", outIface: "", description: "" },
{ community: "65000:20", action: "route" as const, gateway: "192.0.2.1", outIface: "", description: "" },
@@ -22,6 +22,27 @@ test("full roundtrip rejects hidden script, unknown branches, comments, invalid
assert.throws(() => compileScopedCommunitySwitch(body, "65000:30", "192.0.2.9", "", "ipv4"))
assert.throws(() => compileScopedCommunitySwitch(body, "65000:10", "192.0.2.9", 'bad";accept;', "ipv4"))
})
test("native optional quotes preserve exact managed body and existing proof format", () => {
const quoted = compileManagedFilterRules([{ ...rules[0], outIface: "marker-gre-a" }])
const bare = quoted.replace('"marker-gre-a"', "marker-gre-a")
const change = (body: string) => compileScopedCommunitySwitch(body, "65000:10", "192.0.2.9", "marker-gre-b", "ipv4")
assert.deepEqual(change(bare), change(quoted))
assert.match(change(bare).compiledBody, / set gw-interface "marker-gre-b";/)
const proof = (body: string) => JSON.stringify({ id: "*1", body })
const target = (rule: string) => ({ rawRow: { rule }, desiredTargetProof: proof })
assert.equal(scopedTargetRuleMatches(target(bare), quoted, proof(quoted)), true)
assert.equal(scopedTargetRuleMatches(target(quoted), bare, proof(bare)), true)
assert.equal(scopedTargetRuleMatches(target(bare), quoted, proof(bare)), false)
assert.equal(scopedTargetRuleMatches(target(bare), quoted, JSON.stringify({ id: "*2", body: quoted })), false)
for (const altered of [bare.replace("marker-gre-a", "marker-gre-x"), bare.replace("192.0.2.1", "192.0.2.2"),
bare.replace(" set gw-interface", " set gw-interface"), bare.replace("gw-interface", "out-interface"), bare + "\naccept;"]) {
assert.equal(scopedTargetRuleMatches(target(altered), quoted, proof(quoted)), false)
}
for (const name of ["marker:gre/a", "100", "marker gre a"]) {
const unusual = compileManagedFilterRules([{ ...rules[0], outIface: name }])
assert.equal(scopedTargetRuleMatches(target(unusual.replace('"' + name + '"', name)), unusual, proof(unusual)), false)
}
})
test("scoped configuration comparison needs dependency, native metadata and candidate identity conjunction", () => {
const input = { dependencyProof: "dependencies", originalTargetProof: "original", desiredTargetProof: "desired", candidateId: "b" }
const state = { dependencyProof: "dependencies", targetRuleProof: "desired", currentCandidateId: "b" }
@@ -1,5 +1,6 @@
import { isIP } from "node:net"
import { compileManagedFilterRules, filterRuleBranches, filterRuleToken } from "../../services/filter-rules-write-model.js"
import { managedRoutingFilterBodiesMatch } from "../../services/routing-filter-body.js"
type Rule = { community: string; action: "route" | "blackhole"; gateway: string; outIface: string; description: string }
/** Deliberately accepts only the complete compiler output, never partial script parsing. */
@@ -11,7 +12,7 @@ export function compileScopedCommunitySwitch(body: string, community: string, ga
const blackhole = /^\s*set blackhole yes;\s*accept;\s*$/.test(branch.body)
for (const match of branch.condition.matchAll(/bgp-communities includes (\d+:\d+)/g)) rules.push({ community: match[1], action: blackhole ? "blackhole" : "route", gateway: blackhole ? "" : filterRuleToken(branch.body, "gw"), outIface: blackhole ? "" : filterRuleToken(branch.body, "interface"), description: "" })
}
if (!rules.length || compileManagedFilterRules(rules) !== body || new Set(rules.map(rule => rule.community)).size !== rules.length) throw new Error("Full compiler roundtrip required")
if (!rules.length || !managedRoutingFilterBodiesMatch(body, compileManagedFilterRules(rules)) || new Set(rules.map(rule => rule.community)).size !== rules.length) throw new Error("Full compiler roundtrip required")
const selected = rules.filter(rule => rule.community === community)
if (selected.length !== 1 || selected[0].action !== "route") throw new Error("Unique routed community required")
const before = { ...selected[0] }
@@ -20,6 +21,11 @@ export function compileScopedCommunitySwitch(body: string, community: string, ga
return { compiledBody, before, proposed: desired.find(rule => rule.community === community)! }
}
export function scopedTargetRuleMatches(target: { rawRow: Record<string, unknown>; desiredTargetProof: (body: string) => string }, expectedBody: string, expectedProof: unknown) {
return typeof expectedProof === "string" && managedRoutingFilterBodiesMatch(target.rawRow.rule, expectedBody) &&
target.desiredTargetProof(expectedBody) === expectedProof
}
export function scopedConfigurationMatches(input: { dependencyProof: unknown; originalTargetProof: unknown; desiredTargetProof: unknown; candidateId: unknown }, observed: { dependencyProof: unknown; targetRuleProof: unknown; currentCandidateId: unknown }) {
const dependencies = input.dependencyProof === observed.dependencyProof
return { configurationMatchesOriginal: dependencies && input.originalTargetProof === observed.targetRuleProof,
@@ -1,3 +1,4 @@
import { withPartitionWrite } from "../../services/partition-write-transaction.js"
import { randomUUID } from "node:crypto"
import { z } from "zod"
import type { PoolClient } from "pg"
@@ -156,7 +157,7 @@ async function observe(value: Session, phase: "before" | "after", requireSla = t
const expectedBody = phase === "before" ? packet.desiredRule : packet.originalRule
const expectedProof = phase === "before" ? packet.desiredTargetProof : packet.originalTargetProof
const expectedCandidate = phase === "before" ? packet.desiredCandidateId : packet.originalCandidateId
if (context.before.dependencyProof !== packet.dependencyProof || context.before.targetRuleProof !== expectedProof || context.before.currentCandidateId !== expectedCandidate || context.ownedTarget.rawRow.rule !== expectedBody) throw fail("Конфигурация или зависимости обратного действия изменены")
if (context.before.dependencyProof !== packet.dependencyProof || context.before.currentCandidateId !== expectedCandidate || !context.ownedTarget.matchesRuleProof(expectedBody, expectedProof)) throw fail("Конфигурация или зависимости обратного действия изменены")
await context.fence(); await value.authorize()
return context
}
@@ -420,8 +421,10 @@ export function registerScopedRecoveryService() {
reconcile: async (input, authorize) => {
const value = await session(input, false, authorize, true), context = await readRouteCommunityContext(value.source.profile, value.options)
const packet = value.source.configuration.packet, dependencies = context.before.dependencyProof === packet.dependencyProof
const configurationMatchesDesired = dependencies && context.before.targetRuleProof === packet.originalTargetProof && context.before.currentCandidateId === packet.originalCandidateId
const configurationMatchesOriginal = dependencies && context.before.targetRuleProof === packet.desiredTargetProof && context.before.currentCandidateId === packet.desiredCandidateId
const target = context.ownedTarget
const sameTarget = target?.nativeId === packet.nativeId
const configurationMatchesDesired = dependencies && sameTarget && context.before.currentCandidateId === packet.originalCandidateId && target.matchesRuleProof(packet.originalRule, packet.originalTargetProof)
const configurationMatchesOriginal = dependencies && sameTarget && context.before.currentCandidateId === packet.desiredCandidateId && target.matchesRuleProof(packet.desiredRule, packet.desiredTargetProof)
const record = await verifiedRecord(input, value)
const configurationCheckpointAvailable = await configurationRecord(input, value)
const expected = record ? await recoveryResult(input, record.verifiedProof) : null
@@ -437,5 +440,5 @@ export function registerScopedRecoveryService() {
/** Restart maintenance is read-only with respect to the network. */
export async function recoverScopedRecoveryIntents() {
await pool.query("UPDATE mcp_scoped_recovery_intents r SET state='unknown',finished_at=now() WHERE r.state='intent' AND (r.deadline<=now() OR r.recovery_operation_id IS NOT NULL AND EXISTS (SELECT 1 FROM mcp_operations o WHERE o.id=r.recovery_operation_id AND o.state NOT IN ('pending','running')))")
await withPartitionWrite(({ client }) => client.query("UPDATE mcp_scoped_recovery_intents r SET state='unknown',finished_at=now() WHERE r.state='intent' AND (r.deadline<=now() OR r.recovery_operation_id IS NOT NULL AND EXISTS (SELECT 1 FROM mcp_operations o WHERE o.id=r.recovery_operation_id AND o.state NOT IN ('pending','running')))"))
}
@@ -9,12 +9,13 @@ import { pool } from "../../db/index.js"
import { hasPermission } from "../../lib/permissions.js"
import { assertVerifiedConfigurationAccess, executeGuardedConfigurationWrite } from "../../services/config-mutation-policy.js"
import { withApplicationMutationLock, withConfigMutationLock } from "../../services/config-mutation-lock.js"
import { managedRoutingFilterBodiesMatch } from "../../services/routing-filter-body.js"
import { effectiveMcpConnection, audit } from "./store.js"
import { accessGeneration, decryptSecret, encryptSecret, fingerprint, McpError, privateSecretFingerprint, redact } from "./security.js"
import { saveScopedConfigurationCheckpoint, loadScopedConfigurationCheckpoint, saveScopedNetworkCheckpoint, loadScopedNetworkCheckpoint } from "./routing-community-scoped-checkpoint.js"
import { assertScopedCommunityRequest as sealedInput, readScopedPathCheckpoint } from "./routing-community-scoped-request.js"
import { prepareManagementTransportBinding } from "./management-transport.js"
import { readRouteCommunityContext } from "./routing-community-context.js"
import { readRouteCommunityContext, type OwnedRouteCommunityTarget } from "./routing-community-context.js"
import { candidateMatchesGateway } from "./routing-community-context-model.js"
import { rankRoutingCandidates } from "./routing-model.js"
import { readRoutingObservations } from "./routing-observations.js"
@@ -84,6 +85,13 @@ async function receipt(input: Record<string, unknown>, action: string) {
await audit(pool, String(input.ownerId), action, principalSchema.parse(input.principal).connectionId, { receiptId: input.receiptId, requestSeal, revision: input.proofRevision })
}
const matches = (input: Record<string, unknown>, context: { before: Parameters<typeof scopedConfigurationMatches>[1] }) => scopedConfigurationMatches({ dependencyProof: input.dependencyProof, originalTargetProof: input.originalTargetProof, desiredTargetProof: input.desiredTargetProof, candidateId: input.candidateId }, context.before)
async function matchesDesired(input: Record<string, unknown>, context: { before: Parameters<typeof scopedConfigurationMatches>[1]; ownedTarget: Pick<OwnedRouteCommunityTarget, "nativeId" | "matchesRuleProof"> | undefined }) {
if (matches(input, context).configurationMatchesDesired) return true
const target = context.ownedTarget
return !!target && target.nativeId === input.nativeId && context.before.dependencyProof === input.dependencyProof &&
context.before.currentCandidateId === input.candidateId &&
target.matchesRuleProof(await decryptSecret(String(input.desiredRuleCiphertext)), input.desiredTargetProof)
}
async function original(input: Record<string, unknown>, verified: boolean) {
await sealedInput(input)
const value = await observe(input, undefined, verified)
@@ -125,7 +133,7 @@ export async function prepareScopedCommunitySwitch(input: Record<string, unknown
const outIface = value.candidate.gatewayRef.resource === "gre" ? String(target[0].name) : ""
if (!candidateMatchesGateway(value.profile, value.candidate, { gateway: value.candidate.gateway, gatewayInterface: outIface }, target[0])) throw fail("Gateway кандидата не соответствует exact native identity")
const compiled = compileScopedCommunitySwitch(String(owned.rawRow.rule), value.profile.community, value.candidate.gateway, outIface, value.profile.family)
if (compiled.compiledBody === owned.rawRow.rule) throw fail("Путь уже выбран")
if (managedRoutingFilterBodiesMatch(owned.rawRow.rule, compiled.compiledBody)) throw fail("Путь уже выбран")
let managementBindingCiphertext: string | null = null
const hasChannel = (await pool.query("SELECT id FROM mcp_management_channels WHERE server_id=$1 AND revoked_at IS NULL AND expires_at>now()", [profile.serverId])).rows.length > 0
if (hasChannel) {
@@ -199,7 +207,7 @@ export function registerScopedCommunityService() {
await patchScopedCommunity(owned.endpoint, owned.nativeId, fresh.compiledBody)
})
const after = await observe(input, undefined, true, undefined, "configuration")
if (!matches(input, after.context).configurationMatchesDesired) throw fail("Read-back не подтверждён")
if (!await matchesDesired(input, after.context)) throw fail("Read-back не подтверждён")
await after.context.fence()
// This commit precedes the first post-change network probe. A failed
// probe cannot erase the exact, acknowledged configuration read-back.
@@ -223,7 +231,7 @@ export function registerScopedCommunityService() {
if (effectSession && !effectSession.readBudget().closed) {
try {
const published = await readRoutingNetworkScopedCommunityContext(effectSession.sharedContext)
if (!matches(input, published).configurationMatchesDesired) throw fail("Состояние изменилось после effect-check")
if (!await matchesDesired(input, published)) throw fail("Состояние изменилось после effect-check")
await published.fence()
sharedPublication = true
} catch (error) { originalEffects = effectReadFailure("effect-publication-revalidation-unavailable", error) }
@@ -233,7 +241,7 @@ export function registerScopedCommunityService() {
// never duplicated or counted as a completed effect-owner read.
// Closed/exhausted effect owners cannot waive this fresh guard.
const final = await observe(input, undefined, true, undefined, "configuration")
if (!matches(input, final.context).configurationMatchesDesired) throw fail("Состояние изменилось после probe")
if (!await matchesDesired(input, final.context)) throw fail("Состояние изменилось после probe")
await final.context.fence()
}
if (effectSession && originalEffects) {
@@ -252,11 +260,12 @@ export function registerScopedCommunityService() {
verify: async (input, result) => {
const expected = await verifiedResult(input)
if (!expected.configurationCheckpointAvailable || !expected.controlProbeVerified || fingerprint(result) !== fingerprint(expected)) return false
return matches(input, (await observe(input, undefined, false, undefined, "configuration")).context).configurationMatchesDesired
return matchesDesired(input, (await observe(input, undefined, false, undefined, "configuration")).context)
},
reconcile: async (input, authorize) => {
await sealedInput(input); await authorize()
const observed = await observe(input, undefined, false, authorize, "configuration"), config = matches(input, observed.context)
const observed = await observe(input, undefined, false, authorize, "configuration")
const config = { ...matches(input, observed.context), configurationMatchesDesired: await matchesDesired(input, observed.context) }
await observed.context.fence(); await authorize()
const expected = await verifiedResult(input)
const checkpoints = (await pool.query("SELECT s.result FROM mcp_operation_steps s JOIN mcp_operations o ON o.id=s.operation_id WHERE o.connection_id=$1 AND s.state='verified' AND s.intended_state=$2::jsonb AND s.result=$3::jsonb LIMIT 2", [principalSchema.parse(input.principal).connectionId, JSON.stringify(redact({ kind: "service", service: "routing.community.scoped", input })), JSON.stringify(expected)])).rows
@@ -12,7 +12,7 @@ async function fixture() {
const input: Record<string, unknown> = { profile: { autoRecovery: false }, persistentProfileAuthority: authority, nativeId: "*1", originalCandidateId: "a", originalTargetProof: "target-a", dependencyProof: "dep", contextSources: enrolled.sourceProofs }
const prepared: Record<string, unknown> = { ...input, desiredCandidateId: "b" }
const packet: Record<string, unknown> = { nativeId: "*1", originalCandidateId: "a", desiredCandidateId: "b", originalTargetProof: "target-a", desiredTargetProof: "target-b", dependencyProof: "dep", contextSources: enrolled.sourceProofs }
const source = { row: { id: "source-op" }, input, configuration: { checkpointProof: "configuration-proof", packet }, sourceProof: "source-proof" }
const source = { row: { id: "source-op", source_plan_id: "source-plan" }, input, configuration: { checkpointProof: "configuration-proof", packet }, sourceProof: "source-proof" }
const row = { id: "profile", version: 1, connection_id: "connection", grant_expires_at: new Date(Date.now() + 600000), config: {}, state: { baseline: { proof: "inert-authenticated-baseline" } } }
const metadata = { operationName: "routing.community.switch", operationVersion: 7, serviceName: "routing.community.scoped", phase: "forward", stepIndex: 0, ownerId: "owner", serviceInput: input, principal: {} }
let nativeWrites = 0
@@ -55,10 +55,35 @@ for (const [name, key, bad] of drifts) test(`scoped ${name} drift is refused bef
test("main fixed loaded original identity is admitted", async () => { const value = await fixture(); value.metadata.serviceName = "routing.community"; await value.run() })
for (const [name, key, bad] of drifts) test(`main decrypted ${name} drift is refused before native`, async () => { const value = await fixture(); value.metadata.serviceName = "routing.community"; value.prepared[key] = bad; await assert.rejects(value.run); assert.equal(value.writes(), 0) })
test("inverse with enrolled A awaiting its own B transition is admitted", async () => { const value = await fixture(); value.recovery(); await value.run() })
test("inverse from this exact source advanced B baseline is admitted", async () => { const value = await fixture(); value.recovery(); Object.assign(value.enrolled, { candidateId: "b", targetProof: "target-b", transition: { operationId: "source-op", checkpointProof: "configuration-proof" } }); await value.run() })
test("inverse with unrelated B source transition is refused", async () => { const value = await fixture(); value.recovery(); Object.assign(value.enrolled, { candidateId: "b", targetProof: "target-b", transition: { operationId: "different-source", checkpointProof: "configuration-proof" } }); await assert.rejects(value.run); assert.equal(value.writes(), 0) })
const sourceTransition = { operationId: "source-op", planId: "source-plan", checkpointProof: "configuration-proof" }
test("inverse from this exact source advanced B baseline is admitted", async () => { const value = await fixture(); value.recovery(); Object.assign(value.enrolled, { candidateId: "b", targetProof: "target-b", transition: sourceTransition }); await value.run() })
test("inverse from this source can bind a separately authenticated B readback seal", async () => {
const value = await fixture()
value.recovery()
Object.assign(value.enrolled, { candidateId: "b", targetProof: "observed-b", transition: { ...sourceTransition, sourceDesiredTargetProof: "target-b" } })
await value.run()
assert.equal(value.writes(), 1)
})
for (const [key, wrong] of Object.entries({ operationId: "other-source", planId: "other-plan", checkpointProof: "other-checkpoint", sourceDesiredTargetProof: "other-desired" })) {
test(`inverse from observed B refuses mismatched ${key}`, async () => {
const value = await fixture()
value.recovery()
Object.assign(value.enrolled, { candidateId: "b", targetProof: "observed-b", transition: { ...sourceTransition, sourceDesiredTargetProof: "target-b", [key]: wrong } })
await assert.rejects(value.run)
assert.equal(value.writes(), 0)
})
}
test("new forward source must match the enrolled observed seal", async () => {
const value = await fixture()
Object.assign(value.enrolled, { candidateId: "b", targetProof: "observed-b", transition: { ...sourceTransition, sourceDesiredTargetProof: "target-b" } })
Object.assign(value.input, { originalCandidateId: "b", originalTargetProof: "target-b" })
await assert.rejects(value.run)
assert.equal(value.writes(), 0)
value.input.originalTargetProof = "observed-b"
await value.run()
})
test("inverse with B but no authenticated transition is refused", async () => { const value = await fixture(); value.recovery(); Object.assign(value.enrolled, { candidateId: "b", targetProof: "target-b" }); await assert.rejects(value.run) })
test("inverse with another configuration checkpoint is refused", async () => { const value = await fixture(); value.recovery(); Object.assign(value.enrolled, { candidateId: "b", targetProof: "target-b", transition: { operationId: "source-op", checkpointProof: "other-checkpoint" } }); await assert.rejects(value.run) })
test("inverse with another configuration checkpoint is refused", async () => { const value = await fixture(); value.recovery(); Object.assign(value.enrolled, { candidateId: "b", targetProof: "target-b", transition: { ...sourceTransition, checkpointProof: "other-checkpoint" } }); await assert.rejects(value.run) })
test("inverse with external C baseline is refused", async () => { const value = await fixture(); value.recovery(); value.enrolled.candidateId = "c"; await assert.rejects(value.run) })
test("inverse with another authenticated source identity is refused", async () => { const value = await fixture(); value.recovery(); value.metadata.serviceInput.sourceProof = "other"; await assert.rejects(value.run) })
test("inverse with source dependency drift is refused", async () => { const value = await fixture(); value.recovery(); value.source.configuration.packet.dependencyProof = "other"; await assert.rejects(value.run) })
@@ -49,8 +49,8 @@ export async function assertActiveRoutingProfileWriter(serverId:number) {
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}) =>
identity.nativeId===enrolled.nativeId && identity.candidateId===enrolled.candidateId && identity.targetProof===enrolled.targetProof &&
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)
@@ -68,8 +68,10 @@ export async function assertActiveRoutingProfileWriter(serverId:number) {
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 ownDesired=matches({...common,candidateId:packet.desiredCandidateId,targetProof:packet.desiredTargetProof}) &&
enrolled.transition?.operationId===recoverySource.row.id && enrolled.transition?.checkpointProof===recoverySource.configuration.checkpointProof
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()
}
}
@@ -162,7 +162,7 @@ export async function acceptMainBaselineRuntime(actor: AuthUser) {
await pool.query("INSERT INTO mcp_routing_observations(id,src_server_id,dst_server_id,src_interface,dst_interface,family,routing_table,ping_ms,loss_pct,dl_mbps,ul_mbps,ping_at,bandwidth_at,context_verified,source) VALUES($1,$2,$2,$3,'peer','ipv4','main',1,0,100,100,now(),now(),true,'fixture')", [observationId, sid, iface])
await attestRoutingFixture(observationId)
}
const writeAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Main Route context write", mode: "approval", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read", "mm:network:write", "mm:filters:write"], scope: { serverIds: [sid], operationIds: ["routing.analyze", "routing.community.switch", "routing.profiles.save", "routing.profiles.read", "routing.path.verify"], allowGlobal: false }, limits: { callsPerMinute: 120 } }), writerActor)
const writeAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Main Route context write", mode: "approval", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read", "mm:network:write", "mm:filters:write"], scope: { serverIds: [sid], operationIds: ["routing.analyze", "routing.community.switch", "routing.profiles.save", "routing.profiles.read", "routing.controller.read", "routing.controller.update", "routing.path.verify"], allowGlobal: false }, limits: { callsPerMinute: 120 } }), writerActor)
writeConnectionId = writeAccess.connection.id
const writer = await authenticateMcpToken(writeAccess.token)
const automatic = routeAiProfileSchema.parse({ ...mainProfile, autoRecovery: true, candidates: mainProfile.candidates.map(candidate => ({ ...candidate, controlProbe: base.candidates[0].controlProbe })) })
@@ -226,6 +226,22 @@ export async function acceptMainBaselineRuntime(actor: AuthUser) {
console.log("TARGETED_PUBLIC_OVERLAP_TOCTOU_NATIVE0_PASS",JSON.stringify({newProfilesApprovedBeforeEitherEnabled:2,firstActivationApplied:true,secondTerminalCode:overlapTerminal.error_code,secondCasRows:0,enabledProfiles:1,newPrepareRefused:true,nativeWrites:0}))
const publicProfile=await invokeMcpTool(writer,"mm_route_profiles_list",{})
assert.ok(!JSON.stringify(publicProfile).includes(controller.state.baseline.proof),"Public profile DTO must omit baseline proof")
const beforeControllerRead = requests
const controllerView = await invokeMcpTool(writer, "mm_route_controller_get", { profileId: controller.id }) as { id: string; enabled: boolean; state: Record<string, unknown> }
assert.equal(controllerView.id, controller.id)
assert.equal(controllerView.enabled, true)
assert.equal(controllerView.state.baseline, undefined)
const hiddenConfig = { ...mainProfile, candidates: mainProfile.candidates.map(candidate => ({ ...candidate, exitNodeId: sid + 1000 })) }
await pool.query("UPDATE mcp_routing_profiles SET config=$2::jsonb WHERE id=$1", [controller.id, JSON.stringify(hiddenConfig)])
try {
const notFound = (error: unknown) => error instanceof Error && "code" in error && error.code === "NOT_FOUND"
await assert.rejects(invokeMcpTool(writer, "mm_route_controller_get", { profileId: controller.id }), notFound)
await assert.rejects(invokeMcpTool(writer, "mm_route_profile_save_plan", { profileId: controller.id, profile: mainProfile, enabled: false, expiresAt: activation.grantExpiresAt }), notFound)
} finally {
await pool.query("UPDATE mcp_routing_profiles SET config=$2::jsonb WHERE id=$1", [controller.id, JSON.stringify(mainProfile)])
}
assert.equal(requests, beforeControllerRead)
console.log("✓ Route AI controller: exact public state, hidden dependencies reject read and replacement before RouterOS")
// Explicit out-of-application drift after enrollment, before a trusted
// private host prepares another persistent plan. MAC/authority stay valid.
policyComment=legacyComment+"-external-drift"
@@ -302,7 +318,8 @@ export async function acceptMainBaselineRuntime(actor: AuthUser) {
await assert.rejects(client.patch("/routing/filter/rule/*f",{rule:body}))
assert.equal(writes,beforeDenied,"Active controller refuses foreign native write before I/O")
// Disable through the same public human save/CAS service, not direct SQL.
const disable=await invokeMcpTool(writer,"mm_route_profile_save_plan",{profileId:controller.id,profile:mainProfile,enabled:false,expiresAt:activation.grantExpiresAt}) as {planId:string}
await assert.rejects(invokeMcpTool(writer, "mm_route_controller_update_plan", { profileId: randomUUID(), profile: mainProfile, enabled: false, expiresAt: activation.grantExpiresAt }), (error: unknown) => error instanceof Error && "code" in error && error.code === "NOT_FOUND")
const disable=await invokeMcpTool(writer,"mm_route_controller_update_plan",{profileId:controller.id,profile:mainProfile,enabled:false,expiresAt:activation.grantExpiresAt}) as {planId:string}
await approvePlan(disable.planId,writerActor,true)
assert.equal((await waitOperation((await applyPlan(writer,disable.planId,"runtime-main-baseline-disable")).id)).state,"applied")
// Native writer owns the REAL server lock first. Approved activation cannot
@@ -0,0 +1,142 @@
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { mkdtemp, readFile, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { basename, dirname, join, resolve } from "node:path"
import { test, type TestContext } from "node:test"
import ts from "typescript"
import { z } from "zod"
import { scopedTargetRuleMatches } from "./routing-community-scoped-model.js"
import * as security from "./security.js"
import type * as Baseline from "./routing-controller-baseline.js"
// Run the actual baseline implementation and durable HMAC on a temporary key.
// Only the SQL/RouterOS/authority adapters supply synthetic snapshots.
async function fixture(t: TestContext) {
const root = resolve(tmpdir()), prefix = "mcp-controller-baseline-test-"
const state = await mkdtemp(join(root, prefix)), previous = process.env.MCP_STATE_DIR
process.env.MCP_STATE_DIR = state
t.after(async () => {
if (previous === undefined) delete process.env.MCP_STATE_DIR
else process.env.MCP_STATE_DIR = previous
assert.equal(dirname(resolve(state)), root)
assert(basename(state).startsWith(prefix))
await rm(state, { recursive: true })
})
const { fingerprint, privateSecretFingerprint } = security
const now = Date.now(), profileId = randomUUID(), planId = randomUUID(), operationId = randomUUID(), connectionId = randomUUID()
const grant = new Date(now + 120000).toISOString()
const principal = { connectionId, credentialId: randomUUID(), credentialExpiresAt: grant }
const profile = { serverId: 1, table: "tenant", probeVrf: "tenant", candidates: ["a", "b"].map(id => ({ id, jumpHostId: 1, exitNodeId: 1 })) }
const permissions = ["mm:network:read", "mm:network:write", "mm:filters:write"]
const connection = { id: connectionId, owner_id: "owner", generation: await security.accessGeneration(),
owner_valid_until: new Date(grant), owner_claims_issued_at: new Date(now), owner_permissions: permissions,
policy: { status: "active", mode: "operator", expiresAt: grant, permissions,
scope: { serverIds: [1], operationIds: ["routing.analyze", "routing.community.switch", "routing.path.verify"] } } }
const context = { principal, connection } as Parameters<typeof Baseline.prepareRoutingControllerBaseline>[4]
const originalRule = 'if (bgp-communities includes 65000:10) {\n set gw 192.0.2.1;\n set gw-interface "marker-gre-a";\n accept;\n}'
const desiredRule = originalRule.replace("192.0.2.1", "192.0.2.9").replace("marker-gre-a", "marker-gre-b")
const bareRule = desiredRule.replace('"marker-gre-b"', "marker-gre-b")
const signTarget = await security.privateFingerprinter("baseline-test-native-target")
const stableTarget = { ".id": "*1", chain: "input", comment: "managed" }
let observed = { ...stableTarget, rule: originalRule }, candidate = "a", failAfterRead = false, driftSourceAtFence = false
const targetProof = (rule: string) => signTarget(fingerprint({ ...observed, rule }))
const desiredTargetProof = targetProof(desiredRule), dependencyProof = "d".repeat(64), checkpointProof = "c".repeat(64)
const sourceProofs = [{ serverId: 1, proof: "a".repeat(64) }]
const receiptId = randomUUID(), requestSeal = "request-proof", networkSeal = "network-proof"
const input = { proofRevision: 3, profile, principal, ownerId: connection.owner_id, nativeId: "*1", originalCandidateId: "a",
originalTargetProof: targetProof(originalRule), desiredTargetProof, dependencyProof, contextSources: sourceProofs, candidateId: "b",
persistentProfileAuthority: { packet: { profileId, profileVersion: 1 } }, receiptId }
const step = { kind: "service", service: "routing.community.scoped", input }
const configuration = { checkpointProof, observedAt: new Date(now-100).toISOString(), packet: { revision: 2, operationId, stepIndex: 0,
nativeId: "*1", originalCandidateId: "a", originalTargetProof: input.originalTargetProof, desiredCandidateId: "b", desiredTargetProof,
dependencyProof, contextSources: sourceProofs, desiredRule } }
const nativeVerificationSeal = await privateSecretFingerprint(fingerprint({ request: requestSeal, dependency: dependencyProof, desired: desiredTargetProof,
candidate: "b", configuration: checkpointProof, network: networkSeal }), "route-ai-scoped-verified-v3")
const row = { id: operationId, plan_id: planId, state: "applied", step_state: "verified", operation_id: "routing.community.switch",
operation_version: 7, approved_by: "owner", created_at: new Date(now-200), intended_state: security.redact(step),
evidence: { sealed: await security.encryptSecret(JSON.stringify({ steps: [step], target: { scope: "servers", serverIds: [1] } })) },
result: { nativeVerificationSeal, configurationCheckpointProof: checkpointProof, networkCheckpointProof: networkSeal, receiptId,
proofRevision: 3, nativeWrites: 1, configurationVerified: true, postProbeStatus: "passed", controlProbeVerified: true } }
const providers: Record<string, unknown> = {
"./routing-main-transition.js": {},
"../../db/index.js": { pool: { query: async (_sql: string, ids: string[]) => {
assert.deepEqual(ids, [operationId, planId, connectionId])
return { rows: [failAfterRead ? { ...row, step_state: "unknown" } : row] }
} } },
"./registry.js": { findMcpOperation: () => ({ version: 7 }) },
"./routing-community-scoped-request.js": { assertScopedCommunityRequest: async () => requestSeal },
"./routing-profile-authority.js": { assertPersistentScopedProfileAuthority: async () => {},
routingProfileAuthorityBindingSchema: { parse: (value: unknown) => value } },
"./routing-community-scoped-checkpoint.js": { loadScopedConfigurationPacket: async () => configuration,
loadScopedNetworkCheckpoint: async () => ({ status: "passed", networkSeal }) },
"./routing-original-effects.js": {},
zod: { z }, "@mmapp/contracts/mcp-routing": { routeAiProfileSchema: { parse: (value: unknown) => value } },
"../../lib/permissions.js": { hasPermission: (set: string[], permission: string) => set.includes(permission) },
"./store.js": { effectiveMcpConnection: async () => connection },
"./security.js": security,
"./routing-community-context.js": { readRouteCommunityContext: async (_profile: unknown, options: { authorize(): Promise<void> }) => {
await options.authorize()
const target = { nativeId: observed[".id"], rawRow: observed, desiredTargetProof: targetProof }
return { before: { nativeRuleId: target.nativeId, scopedExecutionAvailable: true, currentCandidateId: candidate,
targetRuleProof: targetProof(observed.rule), dependencyProof, sourceProofs },
ownedTarget: { ...target, matchesRuleProof: (body: string, proof: unknown) => scopedTargetRuleMatches(target, body, proof) },
fence: async () => { await options.authorize(); if (driftSourceAtFence) failAfterRead = true } }
} },
}
const source = await readFile(new URL("./routing-controller-baseline.ts", import.meta.url), "utf8")
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText
const baseline = {} as typeof Baseline
new Function("exports", "require", compiled)(baseline, (name: string) => { assert(providers[name], name); return providers[name] })
const original = await baseline.prepareRoutingControllerBaseline(profileId, 1, profile, grant, context)
const links = { planId, operationId, checkpointProof }
return { original, links, desiredTargetProof, targetProof, originalRule, desiredRule, bareRule, baseline,
advance: () => baseline.advanceRoutingControllerBaseline(original, profileId, 1, profile, grant, context, planId, operationId),
assertCurrent: (value: unknown) => baseline.assertRoutingControllerBaseline(value, profileId, 1, profile, grant, context),
driftAfterNativeRead: () => { driftSourceAtFence = true },
observe: (rule: string, metadata: Partial<typeof stableTarget> = {}, selected = "b") => { candidate = selected; observed = { ...stableTarget, ...metadata, rule } } }
}
test("controller signs an exact legacy-shaped next baseline when native target proof matches", async t => {
const f = await fixture(t)
await f.assertCurrent(f.original)
f.observe(f.desiredRule)
const next = await f.advance()
assert.equal(next.packet.targetProof, f.desiredTargetProof)
assert.deepEqual(next.packet.transition, f.links)
assert.equal(next.packet.previousProof, f.original.proof)
assert.deepEqual(await f.assertCurrent(next), next)
})
test("controller authenticates the source but enrolls the exact bare RouterOS readback", async t => {
const f = await fixture(t)
f.observe(f.bareRule)
const next = await f.advance()
assert.equal(next.packet.targetProof, f.targetProof(f.bareRule))
assert.notEqual(next.packet.targetProof, f.desiredTargetProof)
assert.deepEqual(next.packet.transition, { ...f.links, sourceDesiredTargetProof: f.desiredTargetProof })
assert.deepEqual(await f.assertCurrent(next), next)
const tampered = structuredClone(next)
tampered.packet.transition!.sourceDesiredTargetProof = "e".repeat(64)
await assert.rejects(f.baseline.routingControllerBaselinePrincipal(tampered), { code: "STATE_CONFLICT" })
f.observe(f.desiredRule)
await assert.rejects(f.assertCurrent(next), { code: "STATE_CONFLICT" })
f.observe(f.originalRule, {}, "a")
await assert.rejects(f.assertCurrent(next), { code: "STATE_CONFLICT" })
await f.assertCurrent(f.original)
})
test("controller refuses changed selected rule and drift in other native row fields", async t => {
const f = await fixture(t)
f.observe(f.bareRule.replace("marker-gre-b", "other"))
await assert.rejects(f.advance(), { code: "STATE_CONFLICT" })
f.observe(f.bareRule, { comment: "changed" })
await assert.rejects(f.advance(), { code: "STATE_CONFLICT" })
})
test("controller refuses baseline publication if the source operation changes after readback", async t => {
const f = await fixture(t)
f.observe(f.bareRule)
f.driftAfterNativeRead()
await assert.rejects(f.advance(), { code: "STATE_CONFLICT" })
})
@@ -23,7 +23,8 @@ const packetSchema = z.object({
nativeId: z.string().regex(/^\*[a-z0-9]+$/i), candidateId: z.string().min(1),
targetProof: proof, dependencyProof: proof,
sourceProofs: z.array(z.object({ serverId: z.number().int().positive(), proof }).strict()).min(1).max(1000),
observedAt: z.string().datetime(), previousProof: proof.optional(), transition: z.object({ planId: z.string().uuid(), operationId: z.string().uuid(), checkpointProof: proof }).strict().optional(),
observedAt: z.string().datetime(), previousProof: proof.optional(),
transition: z.object({ planId: z.string().uuid(), operationId: z.string().uuid(), checkpointProof: proof, sourceDesiredTargetProof: proof.optional() }).strict().optional(),
}).strict()
const baselineSchema = z.object({ packet: packetSchema, proof }).strict()
export type RoutingControllerBaseline = z.infer<typeof baselineSchema>
@@ -186,14 +187,18 @@ export async function advanceRoutingControllerBaseline(rawBaseline: unknown, pro
result.networkCheckpointProof !== network.networkSeal || result.receiptId !== input.receiptId || result.proofRevision !== input.proofRevision ||
result.nativeWrites !== 1 || result.configurationVerified !== true || result.postProbeStatus !== "passed" || result.controlProbeVerified !== true) throw fail()
const native = await readRouteCommunityContext(profile, { authorize, signal: context.signal, expectedSources: original.sourceProofs, selectedCandidateId: packet.desiredCandidateId })
if (!supportedExecution(profile,native.before) || native.ownedTarget?.nativeId !== original.nativeId || native.ownedTarget?.rawRow.rule !== packet.desiredRule ||
native.before.currentCandidateId !== packet.desiredCandidateId || native.before.targetRuleProof !== packet.desiredTargetProof ||
const targetProof = native.before.targetRuleProof
if (!supportedExecution(profile,native.before) || native.ownedTarget?.nativeId !== original.nativeId || native.before.nativeRuleId !== original.nativeId ||
!targetProof || !native.ownedTarget.matchesRuleProof(packet.desiredRule, packet.desiredTargetProof) || native.before.currentCandidateId !== packet.desiredCandidateId ||
native.before.dependencyProof !== original.dependencyProof || fingerprint(native.before.sourceProofs) !== fingerprint(original.sourceProofs)) throw fail()
await native.fence(); await authorize()
await assertPersistentScopedProfileAuthority(input, context.signal)
if (fingerprint(await readSource()) !== sourceIdentity) throw fail()
const next = packetSchema.parse({ ...original, candidateId: packet.desiredCandidateId, targetProof: packet.desiredTargetProof,
observedAt: new Date().toISOString(), previousProof: value.proof, transition: { planId, operationId, checkpointProof: configuration.checkpointProof } })
// Enroll the exact observed native seal for the next tick. The source seal
// still identifies this operation and its inverse when RouterOS omits quotes.
const next = packetSchema.parse({ ...original, candidateId: packet.desiredCandidateId, targetProof,
observedAt: new Date().toISOString(), previousProof: value.proof, transition: { planId, operationId, checkpointProof: configuration.checkpointProof,
...(targetProof !== packet.desiredTargetProof ? { sourceDesiredTargetProof: packet.desiredTargetProof } : {}) } })
return { packet: next, proof: await sign(next) }
}
+112 -30
View File
@@ -4,7 +4,8 @@ import { z } from "zod"
import { dedupeVrfAliasSnapshots } from "../../services/vrf-production-action-binding.js"
import { pool } from "../../db/index.js"
import { hasManagedCommentPrefix } from "../../managed-markers.js"
import { readRosSnapshot, routerClient, type ConfigurationRow } from "./resources.js"
import { configurationRows, readRosSnapshot, readRoutingPolicySnapshot, resourceById, routerClient, rosSnapshotSourceProof, type ConfigurationRow, type RosSnapshot } from "./resources.js"
import { readRouterOsSnapshotGet } from "../../services/routeros-snapshot-read.js"
import { McpError, fingerprint } from "./security.js"
import { effectiveMcpConnection } from "./store.js"
import { hasPermission } from "../../lib/permissions.js"
@@ -19,7 +20,7 @@ import { readBgpSessionsForServer, readBgpCatalogForServer, readRawBgpSessionsFo
import { pollGreCatalog } from "../../services/gre-catalog-poll.js"
import { fetchServerFilters, readFilterRulesCatalog } from "../../routes/filters.js"
import { readRecursiveCatalogForServer, readRecursiveGatewayCatalogForServer, readRecursiveRoutesForServer, readRecursiveGatewaysForServer } from "../../routes/recursive-routes.js"
import { analyzeEcmpPreflight, preflightPaths, preflightObservation, type PreflightObservation, type PreflightSource } from "./ecmp-preflight.js"
import { analyzeEcmpPreflight, confirmedEcmpInterfaces, confirmedEcmpTable, duplicateEcmpAdjacency, ecmpAdjacency, preflightPaths, preflightObservation, type PreflightObservation, type PreflightSource } from "./ecmp-preflight.js"
import { readLatestInternetPathSnapshot } from "../../services/internet-path-read.js"
import { projectInternetPathObservation } from "../../services/internet-path-observation.js"
import { readOspfOptimizationPreview } from "../../routes/ospf.js"
@@ -27,6 +28,35 @@ import { readOspfOptimizationPreview } from "../../routes/ospf.js"
const serverId = z.number().int().positive(), objectId = z.string().min(1).max(100)
const text = z.string().min(1).max(100), family = z.enum(["ipv4", "ipv6"])
const bool = (value: unknown) => value === true || value === "true" || value === "yes"
export function checkRpfPolicy(serverId: number, snapshots: RosSnapshot[], mode: string) {
const find = (resource: string) => snapshots.find(snapshot => snapshot.serverId === serverId && snapshot.resource === resource)
const tables = find("routing_table"), vrfs = find("vrf"), rules = find("routing_policy:routing_rule"), mangle = find("routing_policy:ip_mangle")
if (!tables || !vrfs || !rules || !mangle || !find("ipv4_route")) {
throw new McpError("PLAN_EXPIRED", "План не содержит актуальные зависимости RPF; подготовьте его заново", 409)
}
const policyRouting = rules.rows.some(row => !bool(row.disabled))
|| mangle.rows.some(row => !bool(row.disabled) && (row.action === "mark-routing" || typeof row.action !== "string" || !row.action.trim()))
const separateTables = [...tables.rows, ...vrfs.rows].some(row => row.name !== "main")
if (mode === "strict" && (policyRouting || separateTables)) throw new McpError("STATE_CONFLICT", "Strict RPF требует main без отдельных таблиц, VRF и действующих правил policy routing", 409)
return policyRouting || separateTables
}
export async function readRpfDependencies(serverId: number, mode: string, sourceProof: string | undefined) {
if (!sourceProof) throw new McpError("STATE_CONFLICT", "Источник IPv4-настроек RPF не подтверждён", 409)
const [tables, vrfs, routes] = await Promise.all(
["routing_table", "vrf", "ipv4_route"].map(resource => readRosSnapshot(serverId, resource, undefined, sourceProof)),
)
const snapshots = [tables, vrfs, routes]
const warnings = ["RPF является IPv4-настройкой. Изменение может затронуть асимметричный трафик; снижение защиты требует компенсирующих правил."]
if (mode !== "no") {
const [rules, mangle] = await Promise.all(["routing_rule", "ip_mangle"].map(resource => readRoutingPolicySnapshot(serverId, resource, sourceProof)))
snapshots.push(rules, mangle)
if (checkRpfPolicy(serverId, snapshots, mode)) warnings.push("Обнаружена policy routing или отдельные таблицы. Проверьте обратный путь для трафика в каждой затронутой таблице.")
}
return { snapshots, warnings }
}
export function registerRoutingControls(register: typeof add) {
register("routing.ospf.application.preview","mm_ospf_application_preview","Модельный OSPF preview приложения с отдельными наблюдаемыми метриками","network","mm:network:read","read","servers",z.object({serverId:z.number().int().positive().safe(),pingWeight:z.number().int().min(0).max(100).default(60),peerServerIds:z.array(z.number().int().positive().safe()).max(10).refine(ids=>new Set(ids).size===ids.length).optional(),offset:z.number().int().min(0).max(100000).default(0),limit:z.number().int().min(1).max(200).default(100)}).strict(),{
read:async(input,context)=>{
@@ -95,11 +125,16 @@ export function registerRoutingControls(register: typeof add) {
register("routing.ecmp.preflight.read", "mm_ecmp_preflight_get", "Предварительная проверка ECMP: NAT/FastTrack/RPF/маркировки без записи", "network", "mm:network:read", "read", "servers", z.object({ serverId, family, routeIds: z.array(objectId).min(2).max(8).refine(ids => new Set(ids).size === ids.length) }).strict(), {
read: async input => {
const { client } = await routerClient(Number(input.serverId)), selectedFamily = input.family as "ipv4" | "ipv6"
const entries = await Promise.all(Object.entries(preflightPaths(selectedFamily)).map(async ([key, path]) => {
try { return [key, preflightObservation(await client.get<unknown>(path), key === "settings" || key === "connectionTracking")] as const }
catch { return [key, { rows: [], error: "Устройство не подтвердило чтение ресурса" }] as const }
}))
return { serverId: input.serverId, observedAt: new Date().toISOString(), ...analyzeEcmpPreflight(selectedFamily, input.routeIds as string[], Object.fromEntries(entries) as Record<PreflightSource, PreflightObservation>) }
const read = async (path: string, singleton = false): Promise<PreflightObservation> => {
try { return preflightObservation(await client.get<unknown>(path), singleton) }
catch { return { rows: [], error: "Устройство не подтвердило чтение ресурса" } }
}
const entries = await Promise.all(Object.entries(preflightPaths(selectedFamily)).map(async ([key, path]) => [key, await read(path, key === "settings" || key === "connectionTracking")] as const))
const observed = Object.fromEntries(entries) as Record<PreflightSource, PreflightObservation>
const ids = input.routeIds as string[]
const needsInventory = observed.routes.rows.some(row => ids.includes(String(row[".id"])) && typeof row["immediate-gw"] === "string" && row["immediate-gw"].length > 0 && !ecmpAdjacency(row["immediate-gw"]))
const inventory = needsInventory ? await read("/interface") : undefined
return { serverId: input.serverId, observedAt: new Date().toISOString(), ...analyzeEcmpPreflight(selectedFamily, ids, observed, inventory) }
},
})
const recursivePaging = z.object({ serverId, offset: z.number().int().min(0).max(100000).default(0), limit: z.number().int().min(1).max(200).default(100) }).strict()
@@ -202,44 +237,91 @@ export function registerRoutingControls(register: typeof add) {
},
})
register("routing.ecmp.update", "mm_ecmp_plan", "Равная стоимость маршрутов ECMP и hash policy", "network", "mm:network:write", "write", "servers", z.object({ serverId, family, routeIds: z.array(objectId).min(2).max(8).refine(ids => new Set(ids).size === ids.length), distance: z.number().int().min(1).max(255).default(1), hashPolicy: z.enum(["l3", "l4", "l3-inner"]).optional() }).strict(), {
version: 2,
version: 7,
prepare: async input => {
const sid = Number(input.serverId), resource = input.family === "ipv4" ? "ipv4_route" : "ipv6_route"
const { client } = await routerClient(sid), snapshot = await readRosSnapshot(sid, resource), ids = input.routeIds as string[]
const raw = await client.get<ConfigurationRow[]>(input.family === "ipv4" ? "/ip/route" : "/ipv6/route")
const sid = Number(input.serverId), ipv4 = input.family === "ipv4", resource = ipv4 ? "ipv4_route" : "ipv6_route"
const base = ipv4 ? "ip" : "ipv6", settingsResource = `${base}_settings`
const { client, server } = await routerClient(sid)
const sourceProof = await rosSnapshotSourceProof(server)
const assertSource = async () => {
if (await rosSnapshotSourceProof((await routerClient(sid)).server) !== sourceProof) {
throw new McpError("STATE_CONFLICT", "Источник маршрутов ECMP изменился при подготовке", 409)
}
}
const read = (resourceId: string) => readRosSnapshot(sid, resourceId, undefined, sourceProof)
const readNative = async (path: string) => {
const value = await readRouterOsSnapshotGet<unknown>({ client, path, assertCurrent: assertSource })
await assertSource()
return preflightObservation(value)
}
const snapshot = await read(resource), ids = input.routeIds as string[]
const observed = await readNative(`/${base}/route`)
if (observed.error) throw new McpError("MEASUREMENT_UNKNOWN", observed.error, 409)
if (fingerprint(configurationRows(resourceById(resource), observed.rows)) !== snapshot.fingerprint) {
throw new McpError("STATE_CONFLICT", "Каталог маршрутов ECMP изменился при подготовке", 409)
}
const raw = observed.rows
const rows = ids.map(id => raw.find(row => row[".id"] === id))
if (rows.some(row => !row || bool(row.dynamic) || bool(row.disabled) || bool(row.invalid) || !hasManagedCommentPrefix(String(row.comment ?? "")))) throw new McpError("PROTECTED_OBJECT", "ECMP требует разрешаемые статические маршруты приложения")
const routes = rows as ConfigurationRow[], first = routes[0]
if (routes.some(row => row["dst-address"] !== first["dst-address"] || String(row["routing-table"] ?? "main") !== String(first["routing-table"] ?? "main"))) throw new McpError("STATE_CONFLICT", "ECMP требует одинаковые prefix и routing table", 409)
if (new Set(routes.map(row => String(row.gateway))).size !== ids.length || routes.some(row => !row.gateway || !row["immediate-gw"])) throw new McpError("MEASUREMENT_UNKNOWN", "Нужны разные подтверждённые next-hop и immediate gateway", 409)
const gateways = routes.map(row => row.gateway)
if (gateways.some(gateway => typeof gateway !== "string" || !gateway.trim()) || new Set(gateways).size !== ids.length) throw new McpError("MEASUREMENT_UNKNOWN", "Нужны разные подтверждённые gateway", 409)
const immediate = routes.map(row => row["immediate-gw"])
let hops = immediate.map(value => ecmpAdjacency(value))
let interfaceIds: Map<string, string> | undefined
if (hops.some(hop => hop === null)) {
const inventory = await readNative("/interface")
if (inventory.error) throw new McpError("MEASUREMENT_UNKNOWN", inventory.error, 409)
interfaceIds = confirmedEcmpInterfaces(inventory.rows)
const names = new Set(interfaceIds.keys())
hops = immediate.map(value => ecmpAdjacency(value, names))
}
if (hops.some(hop => hop === null)) throw new McpError("MEASUREMENT_UNKNOWN", "Не подтверждён одиночный immediate gateway каждого маршрута", 409)
if (duplicateEcmpAdjacency(hops)) throw new McpError("STATE_CONFLICT", "Маршруты ECMP разрешаются в один immediate gateway", 409)
const steps: ChangeEvidence["steps"] = ids.map(id => ({ kind: "ros", step: { serverId: sid, resource, id, action: "update", fields: { distance: String(input.distance) } } }))
const snapshots = [snapshot]
const tableSnapshot = await readRosSnapshot(sid, "routing_table"), table = tableSnapshot.rows.find(row => row.name === String(first["routing-table"] ?? "main"))
if (!table || typeof table[".id"] !== "string" || bool(table.disabled)) throw new McpError("STATE_CONFLICT", "Таблица ECMP отсутствует или отключена", 409)
const tableSnapshot = await read("routing_table")
const table = confirmedEcmpTable(tableSnapshot.rows, String(first["routing-table"] ?? "main"))
if (!table) {
throw new McpError("STATE_CONFLICT", "Для ECMP нужна однозначно определённая активная таблица FIB", 409)
}
snapshots.push(tableSnapshot)
const objects: ChangeEvidence["objects"] = [...ids.map(id => ({ kind: resource, id, serverId: sid })), { kind: "routing_table", id: table[".id"], serverId: sid }]
const objects: ChangeEvidence["objects"] = [...ids.map(id => ({ kind: resource, id, serverId: sid })), { kind: "routing_table", id: table.id, serverId: sid }]
const [settings, vrf, ...impacts] = await Promise.all([
read(settingsResource),
read("vrf"),
...[`${base}_filter`, `${base}_nat`, `${base}_mangle`, `${base}_raw`, "routing_rule"].map(resource => readRoutingPolicySnapshot(sid, resource, sourceProof)),
])
snapshots.push(settings, vrf, ...impacts)
if (input.hashPolicy) {
const settingsResource = input.family === "ipv4" ? "ip_settings" : "ipv6_settings", key = input.family === "ipv4" ? "ipv4-multipath-hash-policy" : "multipath-hash-policy"
const settings = await readRosSnapshot(sid, settingsResource)
const key = ipv4 ? "ipv4-multipath-hash-policy" : "multipath-hash-policy"
if (!(key in (settings.rows[0] ?? {}))) throw new McpError("UNSUPPORTED_CAPABILITY", "Устройство не подтвердило выбранную hash policy", 409)
snapshots.push(settings); steps.push({ kind: "ros", step: { serverId: sid, resource: settingsResource, action: "update", fields: { [key]: String(input.hashPolicy) } } })
steps.push({ kind: "ros", step: { serverId: sid, resource: settingsResource, action: "update", fields: { [key]: String(input.hashPolicy) } } })
objects.push({ kind: settingsResource, id: "singleton", serverId: sid }, { kind: settingsResource, id: "new", serverId: sid })
}
const impacts = await Promise.all(["ip_settings", "ip_filter", "ip_nat", "ip_mangle"].map(resource => readRosSnapshot(sid, resource)))
snapshots.push(...impacts)
return { target: { scope: "servers", serverIds: [sid] }, objects, snapshots, steps, requiresHuman: true,
healthChecks: routes.flatMap(row => [{ serverId: sid, resource, id: String(row[".id"]), field: "resolved" as const, identity: { gateway: String(row.gateway), "immediate-gw": String(row["immediate-gw"]) } }, { serverId: sid, resource, id: String(row[".id"]), field: "active" as const, phase: "after" as const }]),
warnings: ["Равные стоимости проверяются как конфигурация. Распределение реального трафика требует отдельного наблюдения.", "Проверьте NAT, FastTrack, маркировки и асимметрию; RPF автоматически не меняется."] }
await assertSource()
const healthChecks = routes.flatMap((row, index) => {
const hop = hops[index], name = hop?.address === null ? hop.interface : null
const interfaceId = name ? interfaceIds?.get(name) : undefined
const nextHopInterface = name && interfaceId ? { name, id: interfaceId } : undefined
return [{ serverId: sid, resource, id: String(row[".id"]), field: "resolved" as const, identity: { gateway: String(row.gateway), "immediate-gw": String(row["immediate-gw"]) }, nextHopInterface },
{ serverId: sid, resource, id: String(row[".id"]), field: "active" as const, phase: "after" as const }]
})
return { target: { scope: "servers", serverIds: [sid] }, objects, snapshots, steps, requiresHuman: true, healthChecks,
warnings: ["Равные стоимости проверяются как конфигурация. Распределение реального трафика требует отдельного наблюдения.", ipv4 ? "Проверьте NAT, FastTrack, маркировки и асимметрию. Настройка RPF сохраняется." : "Проверьте IPv6 NAT, маркировки, правила маршрутизации и асимметрию."] }
},
})
register("routing.rpf.update", "mm_rpf_plan", "Явное изменение IPv4 reverse path filter", "network", "mm:network:write", "write", "servers", z.object({ serverId, mode: z.enum(["no", "loose", "strict"]), acknowledgeAsymmetry: z.literal(true) }).strict(), {
version: 2,
register("routing.rpf.update", "mm_rpf_plan", "Явное изменение IPv4 reverse path filter", "network", "mm:network:write", "write", "servers", z.object({ serverId, family: family.default("ipv4"), mode: z.enum(["no", "disabled", "loose", "strict"]), acknowledgeAsymmetry: z.literal(true) }).strict(), {
version: 4,
prepare: async input => {
const sid = Number(input.serverId), snapshots = await Promise.all(["ip_settings", "routing_table", "vrf", "ipv4_route"].map(resource => readRosSnapshot(sid, resource)))
if (!("rp-filter" in (snapshots[0].rows[0] ?? {}))) throw new McpError("UNSUPPORTED_CAPABILITY", "RPF не подтверждён устройством", 409)
if (input.mode === "strict" && (snapshots[1].rows.some(row => row.name !== "main") || snapshots[2].rows.some(row => row.name !== "main"))) throw new McpError("STATE_CONFLICT", "Strict RPF не допускается этим планом при нескольких таблицах или VRF", 409)
return { target: { scope: "servers", serverIds: [sid] }, objects: [{ kind: "server", id: String(sid), serverId: sid }, { kind: "ip_settings", id: "singleton", serverId: sid }, { kind: "ip_settings", id: "new", serverId: sid }], snapshots, steps: [{ kind: "ros", step: { serverId: sid, resource: "ip_settings", action: "update", fields: { "rp-filter": String(input.mode) } } }], requiresHuman: true,
warnings: ["RPF является IPv4-настройкой. Изменение может затронуть асимметричный трафик; снижение защиты требует компенсирующих правил."] }
if (input.family === "ipv6") throw new McpError("UNSUPPORTED_CAPABILITY", "RouterOS rp-filter доступен только для IPv4", 409)
const sid = Number(input.serverId)
const mode = input.mode === "disabled" ? "no" : String(input.mode)
const settings = await readRosSnapshot(sid, "ip_settings")
if (!("rp-filter" in (settings.rows[0] ?? {}))) throw new McpError("UNSUPPORTED_CAPABILITY", "RPF не подтверждён устройством", 409)
const { snapshots, warnings } = await readRpfDependencies(sid, mode, settings.sourceProof)
return { target: { scope: "servers", serverIds: [sid] }, objects: [{ kind: "server", id: String(sid), serverId: sid }, { kind: "ip_settings", id: "singleton", serverId: sid }, { kind: "ip_settings", id: "new", serverId: sid }], snapshots: [settings, ...snapshots], steps: [{ kind: "ros", step: { serverId: sid, resource: "ip_settings", action: "update", fields: { "rp-filter": mode } } }], requiresHuman: true, warnings }
},
})
register("routing.vrf.update", "mm_vrf_plan", "Проверенный состав существующего VRF", "network", "mm:network:write", "write", "servers", z.object({ serverId, id: objectId, interfaces: z.array(text).min(1).max(100).refine(values => new Set(values).size === values.length) }).strict(), {
@@ -16,7 +16,7 @@ const originalSecurity = fs.readFileSync(path.join(root, 'backend/src/modules/mc
const securityAst = ts.createSourceFile('security.ts', originalSecurity, ts.ScriptTarget.Latest, true);
const selected = securityAst.statements.filter(node => ts.isFunctionDeclaration(node) && ['fingerprint', 'redact'].includes(node.name?.text)).map(node => node.getText(securityAst)).join('\n');
const securityExports = {};
const securityBody = ts.transpileModule('const sensitive = /password|passwd|secret|private.?key|preshared|pre.shared|authorization|auth.?key|authentication.?key|tcp.?md5.?key|api.?key|certificate.?contents|token|ciphertext/i; const MAX_MCP_PUBLIC_STRING_LENGTH=16000;\n' + selected, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText;
const securityBody = ts.transpileModule('const sensitive = /password|passwd|secret|private.?key|preshared|pre.shared|authorization|auth.?key|authentication.?key|tcp.?md5.?key|api.?key|certificate.?contents|token|ciphertext/i; const MAX_MCP_PUBLIC_STRING_LENGTH=16000; const MAX_MCP_PUBLIC_ARRAY_ITEMS=500;\n' + selected, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText;
new Function('exports', 'tokenDigest', securityBody)(securityExports, sha);
const { fingerprint, redact } = securityExports;
class McpError extends Error { constructor(code, message) { super(message); this.code = code; } }
@@ -0,0 +1,60 @@
import type { ChangeEvidence } from "./action-types.js"
import { confirmedEcmpInterfaces, preflightObservation } from "./ecmp-preflight.js"
import { resourceById, routerClient, rosSnapshotSourceProof } from "./resources.js"
import { McpError } from "./security.js"
import { readRouterOsSnapshotGet } from "../../services/routeros-snapshot-read.js"
export async function verifyHealthChecks(evidence: Pick<ChangeEvidence, "healthChecks" | "snapshots">, serverIds: number[], afterWrite: boolean, authorize?: () => Promise<unknown>) {
const cache = new Map<string, unknown[]>()
const interfaces = new Map<number, Map<string, string>>()
for (const check of evidence.healthChecks ?? []) {
if (check.phase === "after" && !afterWrite) continue
if (!serverIds.includes(check.serverId)) throw new McpError("SERVER_DENIED", "Проверка находится вне плана")
if (check.field === "state" && check.resource !== "ospf_neighbor") throw new McpError("INVALID_INPUT", "Неизвестный критерий здоровья", 409)
if (check.field === "bfd-up" && check.resource !== "bfd_session") throw new McpError("INVALID_INPUT", "Неизвестный критерий BFD", 409)
if (check.field === "resolved" && !["ipv4_route", "ipv6_route"].includes(check.resource)) throw new McpError("INVALID_INPUT", "Неизвестный критерий next-hop", 409)
const key = `${check.serverId}:${check.resource}`
if (!cache.has(key)) {
await authorize?.()
const { client, server } = await routerClient(check.serverId)
const path = resourceById(check.resource).path
const isRoute = check.resource === "ipv4_route" || check.resource === "ipv6_route"
let raw: unknown
if (isRoute) {
const expected = evidence.snapshots.find(snapshot => snapshot.serverId === check.serverId && snapshot.resource === check.resource)?.sourceProof
if (!expected) throw new McpError("PLAN_EXPIRED", "Не подтверждён источник проверки next-hop", 409)
const compareSource = async (current: typeof server) => {
if (await rosSnapshotSourceProof(current) !== expected) {
throw new McpError("STATE_CONFLICT", "Источник проверки next-hop изменился", 409)
}
}
const assertCurrent = async () => {
await authorize?.()
await compareSource((await routerClient(check.serverId)).server)
}
await compareSource(server)
raw = await readRouterOsSnapshotGet({ client, path, assertCurrent })
await assertCurrent()
if (!interfaces.has(check.serverId) && evidence.healthChecks?.some(item => item.serverId === check.serverId && item.resource === check.resource && item.nextHopInterface)) {
const inventory = preflightObservation(await readRouterOsSnapshotGet({ client, path: "/interface", assertCurrent }))
await assertCurrent()
if (inventory.error) throw new McpError(afterWrite ? "VERIFICATION_FAILED" : "STATE_CONFLICT", "Каталог интерфейсов next-hop не подтверждён", 409)
interfaces.set(check.serverId, confirmedEcmpInterfaces(inventory.rows))
}
} else {
raw = await client.get<unknown>(path)
}
await authorize?.()
if (!Array.isArray(raw)) throw new McpError("STATE_UNAVAILABLE", "Ответ проверки здоровья не подтверждён", 503)
cache.set(key, raw)
}
const rows = cache.get(key)!.filter((value): value is Record<string, unknown> => !!value && typeof value === "object" && (value as Record<string, unknown>)[".id"] === check.id)
const row = rows.length === 1 ? rows[0] : undefined
const identityMatches = row && Object.entries(check.identity ?? {}).every(([key, value]) => row[key] === value)
if (check.nextHopInterface && interfaces.get(check.serverId)?.get(check.nextHopInterface.name) !== check.nextHopInterface.id) {
throw new McpError(afterWrite ? "VERIFICATION_FAILED" : "STATE_CONFLICT", "Интерфейс next-hop изменился после подготовки", 409)
}
const healthy = identityMatches && row && (check.field === "resolved" ? typeof row["immediate-gw"] === "string" && row["immediate-gw"].trim().length > 0 && ![true, "true", "yes"].includes(row.invalid as boolean | string) && ![true, "true", "yes"].includes(row.disabled as boolean | string) : check.field === "bfd-up" ? String(row.state).toLowerCase() === "up" : check.field === "state" ? String(row.state).toLowerCase() === "full" : [true, "true", "yes"].includes(row[check.field] as boolean | string))
if (!healthy) throw new McpError(afterWrite ? "VERIFICATION_FAILED" : "STATE_CONFLICT", afterWrite ? "После записи не подтверждён next-hop, Full OSPF или Up BFD" : "Работоспособность next-hop/OSPF/BFD изменилась после подготовки", 409)
}
}
@@ -138,6 +138,7 @@ function retentionOwner(f:Awaited<ReturnType<typeof retentionFixture>>,present=t
calls.push(sql)
if(sql==='SHOW transaction_isolation')return{rows:[{transaction_isolation:'read committed'}]}
if(sql.includes('pg_advisory_xact_lock'))return{rows:[]}
if(sql.includes('FROM mcp_settings WHERE id=1 FOR SHARE'))return{rows:[{enabled:true,requestsRetentionDays:1,auditRetentionDays:180}]}
if(sql.includes('clock_timestamp'))return{rows:[{at:new Date('2026-11-03T12:00:00.000Z')}]}
if(sql.startsWith('DECLARE'))return{rows:[]}
if(sql.startsWith('FETCH'))return{rows:[f.row]}
@@ -159,10 +160,12 @@ test('retention MODEL intent2 authenticates SQL clock after parent GC; unknown n
const f=await retentionFixture(version),owner=retentionOwner(f),before=fingerprint(f.row)
// SQL aging and the transaction owner below are MODELS, never genuine SQL.
await assertLegacyManualHistoryWithinOwner(owner.db,f.profileId,'2026-11-03T12:00:00.000Z',()=>{})
assert.deepEqual(await retainManualInverseChargesWithinOwner(owner.db,1),{deleted:1})
assert.deepEqual(await retainManualInverseChargesWithinOwner(owner.db),{deleted:1})
assert.equal(owner.deleted.length,1);assert.equal(fingerprint(f.row),before)
for(const profile of [f.profileId,randomUUID()])await assertLegacyManualHistoryWithinOwner(owner.db,profile,'2026-11-03T12:00:00.000Z',()=>{})
assert(owner.calls.findIndex(s=>s.includes('pg_advisory_xact_lock'))<owner.calls.findIndex(s=>s.startsWith('SELECT c.recovery_operation_id')))
const admission=owner.calls.findIndex(s=>s.includes('pg_advisory_xact_lock'))
const settings=owner.calls.findIndex(s=>s.includes('FROM mcp_settings WHERE id=1 FOR SHARE'))
assert(admission>=0 && admission<settings && settings<owner.calls.findIndex(s=>s.startsWith('SELECT c.recovery_operation_id')))
const candidate=owner.calls.find(s=>s.startsWith('SELECT c.recovery_operation_id'))!
assert(candidate.indexOf("state IN('pending','running')")<candidate.indexOf('LIMIT 100'))
assert(candidate.includes('r.authority_profile_version IS NOT NULL'))
@@ -180,16 +183,16 @@ test('retention MODEL refuses recent/future/malformed copy, present corrupt pare
f.parent.binding_proof='0'.repeat(64)
await assert.rejects(assertLegacyManualHistoryWithinOwner(retentionOwner(f).db,f.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/MAC/)
const wrong=await retentionFixture();wrong.row.authority_profile_version=2;const o=retentionOwner(wrong)
await assert.rejects(retainManualInverseChargesWithinOwner(o.db,1),/hint/);assert.equal(o.deleted.length,0)
await assert.rejects(retainManualInverseChargesWithinOwner(o.db),/hint/);assert.equal(o.deleted.length,0)
const tamper=await retentionFixture();tamper.intent.manualInverseCharge.proof='0'.repeat(64)
tamper.row.intent_sealed=await encryptSecret(JSON.stringify(tamper.intent));tamper.row.intent_proof=await sign(tamper.intent,'route-ai-scoped-recovery-intent-v1')
const t=retentionOwner(tamper);await assert.rejects(retainManualInverseChargesWithinOwner(t.db,1),/MAC/);assert.equal(t.deleted.length,0)
const t=retentionOwner(tamper);await assert.rejects(retainManualInverseChargesWithinOwner(t.db),/MAC/);assert.equal(t.deleted.length,0)
const legacy=await fixture(true,7)
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([legacy.row]).db,legacy.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/lacks durable/)
const wrongVersion=await retentionFixture(7);wrongVersion.row.recovery_version=8
const mismatched=retentionOwner(wrongVersion)
await assert.rejects(assertLegacyManualHistoryWithinOwner(mismatched.db,wrongVersion.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/signed allocation/)
await assert.rejects(retainManualInverseChargesWithinOwner(mismatched.db,1),/hint/)
await assert.rejects(retainManualInverseChargesWithinOwner(mismatched.db),/hint/)
assert.equal(mismatched.deleted.length,0)
// Revision2 payload proof cannot be replaced by an authentic stamp from a different payload.
const payload=await retentionFixture();payload.row.recovery_payload=await encryptSecret('{}')
@@ -3,7 +3,7 @@ import { z } from "zod"
import { routeAiProfileSchema } from "@mmapp/contracts/mcp-routing"
import type { ChangeEvidence } from "./action-types.js"
import type { McpPrincipal, StoredPlan } from "./store.js"
import { effectiveMcpConnection } from "./store.js"
import { effectiveMcpConnection, getMcpSettings } from "./store.js"
import { decryptSecretExisting, encryptSecret, fingerprint, McpError, privateFingerprinterExisting, privateSecretFingerprint } from "./security.js"
import { routingProfileAuthorityBindingSchema } from "./routing-profile-authority.js"
import type { RoutingSwitchLedgerEntry } from "./routing-switch-ledger-contract.js"
@@ -184,10 +184,10 @@ export async function authenticateManualInverseHistoryWithinOwner(db:PoolClient,
/** Existing retention floor, same admission owner, bounded returned batch.
* The version column is only a candidate hint: all private evidence is checked
* before DELETE. Old revision1 remains pinned while its intent survives. */
export async function retainManualInverseChargesWithinOwner(db: PoolClient, retentionDays: number) {
if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 365) throw fail("Invalid manual inverse retention")
export async function retainManualInverseChargesWithinOwner(db: PoolClient) {
if ((await db.query("SHOW transaction_isolation")).rows[0]?.transaction_isolation !== "read committed") throw fail("Manual inverse GC requires fresh read-committed owner")
await db.query("SELECT pg_advisory_xact_lock(hashtextextended('mcp-runner-admission',0))")
const { requestsRetentionDays } = await getMcpSettings(db)
const observedAt=iso((await db.query("SELECT date_trunc('milliseconds',clock_timestamp()) AS at")).rows[0].at)
const candidates=(await db.query<{recovery_operation_id:string;source_operation_id:string;profile_version:number}>(`SELECT c.recovery_operation_id,c.source_operation_id,c.profile_version
FROM mcp_routing_manual_inverse_charges c JOIN mcp_scoped_recovery_intents r
@@ -195,7 +195,7 @@ export async function retainManualInverseChargesWithinOwner(db: PoolClient, rete
WHERE r.mode='manual' AND r.authority_profile_id IS NULL AND r.authority_profile_version IS NOT NULL
AND c.created_at < now()-make_interval(days=>$1) AND c.created_at < now()-interval '1 hour'
AND NOT EXISTS(SELECT 1 FROM mcp_operations o WHERE o.id=c.recovery_operation_id AND o.state IN('pending','running'))
ORDER BY c.created_at,c.recovery_operation_id LIMIT 100 FOR UPDATE OF c,r`,[retentionDays])).rows
ORDER BY c.created_at,c.recovery_operation_id LIMIT 100 FOR UPDATE OF c,r`,[requestsRetentionDays])).rows
const {manualInverseHistoryProjection,classifyManualInverseHistory}=await import("./routing-manual-inverse-history.js")
let deleted=0
for(const candidate of candidates) {
@@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto"
import { test } from "node:test"
import { z } from "zod"
import ts from "typescript"
import { scopedTargetRuleMatches } from "./routing-community-scoped-model.js"
/** Inert dependency-boundary tests only. SQL/HTTP acceptance must create the
* real checkpoint through the existing manual service and production HMAC;
@@ -36,6 +37,10 @@ async function fixture() {
const checkpoint = { checkpointProof: "persisted-checkpoint", observedAt: new ClockDate(now - 500).toISOString(), packet }
const sharedToken = Object.freeze({}), acquisition = Object.freeze({}), sharedBinding = { sourceOperationId: row.id, profile: structuredClone(profile), principal: structuredClone(principal),
deadline: now + 10000, contextSources: structuredClone(input.contextSources), candidateId: "b" }
let substitutedTarget: string | undefined
const ownedTarget = () => ({ nativeId, rawRow: { rule: nativeBody },
matchesRuleProof: (body: string, proof: unknown) => scopedTargetRuleMatches({ rawRow: { rule: nativeBody },
desiredTargetProof: observed => observed === nativeBody ? nativeTarget : observed === packet.desiredRule ? substitutedTarget ?? "" : "" }, body, proof) })
const providers: Record<string, unknown> = {
"./routing-network-execution-session.js": { isVerifiedManualEffectOwner: (owner: unknown) => owner === sharedToken && manualOwner },
zod: { z }, "@mmapp/contracts/mcp-routing": { routeAiProfileSchema: { parse: (value: unknown) => value } },
@@ -48,7 +53,7 @@ async function fixture() {
sharedReads++; nativeReads++; if (revoked) throw Error("revoked")
return { before: { scopedExecutionAvailable: !guard, scopedExecutionReason: guard ? "ROUTE_RECOVERY_EXECUTOR_UNAVAILABLE" : null,
dependencyProof: nativeDependency, targetRuleProof: nativeTarget, currentCandidateId: nativeCandidate, sourceProofs: input.contextSources },
ownedTarget: { nativeId, rawRow: { rule: nativeBody } }, fence: async () => { if (revoked || sharedClosed) throw Error("revoked") } }
ownedTarget: ownedTarget(), fence: async () => { if (revoked || sharedClosed) throw Error("revoked") } }
} },
"./routing-network-native-session.js": { inspectRoutingNetworkSharedContext: (token: unknown) => { if (token !== sharedToken || sharedClosed) throw Error("Unknown shared context"); return sharedBinding },
assertRoutingNetworkSharedContextReadOwner: (owner: unknown, read: unknown) => { if (owner !== sharedToken || read !== acquisition || sharedClosed) throw Error("Unknown acquisition owner") },
@@ -67,13 +72,13 @@ async function fixture() {
nativeReads++; sharedReads++; afterNative?.(); afterNative = undefined
return { before: { scopedExecutionAvailable: !guard, scopedExecutionReason: null,
dependencyProof: nativeDependency, targetRuleProof: nativeTarget, currentCandidateId: nativeCandidate, sourceProofs: input.contextSources },
ownedTarget: { nativeId, rawRow: { rule: nativeBody } }, fence: async () => { if (revoked || sharedClosed) throw Error("revoked") } }
ownedTarget: ownedTarget(), fence: async () => { if (revoked || sharedClosed) throw Error("revoked") } }
}, readRouteCommunityContext: async (_profile: unknown, options: { authorize(): Promise<void>; signal: AbortSignal; expectedSources: unknown }) => {
assert.equal(options.signal.aborted, false); assert.deepEqual(options.expectedSources, input.contextSources)
await options.authorize(); nativeReads++; afterNative?.(); afterNative = undefined
return { before: { scopedExecutionAvailable: !guard, scopedExecutionReason: guard ? "ROUTE_RECOVERY_EXECUTOR_UNAVAILABLE" : null,
dependencyProof: nativeDependency, targetRuleProof: nativeTarget, currentCandidateId: nativeCandidate, sourceProofs: input.contextSources },
ownedTarget: { nativeId, rawRow: { rule: nativeBody } }, fence: options.authorize }
ownedTarget: ownedTarget(), fence: options.authorize }
} },
}
const source = await readFile(new URL("./routing-network-configuration-bridge.ts", import.meta.url), "utf8")
@@ -98,6 +103,12 @@ async function fixture() {
stats: () => ({ nativeReads, profileChecks, sourceReads, configChecks, sharedReads, sharedClosed }), advance: (ms: number) => { now += ms },
revoke: () => { revoked = true }, sourceChanged: () => { sourceChanged = true }, checkpointMissing: () => { checkpointMissing = true },
checkpointChanged: () => { checkpointChanged = true }, guard: () => { guard = true },
bareNativeReadback: (proof: string) => {
packet.desiredRule = 'if (bgp-communities includes 65000:10) {\n set gw 192.0.2.9;\n set gw-interface "marker-gre-b";\n accept;\n}'
nativeBody = packet.desiredRule.replace('"marker-gre-b"', "marker-gre-b")
nativeTarget = "bare-target"
substitutedTarget = proof
},
drift: (field: string) => { if (field === "body") nativeBody = "other"; if (field === "dependency") nativeDependency = "other"; if (field === "target") nativeTarget = "other"; if (field === "candidate") nativeCandidate = "a"; if (field === "nativeId") nativeId = "*2"; if (field === "generation") generation = "changed" },
afterNative: (hook: () => void) => { afterNative = hook }, afterCheckpoint: (hook: () => void) => { afterCheckpoint = hook }, afterProfile: (hook: () => void) => { afterProfile = hook } }
}
@@ -111,6 +122,15 @@ test("fixed resolver captures opaque context and consume rechecks configuration
assert.equal(f.stats().nativeReads, 2); assert(f.stats().profileChecks > 5)
await assert.rejects(f.consume(token), /Unknown or consumed/); await assert.rejects(f.resolve(), /already issued/)
})
test("optional RouterOS quotes do not change the sealed checkpoint identity", async () => {
const f = await fixture()
f.bareNativeReadback("desired-target")
const observed = await f.consume(await f.resolve())
assert.equal(observed.configuration.bodyIdentity, JSON.stringify(f.packet.desiredRule))
const drift = await fixture()
drift.bareNativeReadback("different-native-metadata")
await assert.rejects(drift.resolve(), /drifted/)
})
for (const mode of ["positive", "unbranded", "wrong-checkpoint", "terminal-source", "standalone"] as const) {
test(`manual revision-2 round ${mode}`, async () => {
@@ -188,8 +188,9 @@ async function resolveConfigurationContext(sourceOperationId: string, action: Ac
selectedCandidateId: String(input.candidateId), authorize, signal, expectedSources: input.contextSources as Array<{ serverId: number; proof: string }> }))
if (!context.before.scopedExecutionAvailable || !context.ownedTarget) throw fail(String(context.before.scopedExecutionReason ?? "Owned context unavailable"))
const owned = context.ownedTarget
if (owned.nativeId !== original.packet.nativeId || owned.nativeId !== input.nativeId || owned.rawRow.rule !== original.packet.desiredRule ||
context.before.dependencyProof !== original.packet.dependencyProof || context.before.targetRuleProof !== original.packet.desiredTargetProof ||
if (owned.nativeId !== original.packet.nativeId || owned.nativeId !== input.nativeId ||
context.before.dependencyProof !== original.packet.dependencyProof ||
!owned.matchesRuleProof(original.packet.desiredRule, original.packet.desiredTargetProof) ||
context.before.currentCandidateId !== original.packet.desiredCandidateId || fingerprint(context.before.sourceProofs) !== fingerprint(original.packet.contextSources)) throw fail("Current owned rule or dependencies drifted from CONFIG_APPLIED")
await guarded(() => context.fence())
const after = await checkpoint()
@@ -15,7 +15,7 @@ import { createRoutingSourceContext, readRoutingEndpoints } from "./routing-obse
import { routeContextClients } from "./routing-community-context-native.js"
import { resolveRoutingNetworkDestinationPins } from "./routing-network-native-destination.js"
import { readMeasurementAddress } from "./routing-measurement-address.js"
import { readRouteCommunityContextWithSharedNativeSession } from "./routing-community-context.js"
import { readRouteCommunityContextWithSharedNativeSession, type OwnedRouteCommunityTarget } from "./routing-community-context.js"
import type { ActionContext, ChangeEvidence } from "./action-types.js"
import type { RoutingNetworkForwardRoundConfigurationContext } from "./routing-network-configuration-bridge.js"
@@ -45,7 +45,7 @@ type SharedBinding = Readonly<{ sourceOperationId: string; profile: z.infer<type
deadline: number; contextSources: Array<{ serverId: number; proof: string }>; candidateId: string; signal: AbortSignal }>
type SharedContextResult = { before: { scopedExecutionAvailable: boolean; scopedExecutionReason: string | null; dependencyProof: string; targetRuleProof: string | null;
currentCandidateId: string | null; sourceProofs: Array<{ serverId: number; proof: string }> }; fence(): Promise<void>;
ownedTarget: { nativeId: string; rawRow: Row } | undefined }
ownedTarget: Pick<OwnedRouteCommunityTarget, "nativeId" | "rawRow" | "matchesRuleProof"> | undefined }
type SharedOwner = { binding: SharedBinding; active(): void; fence(): Promise<void>; close(): void;
read(serverId: number, path: string, body?: Record<string, string>): Promise<unknown>; run(): Promise<SharedContextResult> }
const sharedContexts = new WeakMap<RoutingNetworkSharedContext, SharedOwner>()
@@ -47,7 +47,7 @@ const collections = (path: string): Record<string, unknown>[] => {
if (path === "/ip/vrf") return [{ ".id": "*v1", name: "probe", interfaces: "wan1", disabled: "false" }, { ".id": "*v2", name: "main", interfaces: "all", disabled: "false" }]
if (path === "/routing/route") return [{ ".id": "*r1", afi: "ip", "dst-address": "203.0.113.0/24", "routing-table": "tenant", gateway: "192.0.2.1@main", "immediate-gw": "192.0.2.1%wan1", active: "true", dynamic: "true", contribution: "active", bgp: "true", "bgp.session": "*s1" }]
if (path === "/routing/bgp/connection") return [{ ".id": "*c1", name: "peer1", instance: "default", "remote.address": "192.0.2.1", "local.address": "192.0.2.2", "remote.as": "65001", afi: "ip", templates: "", vrf: "main", "routing-table": "tenant", disabled: "false" }]
if (path === "/routing/bgp/session") return [{ ".id": "*s1", name: "peer1-1", instance: "default", "remote.address": "192.0.2.1", "local.address": "192.0.2.2", "remote.as": "65001", "local.afi": "ip", "remote.afi": "ip", established: "true", vrf: "main", "routing-table": "tenant" }]
if (path === "/routing/bgp/session") return [{ ".id": "*s1", name: "peer1-1", instance: "default", "remote.address": "192.0.2.1@main", "local.address": "192.0.2.2@main", "remote.as": "65001", "local.afi": "ip", "remote.afi": "ip", established: "true", "routing-table": "tenant" }]
if (path === "/routing/bfd/session") return [{ ".id": "*b1", "local-address": "192.0.2.2", "remote-address": "192.0.2.1", interface: "wan1", vrf: "main", state: "up" }]
if (path === "/log") return []
if (path === "/routing/table") return ["main", "tenant", "probe"].map((name, i) => ({ ".id": "*t" + i, name, fib: "true" }))
@@ -16,6 +16,7 @@ import { canonicalVrfMembers } from "../../services/vrf-members.js"
import { readOspfEffectiveDisabled } from "../../services/ospf-effective-disabled.js"
import { isConfirmedRoutingFibTable } from "../../services/routing-table-fib.js"
import * as contextModel from "./routing-community-context-model.js"
import { scopedTargetRuleMatches } from "./routing-community-scoped-model.js"
import { prepareOriginalEffectSelection } from "./routing-original-effects.js"
import { readMeasurementAddress } from "./routing-measurement-address.js"
import { routeContextClients } from "./routing-community-context-native.js"
@@ -95,7 +96,8 @@ async function fixture(autoRecovery = false) {
await loadSource("./routing-community-context.ts", { ...common, "./routing-network-native-session.js": native,
"../../services/routing-table-fib.js": { isConfirmedRoutingFibTable }, "./routing-original-effects.js": { prepareOriginalEffectSelection },
"../../services/bgp-export-model.js": { nativeBgpRows, bgpTemplateReferences }, "../../services/bgp-filter-binding-model.js": { bgpFilterName, effectiveBgpFilter, stableBgpPolicyRow },
"./routing-community-context-model.js": contextModel, "./path-proof-purpose.js": { pathProofTag },
"./routing-community-context-model.js": contextModel, "./routing-community-scoped-model.js": { scopedTargetRuleMatches },
"./path-proof-purpose.js": { pathProofTag },
"./management-transport.js": { assertManagementTransport: async () => { throw Error("No alternate management fixture") } } }, context)
await loadSource("./routing-network-native-session.ts", { ...common, "node:net": { isIP }, zod: { z }, "@mmapp/contracts/mcp-routing": { routeAiProfileSchema },
"./routing-community-context.js": context, "./registry.js": { findMcpOperation: () => ({ version: 7 }) },
@@ -3,6 +3,9 @@ import type { ChangeEvidence } from "./action-types.js"
import { fingerprint, McpError } from "./security.js"
type Checks = NonNullable<ChangeEvidence["observationChecks"]>
export function latestRoutingMetric(rows: readonly RouteObservation[], metric: "pingAt" | "bandwidthAt") {
return rows.find(row => row[metric] !== null || row.sourceVerified === false)
}
export function assertRoutingChecksFresh(checks: Checks, now = Date.now()) {
if (checks.length > 1000 || checks.some(check => !Number.isFinite(Date.parse(check.validUntil)) || Date.parse(check.validUntil) <= now)) throw new McpError("MEASUREMENT_UNKNOWN", "Измерения маршрутизации устарели; нужен новый план", 409)
}
@@ -2,7 +2,7 @@ import type { RouteObservation } from "@mmapp/contracts/mcp-routing"
import { pool } from "../../db/index.js"
import { McpError } from "./security.js"
import type { ChangeEvidence } from "./action-types.js"
import { assertRoutingChecksFresh, assertRoutingObservationValues } from "./routing-observations-model.js"
import { assertRoutingChecksFresh, assertRoutingObservationValues, latestRoutingMetric } from "./routing-observations-model.js"
import { createRoutingSourceContext, readRoutingEndpoints, type RoutingSourceRecord } from "./routing-observation-source.js"
export const routingObservationColumns = `o.id,o.src_server_id AS "srcServerId",o.dst_server_id AS "dstServerId",o.src_interface AS "srcInterface",o.dst_interface AS "dstInterface",o.family,o.routing_table AS "table",o.ping_ms AS "pingMs",o.loss_pct AS "lossPct",o.dl_mbps AS "dlMbps",o.ul_mbps AS "ulMbps",to_char(o.ping_at AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS "pingAt",to_char(o.bandwidth_at AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS "bandwidthAt",o.context_verified AS "contextVerified",o.source,to_char(o.created_at AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS "createdAt",o.source_proof AS "sourceProof"`
@@ -23,16 +23,67 @@ export async function readRoutingObservations(serverIds: number[], serverId?: nu
return observations((await pool.query(`SELECT ${routingObservationColumns} FROM mcp_routing_observations o WHERE o.src_server_id=ANY($1::bigint[]) AND o.dst_server_id=ANY($1::bigint[]) AND ($2::bigint IS NULL OR o.src_server_id=$2 OR o.dst_server_id=$2) AND o.created_at>now()-interval '24 hours' ORDER BY o.created_at DESC,o.id DESC LIMIT 10000`, [serverIds, serverId ?? null])).rows, serverIds)
}
interface RoutingObservationQuery {
serverId?: number
srcServerId?: number
dstServerId?: number
srcInterface?: string
dstInterface?: string
family?: "ipv4" | "ipv6"
table?: string
limit: number
cursor?: { recordedAt: string; id: string }
}
export async function queryRoutingObservations(serverIds: number[], input: RoutingObservationQuery) {
if ([input.serverId, input.srcServerId, input.dstServerId].some(id => id !== undefined && !serverIds.includes(id))) {
throw new McpError("SERVER_DENIED", "История измерений вне разрешённой области", 403)
}
const { rows } = await pool.query(`SELECT ${routingObservationColumns} FROM mcp_routing_observations o
WHERE o.src_server_id=ANY($1::bigint[]) AND o.dst_server_id=ANY($1::bigint[])
AND ($2::bigint IS NULL OR o.src_server_id=$2 OR o.dst_server_id=$2)
AND ($3::bigint IS NULL OR o.src_server_id=$3) AND ($4::bigint IS NULL OR o.dst_server_id=$4)
AND ($5::text IS NULL OR o.src_interface=$5) AND ($6::text IS NULL OR o.dst_interface=$6)
AND ($7::text IS NULL OR o.family=$7) AND ($8::text IS NULL OR o.routing_table=$8)
AND ($9::timestamptz IS NULL OR (o.created_at,o.id)<($9::timestamptz,$10::uuid))
AND o.created_at>now()-interval '24 hours'
ORDER BY o.created_at DESC,o.id DESC LIMIT $11`, [serverIds, input.serverId ?? null, input.srcServerId ?? null,
input.dstServerId ?? null, input.srcInterface ?? null, input.dstInterface ?? null, input.family ?? null, input.table ?? null,
input.cursor?.recordedAt ?? null, input.cursor?.id ?? null, input.limit + 1])
const page = await observations(rows.slice(0, input.limit), serverIds)
const last = page.at(-1)
return { observations: page, nextCursor: rows.length > input.limit && last?.recordedAt ? { recordedAt: last.recordedAt, id: last.id } : null }
}
/** Match the sealed observation to the newest record for the exact context.
* No re-ranking and no replacement by an older healthy observation. */
export async function verifyRoutingObservationChecks(checks: ChangeEvidence["observationChecks"]) {
if (!checks?.length) return
assertRoutingChecksFresh(checks)
const { rows } = await pool.query(`SELECT latest.* FROM jsonb_array_elements($1::jsonb) WITH ORDINALITY AS q(value,ordinality)
const { rows } = await pool.query(`SELECT q.ordinality AS "checkOrdinal", latest.* FROM jsonb_array_elements($1::jsonb) WITH ORDINALITY AS q(value,ordinality)
LEFT JOIN LATERAL(SELECT ${routingObservationColumns} FROM mcp_routing_observations o
WHERE o.src_server_id=(q.value->>'srcServerId')::bigint AND o.dst_server_id=(q.value->>'dstServerId')::bigint
AND o.src_interface=q.value->>'srcInterface' AND o.family=q.value->>'family' AND o.routing_table=q.value->>'table'
AND o.created_at>now()-interval '24 hours' ORDER BY o.created_at DESC,o.id DESC LIMIT 1) latest ON true ORDER BY q.ordinality`, [JSON.stringify(checks)])
const latest = await observations(rows, [...new Set(checks.flatMap(check => [check.srcServerId, check.dstServerId]))])
AND (q.value->>'dstInterface' IS NULL OR o.dst_interface=q.value->>'dstInterface')
AND o.created_at>now()-interval '24 hours'
AND (q.value->>'metric' IS NULL OR (o.created_at,o.id)>=(SELECT sealed.created_at,sealed.id FROM mcp_routing_observations sealed WHERE sealed.id=(q.value->>'id')::uuid))
ORDER BY o.created_at DESC,o.id DESC LIMIT (CASE WHEN q.value->>'metric' IS NULL THEN 1 ELSE 10001 END)) latest ON true
ORDER BY q.ordinality,latest."createdAt" DESC,latest.id DESC LIMIT 10001`, [JSON.stringify(checks)])
if (rows.length > 10000) throw new McpError("MEASUREMENT_UNKNOWN", "Слишком много новых измерений маршрутизации; нужен новый план", 409)
const verified = await observations(rows.filter(row => row.id !== null).map(({ checkOrdinal: _, ...row }) => row), [...new Set(checks.flatMap(check => [check.srcServerId, check.dstServerId]))])
const groups = new Map<number, RouteObservation[]>()
let index = 0
for (const row of rows) {
if (row.id === null) continue
const ordinal = Number(row.checkOrdinal)
const group = groups.get(ordinal) ?? []
group.push(verified[index++])
groups.set(ordinal, group)
}
const latest = checks.flatMap((check, position) => {
const candidates = groups.get(position + 1) ?? []
const match = check.metric ? latestRoutingMetric(candidates, check.metric) : candidates[0]
return match ? [match] : []
})
assertRoutingObservationValues(checks, latest)
}
@@ -5,27 +5,58 @@ import { randomUUID } from 'node:crypto';
import test from 'node:test';
const require=createRequire(new URL('../../../package.json',import.meta.url)),ts=require('typescript'),{z}=require('zod');
const code=ts.transpileModule(await readFile(new URL('./routing-community-scoped-service.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
async function fixture(mode='prepared',failure){
async function loadModel(path, resolveDependency = require) {
const source = await readFile(new URL(path, import.meta.url), 'utf8');
const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText;
const exports = {};
new Function('exports', 'require', code)(exports, resolveDependency);
return exports;
}
const filterBody = await loadModel('../../services/routing-filter-body.ts');
const pathProofPurpose = await loadModel('./path-proof-purpose.ts');
const nativeEndpoint = await loadModel('./routing-native-endpoint.ts');
const { originalEffectReportSchema } = await loadModel('./routing-original-effects.ts', name => {
if (name === './routing-community-context-model.js' || name === './security.js') return {};
if (name === './routing-native-endpoint.js') return nativeEndpoint;
return require(name);
});
class FixtureMcpError extends Error {
constructor(code, message) {
super(message);
this.code = code;
}
}
async function fixture(mode='prepared',failure,{ proofRevision = 3, manualEffectPolicy } = {}){
const calls=[],principal={connectionId:randomUUID(),credentialId:randomUUID(),credentialExpiresAt:new Date(Date.now()+120000).toISOString()},connection={id:principal.connectionId,owner_id:'owner',generation:'generation',owner_permissions:['mm:filters:write','mm:network:read'],policy:{mode:'copilot',permissions:['mm:filters:write','mm:network:read'],scope:{serverIds:[1]},expiresAt:new Date(Date.now()+120000).toISOString()},owner_valid_until:new Date(Date.now()+120000)};
const profile={serverId:1,family:'ipv4',table:'tenant',probeVrf:'probe',community:'65000:10',autoRecovery:false,candidates:[{id:'a',jumpHostId:1,exitNodeId:1,gateway:'192.0.2.8',gatewayRef:{resource:'ipv4_route',id:'*r0'},controlProbe:{}},{id:'b',jumpHostId:1,exitNodeId:1,gateway:'192.0.2.9',gatewayRef:{resource:'ipv4_route',id:'*r'},controlProbe:{}}],excludedCandidateIds:[],pinnedCandidateId:null};
const input={serverId:1,profile,candidateId:'b',principal,ownerId:'owner',policyProof:JSON.stringify(connection.policy),connectionGeneration:'generation',generation:'access',proofRevision:3,originalCandidateId:'a',nativeId:'*1',outIface:'',originalRuleCiphertext:'original',desiredRuleCiphertext:'desired',desiredTargetProof:'desired-proof',beforeRules:[],receiptId:randomUUID()};
const input={serverId:1,profile,candidateId:'b',principal,ownerId:'owner',policyProof:JSON.stringify(connection.policy),connectionGeneration:'generation',generation:'access',proofRevision,originalCandidateId:'a',nativeId:'*1',outIface:'',originalRuleCiphertext:'original',desiredRuleCiphertext:'desired',desiredTargetProof:'desired-proof',beforeRules:[],receiptId:randomUUID()};
if (manualEffectPolicy) input.manualEffectPolicy = manualEffectPolicy;
if(mode!=='legacy')input.originalEffectSelection=mode==='prepared'?{status:'prepared',probe:{key:'b',serverId:1,destination:'198.51.100.20',source:'198.51.100.10',interface:'wan',probeVrf:'probe'}}:{status:'unknown',reason:'unsupported-source'};
let wrote=false,handler,network,foreign=false,reads=0,rounds=0,closed=0,ownerClosed=false,nativeCalls=100;
const unknown=reason=>({scope:'prepared-route-fib-adjacency-control-effects',status:'unknown',reason,rounds:[],logCoverage:'unknown',semanticProof:false,forwardingVerified:false,autoRecoveryAllowed:false});
const report={...unknown(null),status:'observed-expected',rounds:[{controlProbe:'passed'},{controlProbe:'passed'}]};
const session={sharedContext:{},readSnapshot:async stage=>{calls.push('baseline:'+stage);return {};},readConfigurationBoundForwardSnapshot:async()=>{calls.push('round:'+(++rounds));if(failure==='round'||failure==='round-closed'){ownerClosed=failure==='round-closed';throw Error('unknown');}return {};},readBudget:()=>({deadline:Date.now()+10000,calls:nativeCalls,bytes:1000,rows:50,closed:ownerClosed}),close:()=>{closed++;ownerClosed=true;calls.push('close-session');}};
const security = {accessGeneration:async()=>'access',decryptSecret:async x=>x,encryptSecret:async x=>x,fingerprint:JSON.stringify,McpError:FixtureMcpError,privateSecretFingerprint:async()=>'seal',redact:x=>x};
const { readScopedPathCheckpoint } = await loadModel('./routing-community-scoped-request.ts', name => {
if (name === './path-proof-purpose.js') return pathProofPurpose;
if (name === './security.js') return security;
return require(name);
});
const pathCheckId = randomUUID(), observedAt = new Date();
const pathCheckpoint = { id: pathCheckId, connection_id: principal.connectionId, status: 'passed', created_at: observedAt, evidence: { observedAt: observedAt.toISOString() } };
const providers={
'node:crypto':{randomUUID},zod:{z},'@mmapp/contracts/mcp-routing':{routeAiProfileSchema:{parse:x=>x}},'../../db/index.js':{pool:{query:async sql=>({rows:String(sql).includes('FROM mcp_path_checks')?[{id:randomUUID(),connection_id:principal.connectionId,status:'passed',created_at:new Date(),evidence:{}}]:[]})}},
'node:crypto':{randomUUID},zod:{z},'@mmapp/contracts/mcp-routing':{routeAiProfileSchema:{parse:x=>x}},'../../db/index.js':{pool:{query:async (sql, values)=>({rows:String(sql).includes('FROM mcp_path_checks')&&values?.[0]===pathCheckId&&values?.[1]===principal.connectionId?[pathCheckpoint]:[]})}},
'../../lib/permissions.js':{hasPermission:(list,value)=>list.includes(value)},
'../../services/config-mutation-policy.js':{assertVerifiedConfigurationAccess:async()=>{},executeGuardedConfigurationWrite:async(_sid,_method,_path,work)=>{calls.push('guarded-intent');return work();}},
'../../services/config-mutation-lock.js':{withApplicationMutationLock:work=>work(),withConfigMutationLock:(_ids,work)=>work()},
'./store.js':{effectiveMcpConnection:async()=>connection,audit:async()=>{}},
'./security.js':{accessGeneration:async()=>'access',decryptSecret:async x=>x,encryptSecret:async x=>x,fingerprint:JSON.stringify,McpError:Error,privateSecretFingerprint:async()=>'seal',redact:x=>x},
'../../services/routing-filter-body.js': filterBody,
'./store.js':{effectiveMcpConnection:async()=>connection,audit:async(_db,_owner,action)=>{calls.push(action);}},
'./security.js':security,
'./routing-community-scoped-checkpoint.js':{saveScopedConfigurationCheckpoint:async()=>{calls.push('CONFIG_APPLIED');},loadScopedConfigurationCheckpoint:async()=>({checkpointProof:'configuration'}),saveScopedNetworkCheckpoint:async(_input,_seal,check,originalEffects)=>{calls.push('save-network');network={status:check.status,networkSeal:'network',...(originalEffects?{originalEffects}:{})};},loadScopedNetworkCheckpoint:async()=>network},
'./routing-community-scoped-request.js':{assertScopedCommunityRequest:async()=>'request'},'./management-transport.js':{prepareManagementTransportBinding:async()=>({})},
'./routing-community-scoped-request.js':{assertScopedCommunityRequest:async()=>'request',readScopedPathCheckpoint},'./management-transport.js':{prepareManagementTransportBinding:async()=>({})},
'./routing-community-context.js':{readRouteCommunityContext:async()=>{reads++;return {before:{scopedExecutionAvailable:true,currentCandidateId:wrote?'b':'a',dependencyProof:'dep',sourceProofs:[{serverId:1,proof:'source'}]},ownedTarget:{nativeId:'*1',rawRow:{rule:wrote?'desired':'original'},endpoint:{},desiredTargetProof:()=> 'desired-proof'},fence:async()=>{},clients:new Map([[1,{get:async()=>[{'.id':'*r'}]}]]),captureOriginalEffectSelection:async()=>{calls.push('capture-prepared-selection');return input.originalEffectSelection;}};}},
'./routing-community-context-model.js':{candidateMatchesGateway:()=>true},'./routing-model.js':{rankRoutingCandidates:()=>profile.candidates.map(candidate=>({candidate,eligible:true}))},'./routing-observations.js':{readRoutingObservations:async()=>[]},
'./path-verification.js':{requireRecentPath:async()=>'path-check',checkCandidatePath:async()=>{calls.push('legacy-probe');return {id:randomUUID(),candidateId:'b',status:'passed',observedAt:new Date().toISOString()};}},
'./path-verification.js':{requireRecentPath:async()=>pathCheckId,checkCandidatePath:async()=>{calls.push('legacy-probe');return {id:randomUUID(),candidateId:'b',status:'passed',observedAt:new Date().toISOString()};}},
'./routing-community-scoped-model.js':{compileScopedCommunitySwitch:()=>({compiledBody:'desired',proposed:{community:'65000:10',gateway:'192.0.2.9',outIface:''},before:{community:'65000:10',gateway:'192.0.2.8',outIface:''}}),scopedConfigurationMatches:()=>({configurationMatchesOriginal:!wrote,configurationMatchesDesired:wrote&&!foreign})},
'./routing-community-scoped-native.js':{patchScopedCommunity:async()=>{calls.push('PATCH');wrote=true;}},'./service-registry.js':{registerMcpService:(_name,value)=>{handler=value;}},
'./routing-profile-authority.js':{assertPersistentScopedProfileAuthority:async()=>{},resolveRoutingProfileAuthority:async()=>{},routingProfileAuthorityBindingSchema:{parse:x=>x},scopedProfileAuthorityTransport:async()=>({})},
@@ -33,19 +64,57 @@ async function fixture(mode='prepared',failure){
'./routing-network-execution-session.js':{createRoutingNetworkSessionForCurrentExecution:async options=>{calls.push('open-session');assert.deepEqual(Object.keys(options).sort(),['probes','servers']);if(failure==='open')throw Error('unknown');return session;}},
'./routing-network-configuration-bridge.js':{resolveRoutingNetworkForwardRoundConfigurationContext:async()=>{calls.push('configuration-round-owner');return {};},closeRoutingNetworkForwardRoundConfigurationContext:()=>{calls.push('close-round-owner');}},
'./routing-network-scoped-context.js':{readRoutingNetworkScopedCommunityContext:async()=>{calls.push('shared-publication');if(failure==='publication')throw Error('DO_NOT_EXPOSE_TEST_DETAIL');nativeCalls+=10;return{before:{dependencyProof:'dep',targetRuleProof:'desired-proof',currentCandidateId:'b'},fence:async()=>{}};}},
'./routing-original-effects.js':{originalEffectSelectionSchema:{safeParse:x=>x?{success:true,data:x}:{success:false}},unknownOriginalEffects:unknown,originalEffectBaselineMatches:()=>failure!=='baseline',inspectOriginalEffectRounds:async()=>{calls.push('inspect-effects');if(failure==='foreign')foreign=true;return report;}}
'./routing-original-effects.js':{originalEffectReportSchema,originalEffectSelectionSchema:{safeParse:x=>x?{success:true,data:x}:{success:false}},unknownOriginalEffects:unknown,originalEffectBaselineMatches:()=>failure!=='baseline',inspectOriginalEffectRounds:async()=>{calls.push('inspect-effects');if(failure==='foreign')foreign=true;return report;}}
};
const exports={};new Function('exports','require',code)(exports,name=>{assert.ok(providers[name],name);return providers[name];});exports.registerScopedCommunityService();
return {calls,input,run:()=>handler.execute(input,{operationId:randomUUID(),phase:'forward',stepIndex:0}),prepare:()=>exports.prepareScopedCommunitySwitch({profile,candidateId:'b'},{principal,connection}),stats:()=>({wrote,closed,reads}),network:()=>network};
}
test('actual service consumer baselines before the single PATCH and persists separate effects after genuine CONFIG_APPLIED and manual probe',async()=>{const f=await fixture(),result=await f.run();const c=f.calls;assert(c.indexOf('baseline:before')<c.indexOf('PATCH'));assert(c.indexOf('CONFIG_APPLIED')<c.indexOf('configuration-round-owner'));assert(c.indexOf('legacy-probe')<c.indexOf('round:1'));assert(c.indexOf('round:2')<c.indexOf('shared-publication'));assert(c.indexOf('shared-publication')<c.indexOf('save-network'));assert.equal(c.filter(x=>x==='PATCH').length,1);assert.equal(result.configurationVerified,true);assert.equal(result.controlProbeVerified,true);assert.equal(result.forwardingVerified,false);assert.equal(result.originalEffects.status,'observed-expected');assert.equal(result.originalEffects.budget.calls,110);assert.equal(f.stats().closed,1);assert.deepEqual(result.originalEffects,f.network().originalEffects);});
for(const mode of ['legacy','unknown'])test(`existing ${mode} manual path does not open native effect session`,async()=>{const f=await fixture(mode),result=await f.run();assert(!f.calls.includes('open-session'));assert.equal(result.configurationVerified,true);assert.equal(result.controlProbeVerified,true);assert.equal(f.calls.filter(x=>x==='PATCH').length,1);});
for(const failure of ['open','baseline','round','round-closed','publication'])test(`optional ${failure} uncertainty preserves actual manual config/control receipt with effects unknown`,async()=>{const f=await fixture('prepared',failure),result=await f.run();assert.equal(result.configurationVerified,true);assert.equal(result.controlProbeVerified,true);assert.equal(result.originalEffects.status,'unknown');assert.equal(f.calls.filter(x=>x==='PATCH').length,1);assert(f.calls.includes('save-network'));});
for (const mode of ['legacy', 'unknown']) test(`existing ${mode} manual path does not open native effect session`, async () => {
const f = await fixture(mode, undefined, { proofRevision: 2 });
const result = await f.run();
assert(!f.calls.includes('open-session'));
assert.equal(result.configurationVerified, true);
assert.equal(result.controlProbeVerified, true);
assert.equal(f.calls.filter(call => call === 'PATCH').length, 1);
});
for (const failure of ['open', 'baseline', 'round', 'round-closed', 'publication']) test(`optional ${failure} uncertainty preserves config/control receipt with effects unknown`, async () => {
const f = await fixture('prepared', failure, ['open', 'baseline'].includes(failure)
? { proofRevision: 2, manualEffectPolicy: 'observational-v1' }
: { proofRevision: 3 });
const result = await f.run();
assert.equal(result.configurationVerified, true);
assert.equal(result.controlProbeVerified, true);
assert.equal(result.originalEffects.status, 'unknown');
assert.equal(f.calls.filter(call => call === 'PATCH').length, 1);
assert(f.calls.includes('save-network'));
});
for (const [description, mode, failure] of [
['missing selection', 'legacy'],
['unknown selection', 'unknown'],
['unavailable native session', 'prepared', 'open'],
['baseline drift', 'prepared', 'baseline'],
]) {
test(`persistent revision 3 refuses ${description} before dispatch`, async () => {
const f = await fixture(mode, failure, { proofRevision: 3 });
await assert.rejects(f.run, { code: 'MEASUREMENT_UNKNOWN' });
for (const call of ['routing.scoped.attempt', 'guarded-intent', 'PATCH', 'CONFIG_APPLIED', 'save-network']) {
assert(!f.calls.includes(call), call);
}
});
}
test('foreign configuration drift after optional effect observations still refuses publication, never repeats PATCH',async()=>{const f=await fixture('prepared','foreign');await assert.rejects(f.run);assert.equal(f.calls.filter(x=>x==='PATCH').length,1);assert(!f.calls.includes('save-network'));assert.equal(f.stats().closed,1);});
test('revision2 remains on existing manual path even with a prepared-like field',async()=>{const f=await fixture();f.input.proofRevision=2;await f.run();assert(!f.calls.includes('open-session'));});
test('sealed manual policy runs read-only rounds through the real scoped consumer',async()=>{const f=await fixture();f.input.proofRevision=2;f.input.manualEffectPolicy='observational-v1';const result=await f.run();assert.equal(f.calls.filter(x=>x==='PATCH').length,1);assert(f.calls.includes('round:2'));assert.equal(result.originalEffects.status,'observed-expected');});
test('actual preparation consumes the required real-context selection method and includes it in sealed service input',async()=>{const f=await fixture(),evidence=await f.prepare();assert(f.calls.includes('capture-prepared-selection'));assert.deepEqual(evidence.steps[0].input.originalEffectSelection,f.input.originalEffectSelection);assert.equal(evidence.steps[0].input.requestProof,'seal');assert(!f.calls.includes('PATCH'));});
test('closed effect owner falls back to exactly the old final context read without bypassing publication guard',async()=>{const legacy=await fixture('legacy');await legacy.run();const f=await fixture('prepared','round-closed');await f.run();assert(!f.calls.includes('shared-publication'));assert.equal(f.stats().reads,legacy.stats().reads);});
test('closed effect owner falls back to exactly the old final context read without bypassing publication guard', async () => {
const legacy = await fixture('legacy', undefined, { proofRevision: 2 });
await legacy.run();
const f = await fixture('prepared', 'round-closed', { proofRevision: 2, manualEffectPolicy: 'observational-v1' });
await f.run();
assert(!f.calls.includes('shared-publication'));
assert.equal(f.stats().reads, legacy.stats().reads);
});
test('optional publication error exposes only a bounded failure category', async()=>{
const f=await fixture('prepared','publication'), result=await f.run();
@@ -10,6 +10,20 @@ const effectCode=ts.transpileModule(await readFile(new URL('./routing-original-e
const endpointCode=ts.transpileModule(await readFile(new URL('./routing-native-endpoint.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText,endpointExports={};
new Function('exports','require',endpointCode)(endpointExports,require);
new Function('exports','require',effectCode)(effectExports,name=>{if(name==='node:net')return require('node:net');if(name==='zod')return{z};if(name==='./security.js')return{fingerprint:hash};if(name==='./routing-community-context-model.js')return{};if(name==='./routing-native-endpoint.js')return endpointExports;throw Error(name);});
async function loadModel(path, resolveDependency) {
const source = await readFile(new URL(path, import.meta.url), 'utf8');
const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText;
const exports = {};
new Function('exports', 'require', code)(exports, resolveDependency);
return exports;
}
const filterBody = await loadModel('../../services/routing-filter-body.ts', require);
const filterRules = await loadModel('../../services/filter-rules-write-model.ts', require);
const { scopedTargetRuleMatches } = await loadModel('./routing-community-scoped-model.ts', name => {
if (name === '../../services/routing-filter-body.js') return filterBody;
if (name === '../../services/filter-rules-write-model.js') return filterRules;
return require(name);
});
const preparedValue={status:'prepared',revision:1,serverId:1,routeSelectionScope:'table-afi-community-observation',candidateId:'b',family:'ipv4',table:'tenant',community:'65000:10',expectedGateway:'192.0.2.9',expectedImmediate:{address:'198.51.100.2',interface:'ether2'},binding:{connectionId:'*c',sessionId:'*s',instance:'default',table:'tenant',vrf:'main',localAddress:'192.0.2.1',remoteAddress:'192.0.2.2',remoteAsn:65001,afi:'ip'},bfd:{applicability:'not-configured',evidence:'explicit-use-bfd-false'},routes:[{nativeId:'*r',prefix:'203.0.113.0/24',originalGateway:'192.0.2.8'}],probe:{key:'b',serverId:1,destination:'198.51.100.20',source:'198.51.100.10',interface:'wan',probeVrf:'probe',expectedHops:['198.51.100.20']}};
async function fixture(selection='absent',effectStatus='observed-expected',switched=true){
const principal={connectionId:randomUUID(),credentialId:randomUUID(),credentialExpiresAt:new Date(Date.now()+120000).toISOString()},profileId=randomUUID(),planId=randomUUID(),operationId=randomUUID(),grant=new Date(Date.now()+60000).toISOString();
@@ -21,13 +35,24 @@ async function fixture(selection='absent',effectStatus='observed-expected',switc
if(selection!=='absent')input.originalEffectSelection=selection?.status==='prepared'&&Object.keys(selection).length===1?structuredClone(preparedValue):selection;
const packet={revision:2,operationId,stepIndex:0,nativeId:'*1',originalCandidateId:'a',originalTargetProof:originalTarget,dependencyProof:dependency,contextSources:sourceProofs,desiredCandidateId:'b',desiredTargetProof:desiredTarget,desiredRule:'desired'};
const network={status:'passed',networkSeal,...(effectStatus==='missing'?{}:{originalEffects:{status:effectStatus,switchEffectObserved:switched,selectionIdentity:hash(input.originalEffectSelection??null),transition:{changedPrefixes:switched?['203.0.113.0/24']:[]}}})};
const readNativeContext = async () => {
nativeReads++;
const rawRow = { rule: advancing ? 'desired' : 'original' };
const targetRuleProof = advancing ? desiredTarget : originalTarget;
const target = { nativeId: '*1', rawRow, desiredTargetProof: body => body === rawRow.rule ? targetRuleProof : hash(body) };
return {
before: { scopedExecutionAvailable: true, currentCandidateId: advancing ? 'b' : 'a', targetRuleProof, dependencyProof: dependency, sourceProofs, nativeRuleId: '*1' },
ownedTarget: { ...target, matchesRuleProof: (body, proof) => scopedTargetRuleMatches(target, body, proof) },
fence: async () => {},
};
};
const providers={
'./routing-main-transition.js':{},'../../db/index.js':{pool:{query:async()=>({rows:[row]})}},'./registry.js':{findMcpOperation:()=>({version:7})},
'./routing-community-scoped-request.js':{assertScopedCommunityRequest:async()=>requestSeal},'./routing-profile-authority.js':{assertPersistentScopedProfileAuthority:async()=>{},routingProfileAuthorityBindingSchema:{parse:x=>x}},
'./routing-community-scoped-checkpoint.js':{loadScopedConfigurationPacket:async()=>({packet,checkpointProof,observedAt:new Date(Date.now()-500).toISOString()}),loadScopedNetworkCheckpoint:async()=>network},
zod:{z},'@mmapp/contracts/mcp-routing':{routeAiProfileSchema:{parse:x=>x}},'../../lib/permissions.js':{hasPermission:(list,value)=>list.includes(value)},'./store.js':{effectiveMcpConnection:async()=>connection},
'./security.js':{accessGeneration:async()=>'access',constantEqual:(a,b)=>a===b,decryptSecret:async x=>x,fingerprint:hash,redact:x=>x,McpError:Error,privateSecretFingerprint:sign},
'./routing-community-context.js':{readRouteCommunityContext:async()=>{nativeReads++;return {before:{scopedExecutionAvailable:true,currentCandidateId:advancing?'b':'a',targetRuleProof:advancing?desiredTarget:originalTarget,dependencyProof:dependency,sourceProofs,nativeRuleId:'*1'},ownedTarget:{nativeId:'*1',rawRow:{rule:advancing?'desired':'original'}},fence:async()=>{}};}},
'./routing-community-context.js':{readRouteCommunityContext: readNativeContext},
'./routing-original-effects.js':{originalEffectSelectionSchema:effectExports.originalEffectSelectionSchema}
};
const exports={};new Function('exports','require',code)(exports,name=>{assert.ok(providers[name],name);return providers[name];});
+35 -20
View File
@@ -11,18 +11,29 @@ import { accessGeneration, McpError } from "./security.js"
import { audit, validatePrincipal, type McpPrincipal } from "./store.js"
import { decideRoutingSwitch, selectFailoverReserveProbes, type ControllerState, type PathHealth, type rankRoutingCandidates } from "./routing-model.js"
import type { add } from "./actions.js"
import type { ActionContext } from "./action-types.js"
import { COMMUNITY_PATH_MAX_AGE_MS } from "./path-proof-purpose.js"
import { controllerProfileCurrent, publishControllerState, saveControllerProfileCas, withControllerProfileLease } from "./routing-controller-fence.js"
import { assertIdentifiedRoutingCompletion, createRoutingReservationStore } from "./routing-reservation-store.js"
const targetIds = (profile: RouteAiProfile) => [...new Set([profile.serverId, ...profile.candidates.flatMap(candidate => [candidate.jumpHostId, candidate.exitNodeId])])]
const profilePublicColumns = "id,version,config,state,enabled,grant_expires_at,updated_at"
const profileInScope = (config: unknown, serverIds: number[]) => targetIds(routeAiProfileSchema.parse(config)).every(id => serverIds.includes(id))
const profileSaveSchema = z.object({ profileId: z.string().uuid().optional(), profile: routeAiProfileSchema, enabled: z.boolean().default(false), expiresAt: z.string().datetime({ offset: true }) }).strict()
function assertControllerCanBeEnabled(profile: RouteAiProfile) {
const reason = routeAiControllerBlockReason(profile)
if (reason) throw new McpError("UNSUPPORTED_CAPABILITY", reason, 409)
}
export function registerRoutingProfileActions(register: typeof add) {
register("routing.profiles.read", "mm_route_profiles_list", "Профили и состояние контроллера Route AI", "network", "mm:network:read", "read", "connection", z.object({}).strict(), {
read: async (_input, context) => ({ profiles: (await pool.query("SELECT id,version,config,state,enabled,grant_expires_at,updated_at FROM mcp_routing_profiles WHERE connection_id=$1 ORDER BY updated_at DESC LIMIT 200", [context.connection.id])).rows.filter(row => targetIds(routeAiProfileSchema.parse(row.config)).every(id => context.connection.policy.scope.serverIds.includes(id))).map(row=>({...row,state:publicRoutingControllerState(row.state)})) }),
read: async (_input, context) => ({ profiles: (await pool.query(`SELECT ${profilePublicColumns} FROM mcp_routing_profiles WHERE connection_id=$1 ORDER BY updated_at DESC LIMIT 200`, [context.connection.id])).rows.filter(row => profileInScope(row.config, context.connection.policy.scope.serverIds)).map(row => ({ ...row, state: publicRoutingControllerState(row.state) })) }),
})
register("routing.controller.read", "mm_route_controller_get", "Состояние выбранного контроллера Route AI", "network", "mm:network:read", "read", "connection", z.object({ profileId: z.string().uuid() }).strict(), {
read: async (input, context) => {
const { rows: [row] } = await pool.query(`SELECT ${profilePublicColumns} FROM mcp_routing_profiles WHERE id=$1 AND connection_id=$2`, [input.profileId, context.connection.id])
if (!row || !profileInScope(row.config, context.connection.policy.scope.serverIds)) throw new McpError("NOT_FOUND", "Профиль не найден", 404)
return { ...row, state: publicRoutingControllerState(row.state) }
},
})
registerMcpService("routing.profile.save", {
current: async input => (await pool.query("SELECT id,version,config,enabled,grant_expires_at FROM mcp_routing_profiles WHERE id=$1 AND connection_id=$2", [input.profileId, input.connectionId])).rows[0] ?? null,
@@ -40,24 +51,28 @@ export function registerRoutingProfileActions(register: typeof add) {
},
verify: async (input, result) => !!result && (await pool.query("SELECT config,state,enabled,version,credential_id,generation,policy_version,grant_expires_at FROM mcp_routing_profiles WHERE id=$1 AND connection_id=$2", [input.profileId, input.connectionId])).rows.some(row => row.version === (result as { version: number }).version && row.version === (input.expectedVersion === null ? 1 : Number(input.expectedVersion) + 1) && JSON.stringify(routeAiProfileSchema.parse(row.config)) === JSON.stringify(routeAiProfileSchema.parse(input.profile)) && row.enabled === input.enabled && (!input.enabled || fingerprint(row.state.baseline)===fingerprint(input.baseline)) && row.credential_id === input.credentialId && row.generation === input.generation && row.policy_version === input.policyVersion && row.grant_expires_at.getTime() === Date.parse(String(input.expiresAt))),
})
register("routing.profiles.save", "mm_route_profile_save_plan", "Сохранить профиль и временный допуск контроллера", "network", "mm:network:write", "write", "servers", z.object({ profileId: z.string().uuid().optional(), profile: routeAiProfileSchema, enabled: z.boolean().default(false), expiresAt: z.string().datetime({ offset: true }) }).strict(), {
target: input => ({ scope: "servers", serverIds: targetIds(routeAiProfileSchema.parse(input.profile)) }),
prepare: async (input, context) => {
const profile = routeAiProfileSchema.parse(input.profile), ids = targetIds(profile), profileId = input.profileId ?? randomUUID()
if (input.enabled) assertControllerCanBeEnabled(profile)
const expires = Date.parse(String(input.expiresAt)), credentialExpires = Date.parse(context.principal.credentialExpiresAt)
if (expires <= Date.now() || expires > Math.min(Date.now() + 86400000, credentialExpires, Date.parse(context.connection.policy.expiresAt), context.connection.owner_valid_until.getTime())) throw new McpError("INVALID_EXPIRATION", "Допуск контроллера ограничен 24 часами и сроком подтверждения прав владельца", 400)
const existing = (await pool.query("SELECT id,connection_id,version,config,enabled,grant_expires_at FROM mcp_routing_profiles WHERE id=$1", [profileId])).rows[0]
if (existing && existing.connection_id !== context.connection.id) throw new McpError("NOT_FOUND", "Профиль не найден", 404)
if (input.enabled && !["routing.analyze", "routing.community.switch"].every(id => context.connection.policy.scope.operationIds.includes(id))) throw new McpError("OPERATION_DENIED", "Для контроллера нужны отдельные разрешения анализа и переключения")
if(input.enabled)await assertNoActiveRoutingProfileOverlap(String(profileId),profile)
const baseline=input.enabled?await prepareRoutingControllerBaseline(String(profileId),(existing?.version??0)+1,profile,String(input.expiresAt),context):undefined
const { connection_id: _connection, ...before } = existing ?? {}
return { target: { scope: "servers", serverIds: ids }, objects: ids.map(serverId => ({ kind: "server", id: String(serverId), serverId })), snapshots: [], before: existing ? before : null,
steps: [{ kind: "service", service: "routing.profile.save", input: { ...input, profileId, expectedVersion: existing?.version ?? null, connectionId: context.connection.id, baseline, credentialId: context.principal.credentialId, generation: await accessGeneration(), policyVersion: context.connection.policy.version } }], requiresHuman: true,
warnings: ["Контроллер использует исходный доступ, лимиты и подтверждения. Отзыв токена или изменение политики останавливает его.", "При неизвестном пути или результате записи контроллер приостанавливается."] }
},
version: 4,
const profileTarget = (input: Record<string, unknown>) => ({ scope: "servers" as const, serverIds: targetIds(routeAiProfileSchema.parse(input.profile)) })
const prepareProfileSave = (updateOnly: boolean) => async (input: Record<string, unknown>, context: ActionContext) => {
const profile = routeAiProfileSchema.parse(input.profile), ids = targetIds(profile), profileId = input.profileId ?? randomUUID()
if (input.enabled) assertControllerCanBeEnabled(profile)
const expires = Date.parse(String(input.expiresAt)), credentialExpires = Date.parse(context.principal.credentialExpiresAt)
if (expires <= Date.now() || expires > Math.min(Date.now() + 86400000, credentialExpires, Date.parse(context.connection.policy.expiresAt), context.connection.owner_valid_until.getTime())) throw new McpError("INVALID_EXPIRATION", "Допуск контроллера ограничен 24 часами и сроком подтверждения прав владельца", 400)
const existing = (await pool.query("SELECT id,connection_id,version,config,enabled,grant_expires_at FROM mcp_routing_profiles WHERE id=$1", [profileId])).rows[0]
if (existing ? existing.connection_id !== context.connection.id || !profileInScope(existing.config, context.connection.policy.scope.serverIds) : updateOnly) throw new McpError("NOT_FOUND", "Профиль не найден", 404)
const affectedIds = existing ? [...new Set([...ids, ...targetIds(routeAiProfileSchema.parse(existing.config))])] : ids
if (input.enabled && !["routing.analyze", "routing.community.switch"].every(id => context.connection.policy.scope.operationIds.includes(id))) throw new McpError("OPERATION_DENIED", "Для контроллера нужны отдельные разрешения анализа и переключения")
if(input.enabled)await assertNoActiveRoutingProfileOverlap(String(profileId),profile)
const baseline=input.enabled?await prepareRoutingControllerBaseline(String(profileId),(existing?.version??0)+1,profile,String(input.expiresAt),context):undefined
const { connection_id: _connection, ...before } = existing ?? {}
return { target: { scope: "servers" as const, serverIds: affectedIds }, objects: affectedIds.map(serverId => ({ kind: "server", id: String(serverId), serverId })), snapshots: [], before: existing ? before : null,
steps: [{ kind: "service" as const, service: "routing.profile.save", input: { ...input, profileId, expectedVersion: existing?.version ?? null, connectionId: context.connection.id, baseline, credentialId: context.principal.credentialId, generation: await accessGeneration(), policyVersion: context.connection.policy.version } }], requiresHuman: true,
warnings: ["Контроллер использует исходный доступ, лимиты и подтверждения. Отзыв токена или изменение политики останавливает его.", "При неизвестном пути или результате записи контроллер приостанавливается."] }
}
register("routing.profiles.save", "mm_route_profile_save_plan", "Сохранить профиль и временный допуск контроллера", "network", "mm:network:write", "write", "servers", profileSaveSchema, {
target: profileTarget, prepare: prepareProfileSave(false), version: 5,
})
register("routing.controller.update", "mm_route_controller_update_plan", "Обновить профиль и допуск контроллера Route AI", "network", "mm:network:write", "write", "servers", profileSaveSchema.extend({ profileId: z.string().uuid(), enabled: z.boolean() }), {
target: profileTarget, prepare: prepareProfileSave(true),
})
}
@@ -109,7 +124,7 @@ export async function tickRoutingProfiles() {
await assertRoutingControllerBaseline(state.baseline,row.id,row.version,profile,row.grant_expires_at.toISOString(),{principal,connection})
if (state.probedAt && Date.now() - Date.parse(state.probedAt) < 55000) return
await current()
const analysis = await invokeMcpTool(principal, "mm_route_optimizer_analyze", { profile }) as { ranking: ReturnType<typeof rankRoutingCandidates>; currentCandidateId: string | null }
const analysis = await invokeMcpTool(principal, "mm_route_optimizer_analyze", { profile, limit: 100 }) as { ranking: ReturnType<typeof rankRoutingCandidates>; currentCandidateId: string | null }
if (state.expectedCandidateId && analysis.currentCandidateId !== state.expectedCandidateId) { await stop("Наблюдаемый путь изменился вне ожидаемого переключения; требуется проверка вмешательства или дрейфа"); return }
if (analysis.currentCandidateId) next.expectedCandidateId = analysis.currentCandidateId
if (profile.candidates.some(candidate => candidate.controlProbe) && !connection.policy.scope.operationIds.includes("routing.path.verify")) { await stop("Не выдано право контрольной проверки пути"); return }
+61 -36
View File
@@ -1,3 +1,4 @@
import { withPartitionWrite } from "../../services/partition-write-transaction.js"
import { pollErrorLog } from "../../services/server-poll-error-provenance.js"
import { vrfPreparationDiagnostic } from "./vrf-preparation-diagnostic.js"
import { createVrfRuntimeDiagnosticBoundary } from "../../services/vrf-execution-diagnostic.js"
@@ -31,9 +32,11 @@ import { findAction, findActionByOperation } from "./actions.js"
import { refuseExpiredOperationReplay } from "./operation-replay-tombstones.js"
import { accessGeneration, decryptSecret, encryptSecret, fingerprint, privateSecretFingerprint, McpError, redact } from "./security.js"
import { audit, getConnection, transaction, validatePrincipal, effectiveMcpConnection, assertConnectionVisible, type McpPrincipal, type ConnectionRow, type StoredPlan, type StoredOperation } from "./store.js"
import { executeRosStep, readRosSnapshot, resourceById, routerClient, type RosSnapshot } from "./resources.js"
import { executeRosStep, readRosSnapshot, routerClient, type RosSnapshot } from "./resources.js"
import { verifyHealthChecks } from "./routing-health-checks.js"
import { verifyRosStepSnapshots } from "./ros-step-verification.js"
import { createRosPrefix } from "./ros-prefix.js"
import { checkRpfPolicy } from "./routing-controls.js"
import type { ActionContext, ChangeEvidence, ChangeStep } from "./action-types.js"
import { logsFollowCancellation } from "./logs-follow-cancellation.js"
import { findMcpService, type McpServiceExecutionContext } from "./service-registry.js"
@@ -115,10 +118,11 @@ async function reserveCall(principal: McpPrincipal, toolName: string, input: Rec
if (reservation.result.decision === "denied") throw new McpError(reservation.result.reasons[0]?.code ?? "PERMISSION_DENIED", reservation.result.reasons.map(reason => reason.message).join("; "))
return { action, requestId, context: reservation.context }
}
async function completeRequest(id: string, started: number, result: unknown, error?: unknown) {
async function completeRequest(id: string, started: number, result: unknown, error?: unknown, assertDeliveryAccess?: (client: PoolClient) => Promise<void>) {
await transaction(async client => {
await client.query("UPDATE mcp_requests SET status=$2,result=$3::jsonb,error_code=$4,duration_ms=$5,finished_at=now() WHERE id=$1", [id, error ? "error" : "completed", JSON.stringify(redact(error ? { message: error instanceof McpError ? error.message : "Внутренняя ошибка операции" } : result)), error instanceof McpError ? error.code : error ? "INTERNAL_ERROR" : null, Math.min(2147483647, Date.now() - started)])
await client.query("DELETE FROM mcp_read_leases WHERE id=$1", [id])
await assertDeliveryAccess?.(client)
})
}
export async function invokeMcpTool(principal: McpPrincipal, toolName: string, raw: unknown, signal?: AbortSignal, internal?: { persistentRoutingProfile: { id: string; version: number } }): Promise<unknown> {
@@ -198,11 +202,16 @@ async function invokeMcpToolInner(principal: McpPrincipal, toolName: string, raw
vrfPreparing = false
} else throw new McpError("UNSUPPORTED_CAPABILITY", "Нет обработчика операции", 409)
assertActive()
const current = await effectiveMcpConnection(principal)
assertActive()
if (fingerprint(current.policy) !== fingerprint(call.context.connection.policy) || fingerprint(current.owner_permissions) !== fingerprint(call.context.connection.owner_permissions)) throw new McpError("STATE_CONFLICT", "Доступ изменился во время вызова; результат не выдан", 409)
const assertCurrentResultAccess = async (client?: PoolClient) => {
const current = await effectiveMcpConnection(principal, client)
assertActive()
if (fingerprint(current.policy) !== fingerprint(call.context.connection.policy) || fingerprint(current.owner_permissions) !== fingerprint(call.context.connection.owner_permissions)) throw new McpError("STATE_CONFLICT", "Доступ изменился во время вызова; результат не выдан", 409)
}
await assertCurrentResultAccess()
const output = redact(result)
await completeRequest(call.requestId, started, output)
const checkDelivery = action.operation.section === "logs" && action.operation.effect === "read" ? assertCurrentResultAccess : undefined
await completeRequest(call.requestId, started, output, undefined, checkDelivery)
await checkDelivery?.()
assertActive()
return output
} catch (error) {
@@ -350,6 +359,11 @@ export async function applyPlan(principal: McpPrincipal, planId: string, idempot
const action = findActionByOperation(plan.operation_id)
if (plan.policy_version !== connection.policy.version || plan.operation_version !== action.operation.version || plan.generation !== await accessGeneration() || plan.expires_at.getTime() <= Date.now()) throw new McpError("PLAN_EXPIRED", "План или его разрешения устарели", 409)
if (!["draft", "approved"].includes(plan.state)) throw new McpError("APPROVAL_REQUIRED", "План требует подтверждения или недоступен", 409)
for (const change of evidence.steps) {
if (change.kind === "ros" && change.step.resource === "ip_settings" && change.step.fields["rp-filter"] === "strict") {
checkRpfPolicy(change.step.serverId, evidence.snapshots, "strict")
}
}
let scopedPathExpiresAt: number | undefined
if (plan.operation_id === "routing.community.switch" && requiresScopedRoutingOwnerApproval(evidence)) {
if (plan.approved_by !== connection.owner_id) throw new McpError("APPROVAL_REQUIRED", "Переключение постоянного профиля подтверждает владелец подключения", 409)
@@ -463,6 +477,7 @@ async function executeOperationWithLease(operationId: string, principal: McpPrin
const plan = await loadPlan(operation.plan_id), evidence = JSON.parse(await decryptSecret(plan.evidence.sealed)) as ChangeEvidence
const serverIds = evidence.target.scope === "servers" ? evidence.target.serverIds : evidence.objects.flatMap(object => object.serverId ? [object.serverId] : [])
let attempted = false, confirmed = 0, activeServiceStep = false
let ecmpStoppedBeforeDispatch = false
let vrfDiagnosticFallback: ReturnType<typeof createVrfRuntimeDiagnosticBoundary> | undefined
let reservedForwardPassed = false
let reservedRecoverySummary: Record<string, unknown> | undefined
@@ -531,14 +546,25 @@ async function executeOperationWithLease(operationId: string, principal: McpPrin
if (productionVrf && (evidence.steps.length !== 1 || evidence.steps[0].kind !== "ros" || evidence.steps[0].step.resource !== "vrf")) throw new McpError("STATE_CONFLICT", "VRF execution требует exact single typed step", 409)
if (await routingReservationEvidence(plan, evidence) && (!routingReservationForward || !routingReservationId)) throw new McpError("STATE_CONFLICT", "No confirmed reserved forward admission", 409)
if (plan.operation_version !== findActionByOperation(plan.operation_id).operation.version) throw new McpError("PLAN_EXPIRED", "Версия операции изменилась до запуска", 409)
const checksResolvedNextHop = evidence.healthChecks?.some(check => check.field === "resolved") === true
if (plan.operation_id === "operations.recover" && !checksResolvedNextHop && evidence.steps.some(change => change.kind === "ros" && ["ipv4_route", "ipv6_route"].includes(change.step.resource))) {
const { operationId: sourceId } = JSON.parse(await decryptSecret(plan.payload.sealed)) as { operationId: string }
const { rows: [source] } = await pool.query("SELECT p.operation_id FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id WHERE o.id=$1 AND o.connection_id=$2", [sourceId, plan.connection_id])
if (!source || source.operation_id === "routing.ecmp.update") throw new McpError("PLAN_EXPIRED", "Подготовьте новый обратный план с проверкой next-hop", 409)
}
const applyWithLocks = () => withConfigMutationLock(serverIds, async () => {
await authorize()
const prefix = evidence.steps.some(step => step.kind === "ros") ? createRosPrefix(productionVrf ? evidence.snapshots.filter(snapshot => !isVrfCompanionResource(snapshot.resource)) : evidence.snapshots) : undefined
for (const change of evidence.steps) {
if (change.kind === "ros" && change.step.resource === "ip_settings" && change.step.fields["rp-filter"] === "strict") {
checkRpfPolicy(change.step.serverId, evidence.snapshots, "strict")
}
}
if (prefix) await prefix.check(authorizeCatalog)
else for (const snapshot of evidence.snapshots) {
if ((await readRosSnapshot(snapshot.serverId, snapshot.resource)).fingerprint !== snapshot.fingerprint) throw new McpError("STATE_CONFLICT", "Конфигурация изменилась после подготовки", 409)
}
await verifyHealthChecks(evidence, serverIds, false)
await verifyHealthChecks(evidence, serverIds, false, authorizeCatalog)
for (const step of evidence.steps) {
if (step.kind === "service" && fingerprint(await serviceCurrent(step.service, step.input)) !== fingerprint(evidence.before)) throw new McpError("STATE_CONFLICT", "Данные сервиса изменились", 409)
}
@@ -573,7 +599,24 @@ async function executeOperationWithLease(operationId: string, principal: McpPrin
const reservedStep: { outcome?: ReservedRoutingStepOutcome<unknown> } = {}
vrfDiagnosticFallback = productionVrf && step.kind === "ros" && step.step.resource === "vrf" && ["ros.vrf.create", "ros.vrf.update", "ros.vrf.delete", "routing.vrf.update"].includes(plan.operation_id)
? createVrfRuntimeDiagnosticBoundary(operationId, step.step.action === "update" ? "patch" : step.step.action, diagnostic => { console.error(JSON.stringify(diagnostic)) }) : undefined
const result = await withVerifiedConfigurationChange({ actorId: connection.owner_id, connectionId: connection.id, serverIds, objects: evidence.objects, approved: !!plan.approved_by, serverWide: step.kind === "service", ...(serviceExecution?{serviceExecution}:{}), authorize: async () => { await (step.kind === "ros" ? authorizeCatalog() : authorize()) }, ...(step.kind === "ros" ? { beforeDispatch: async () => { await prefix!.check(authorizeCatalog); await authorize(); await authorizeCatalog(); if (productionVrf) { if (!vrfBeforeDispatch) throw new McpError("STATE_CONFLICT", "VRF command guard отсутствует", 409); await vrfBeforeDispatch() } } } : {}) },
const beforeDispatch = step.kind === "ros" ? async () => {
await prefix!.check(authorizeCatalog)
if (checksResolvedNextHop) {
try {
await verifyHealthChecks(evidence, serverIds, false, authorizeCatalog)
} catch (error) {
ecmpStoppedBeforeDispatch = true
throw error
}
}
await authorize()
await authorizeCatalog()
if (productionVrf) {
if (!vrfBeforeDispatch) throw new McpError("STATE_CONFLICT", "VRF command guard отсутствует", 409)
await vrfBeforeDispatch()
}
} : undefined
const result = await withVerifiedConfigurationChange({ actorId: connection.owner_id, connectionId: connection.id, serverIds, objects: evidence.objects, approved: !!plan.approved_by, serverWide: step.kind === "service", ...(serviceExecution?{serviceExecution}:{}), authorize: async () => { await (step.kind === "ros" ? authorizeCatalog() : authorize()) }, beforeDispatch },
async () => {
vrfDiagnosticFallback?.executorEntered()
if (step.kind === "ros") {
@@ -634,7 +677,7 @@ async function executeOperationWithLease(operationId: string, principal: McpPrin
} else if (!await verifyStep(step, result)) throw new McpError("VERIFICATION_FAILED", "Запись подтверждена, но проверка результата не прошла", 409)
await pool.query("UPDATE mcp_operation_steps SET state=$3 WHERE operation_id=$1 AND step_index=$2", [operationId, index, evidence.verificationScope === "acknowledgement" ? "acknowledged" : "verified"])
}
await verifyHealthChecks(evidence, serverIds, true)
await verifyHealthChecks(evidence, serverIds, true, authorizeCatalog)
await prefix?.check(authorizeCatalog)
if (evidence.ospfOwnerGraph) {
await authorizeGrant()
@@ -675,39 +718,21 @@ async function executeOperationWithLease(operationId: string, principal: McpPrin
const diagnosticNotStarted = diagnosticOnly && confirmed === 0 && code === "READ_CANCELLED" &&
(error as { diagnosticNotStarted?: unknown } | null)?.diagnosticNotStarted === true &&
diagnosticProgress?.state === "cancelled" && diagnosticProgress.attemptedStages === 0 && diagnosticProgress.completedStages === 0
const state = !attempted || diagnosticNotStarted ? "rejected" : code === "VERIFICATION_FAILED" ? "partial" : "unknown"
const state = ecmpStoppedBeforeDispatch ? confirmed === 0 ? "rejected" : "partial"
: !attempted || diagnosticNotStarted ? "rejected" : code === "VERIFICATION_FAILED" ? "partial" : "unknown"
const dispatchOutcome = ecmpStoppedBeforeDispatch ? { failedStepDispatched: false } : {}
// Private operator STDERR only; never HTTP/result/audit payload.
try { const diagnostic = pollErrorLog(error, operationId, plan.operation_id); if (diagnostic) console.error(JSON.stringify(diagnostic)) } catch { /* Logging cannot replace operation outcome. */ }
const recovery = reservedRecoverySummary ?? (code === "VERIFICATION_FAILED" && confirmed === evidence.steps.length && evidence.recovery
? await performReservedRecovery(operation, plan, evidence, principal, serverIds)
: undefined)
await transaction(async client => {
await client.query("UPDATE mcp_operations SET state=$2,verification=$3,error_code=$4,result=$5::jsonb,finished_at=now() WHERE id=$1", [operationId, state, code === "VERIFICATION_FAILED" ? "failed" : "inconclusive", code, JSON.stringify({ confirmedWrites: diagnosticOnly ? 0 : confirmed, needsReview: attempted && !diagnosticNotStarted, recovery, ...(nativeProgress ? { nativeProgress } : {}), ...(diagnosticProgress ? { diagnosticProgress } : {}), ...(diagnosticOnly ? { completedServiceSteps: confirmed } : {}) })])
await client.query("UPDATE mcp_operations SET state=$2,verification=$3,error_code=$4,result=$5::jsonb,finished_at=now() WHERE id=$1", [operationId, state, code === "VERIFICATION_FAILED" ? "failed" : "inconclusive", code, JSON.stringify({ confirmedWrites: diagnosticOnly ? 0 : confirmed, needsReview: state !== "rejected", ...dispatchOutcome, recovery, ...(nativeProgress ? { nativeProgress } : {}), ...(diagnosticProgress ? { diagnosticProgress } : {}), ...(diagnosticOnly ? { completedServiceSteps: confirmed } : {}) })])
await client.query("UPDATE mcp_plans SET state='completed' WHERE id=$1", [plan.id])
await audit(client, "mcp-runner", "operation.stop", operation.connection_id, { operationId, state, code, confirmedWrites: diagnosticOnly ? 0 : confirmed, ...(nativeProgress ? { nativeProgress } : {}), ...(diagnosticProgress ? { diagnosticProgress } : {}) })
await audit(client, "mcp-runner", "operation.stop", operation.connection_id, { operationId, state, code, confirmedWrites: diagnosticOnly ? 0 : confirmed, ...dispatchOutcome, ...(nativeProgress ? { nativeProgress } : {}), ...(diagnosticProgress ? { diagnosticProgress } : {}) })
})
}
}
async function verifyHealthChecks(evidence: ChangeEvidence, serverIds: number[], afterWrite: boolean) {
const cache = new Map<string, unknown[]>()
for (const check of evidence.healthChecks ?? []) {
if (check.phase === "after" && !afterWrite) continue
if (!serverIds.includes(check.serverId)) throw new McpError("SERVER_DENIED", "Проверка находится вне плана")
if (check.field === "state" && check.resource !== "ospf_neighbor") throw new McpError("INVALID_INPUT", "Неизвестный критерий здоровья", 409)
if (check.field === "bfd-up" && check.resource !== "bfd_session") throw new McpError("INVALID_INPUT", "Неизвестный критерий BFD", 409)
if (check.field === "resolved" && !["ipv4_route", "ipv6_route"].includes(check.resource)) throw new McpError("INVALID_INPUT", "Неизвестный критерий next-hop", 409)
const key = `${check.serverId}:${check.resource}`
if (!cache.has(key)) {
const { client } = await routerClient(check.serverId), raw = await client.get<unknown>(resourceById(check.resource).path)
if (!Array.isArray(raw)) throw new McpError("STATE_UNAVAILABLE", "Ответ проверки здоровья не подтверждён", 503)
cache.set(key, raw)
}
const row = cache.get(key)!.find((value): value is Record<string, unknown> => !!value && typeof value === "object" && (value as Record<string, unknown>)[".id"] === check.id)
const identityMatches = row && Object.entries(check.identity ?? {}).every(([key, value]) => row[key] === value)
const healthy = identityMatches && row && (check.field === "resolved" ? typeof row["immediate-gw"] === "string" && row["immediate-gw"].trim().length > 0 && ![true, "true", "yes"].includes(row.invalid as boolean | string) && ![true, "true", "yes"].includes(row.disabled as boolean | string) : check.field === "bfd-up" ? String(row.state).toLowerCase() === "up" : check.field === "state" ? String(row.state).toLowerCase() === "full" : [true, "true", "yes"].includes(row[check.field] as boolean | string))
if (!healthy) throw new McpError(afterWrite ? "VERIFICATION_FAILED" : "STATE_CONFLICT", afterWrite ? "После записи не подтверждён next-hop, Full OSPF или Up BFD" : "Работоспособность next-hop/OSPF/BFD изменилась после подготовки", 409)
}
}
async function performReservedRecovery(operation: StoredOperation, plan: StoredPlan, evidence: ChangeEvidence, principal: McpPrincipal, serverIds: number[]) {
const recovery = evidence.recovery!
let attempted = false
@@ -757,7 +782,7 @@ export async function recoverMcpOperations() {
// A second backend must not interrupt a runner still holding its session lease.
const { rows } = await pool.query("SELECT id,connection_id FROM mcp_operations WHERE state IN ('pending','running') AND created_at<now()-interval '2 minutes' ORDER BY created_at LIMIT 100")
for (const row of rows) {
await transaction(async client => {
await withPartitionWrite(async ({ client }) => {
const { rows: [lock] } = await client.query("SELECT pg_try_advisory_xact_lock(hashtextextended($1,0)) AS locked", ["mcp-operation:" + row.id])
if (!lock.locked) return
const changed = await client.query("UPDATE mcp_operations SET state='unknown',verification='inconclusive',error_code='PROCESS_INTERRUPTED',finished_at=now() WHERE id=$1 AND state IN ('pending','running') RETURNING plan_id", [row.id])
@@ -766,7 +791,7 @@ export async function recoverMcpOperations() {
await audit(client, "mcp-recovery", "operation.interrupted", row.connection_id, { operationId: row.id, retryAllowed: false })
})
}
await pool.query("UPDATE mcp_plans SET state='expired' WHERE expires_at<now() AND state IN ('draft','approved','awaiting-approval')")
await pool.query("DELETE FROM mcp_read_leases WHERE expires_at<now()")
await pool.query("UPDATE mcp_recovery_intents SET state='unknown',finished_at=now() WHERE state='intent' AND operation_id IN (SELECT id FROM mcp_operations WHERE state NOT IN ('pending','running'))")
await withPartitionWrite(({ client }) => client.query("UPDATE mcp_plans SET state='expired' WHERE expires_at<now() AND state IN ('draft','approved','awaiting-approval')"))
await withPartitionWrite(({ client }) => client.query("DELETE FROM mcp_read_leases WHERE expires_at<now()"))
await withPartitionWrite(({ client }) => client.query("UPDATE mcp_recovery_intents SET state='unknown',finished_at=now() WHERE state='intent' AND operation_id IN (SELECT id FROM mcp_operations WHERE state NOT IN ('pending','running'))"))
}
+2 -1
View File
@@ -116,6 +116,7 @@ export async function decryptBinary(value: Buffer): Promise<Buffer> {
}
const sensitive = /password|passwd|secret|private.?key|preshared|pre.shared|authorization|auth.?key|authentication.?key|tcp.?md5.?key|api.?key|certificate.?contents|token|ciphertext/i
export const MAX_MCP_PUBLIC_STRING_LENGTH = 16000
export const MAX_MCP_PUBLIC_ARRAY_ITEMS = 500
export function redact(value: unknown, depth = 0): unknown {
if (depth > 12) return "[truncated]"
if (value instanceof Date) return value.toISOString()
@@ -123,7 +124,7 @@ export function redact(value: unknown, depth = 0): unknown {
.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [redacted]")
.replace(/\bmmcp_[a-z]+_[A-Za-z0-9_-]+/g, "[redacted]")
.replace(/((?:password|secret|private-key|preshared-key|auth-key|authentication-key|tcp-md5-key|api-key|token)\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi, "$1[redacted]")
if (Array.isArray(value)) return value.slice(0, 500).map(item => redact(item, depth + 1))
if (Array.isArray(value)) return value.slice(0, MAX_MCP_PUBLIC_ARRAY_ITEMS).map(item => redact(item, depth + 1))
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([key]) => key !== "__mmappMcpOperationClaimV1").slice(0, 200).map(([key, item]) => [key, sensitive.test(key) && !/Ref$|Configured$/i.test(key) ? "[redacted]" : redact(item, depth + 1)]))
return value
}
+3 -2
View File
@@ -57,8 +57,9 @@ export async function transaction<T>(work: (client: PoolClient) => Promise<T>):
export async function audit(client: Pick<PoolClient, "query">, actorId: string, action: string, connectionId: string | null, payload: unknown): Promise<void> {
await client.query("INSERT INTO mcp_audit(actor_id,action,connection_id,payload) VALUES($1,$2,$3,$4::jsonb)", [actorId, action, connectionId, JSON.stringify(redact(payload))])
}
export async function getMcpSettings() {
const { rows: [row] } = await pool.query("SELECT enabled,requests_retention_days AS \"requestsRetentionDays\",audit_retention_days AS \"auditRetentionDays\" FROM mcp_settings WHERE id=1")
export async function getMcpSettings(retentionOwner?: PoolClient) {
const { rows: [row] } = await (retentionOwner ?? pool).query(`SELECT enabled,requests_retention_days AS "requestsRetentionDays",
audit_retention_days AS "auditRetentionDays" FROM mcp_settings WHERE id=1${retentionOwner ? " FOR SHARE" : ""}`)
if (!row) throw new McpError("STATE_UNAVAILABLE", "Настройки MCP отсутствуют; выполните миграции", 503)
return mcpSettingsSchema.parse(row)
}
+112 -51
View File
@@ -1,3 +1,8 @@
import { isDeepStrictEqual } from "node:util"
import { withConfigMutationLock } from "../../services/config-mutation-lock.js"
import { acknowledgedCreateId } from "./native-create-identity.js"
import { readRosSnapshot } from "./resources.js"
import { proveRosDriftExpectation } from "./ros-step-verification.js"
import { randomUUID } from "node:crypto"
import { retainServerProbeStorage } from "../../services/server-probe-retention.js"
import { maintainServerProbeOperationRetirement } from "../../services/server-probe-operation-retirement.js"
@@ -6,47 +11,95 @@ import { compactCompletedServerPollStorage } from "../../services/server-poll-te
import { retireCompletedServerPollOperations } from "../../services/server-poll-operation-retirement.js"
import { pool } from "../../db/index.js"
import { sharedDatabaseGate } from "../../services/shared-database-gate.js"
import { recoverMcpOperations, verifyStep } from "./runtime.js"
import { recoverMcpOperations } from "./runtime.js"
import { retireOrdinaryMcpOperations } from "./ordinary-operation-retirement.js"
import { audit, getMcpSettings } from "./store.js"
import { accessGeneration, decryptSecret, redact } from "./security.js"
import { accessGeneration, decryptSecretExisting, redact } from "./security.js"
import { tickRoutingProfiles } from "./routing-profiles.js"
import type { ChangeEvidence } from "./action-types.js"
import { routingReservationTransaction } from "./routing-reservation-transaction.js"
import { withPartitionWrite } from "../../services/partition-write-transaction.js"
import { retainManualInverseChargesWithinOwner } from "./routing-manual-inverse-store.js"
import { recoverScopedRecoveryIntents } from "./routing-community-scoped-recovery.js"
type DriftCheckpoint = { before_sealed: string; after_sealed: string; result: unknown; endpoint_epoch: string }
const driftCheckpointSql = `SELECT k.before_sealed,k.after_sealed,s.result,device.routing_endpoint_epoch AS endpoint_epoch
FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id JOIN mcp_connections c ON c.id=o.connection_id
JOIN mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=$2
JOIN mcp_operation_checkpoints k ON k.operation_id=s.operation_id AND k.step_index=s.step_index
JOIN servers device ON device.id=s.server_id
WHERE o.id=$1 AND c.id=$7 AND p.connection_id=c.id
AND o.state='applied' AND o.verification='passed' AND o.finished_at>now()-interval '7 days'
AND c.policy->>'status'='active' AND (c.policy->>'expiresAt')::timestamptz>now() AND c.generation=$3
AND p.evidence->>'sealed'=$4 AND s.server_id=$5 AND s.intended_state=$6::jsonb AND device.enabled
AND s.state='verified' AND s.result IS NOT NULL AND k.after_sealed IS NOT NULL
AND NOT EXISTS(SELECT 1 FROM mcp_operations newer JOIN mcp_operation_steps ns ON ns.operation_id=newer.id
WHERE newer.created_at>o.created_at AND ns.server_id=s.server_id)`
export async function detectMcpDrift() {
// A newer operation supersedes this expectation, even when its outcome is unknown.
const { rows } = await pool.query(`SELECT o.id,o.connection_id,p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id
JOIN mcp_connections c ON c.id=o.connection_id WHERE o.state='applied' AND o.finished_at>now()-interval '7 days'
const generation = await accessGeneration()
const { rows } = await pool.query<{ id: string; connection_id: string; evidence: { sealed: string } }>(`SELECT o.id,o.connection_id,p.evidence
FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id JOIN mcp_connections c ON c.id=o.connection_id
WHERE o.state='applied' AND o.verification='passed' AND o.finished_at>now()-interval '7 days'
AND c.policy->>'status'='active' AND (c.policy->>'expiresAt')::timestamptz>now() AND c.generation=$1
ORDER BY o.finished_at DESC LIMIT 50`, [await accessGeneration()])
ORDER BY o.finished_at DESC LIMIT 50`, [generation])
const seen = new Set<string>()
for (const row of rows) {
const evidence = JSON.parse(await decryptSecret(row.evidence.sealed)) as ChangeEvidence
for (let index = 0; index < evidence.steps.length; index++) {
const step = evidence.steps[index]
// Only native configuration has a stable expected state suitable for drift checks.
// Service receipts (e.g. backup creation) are not assertions of ongoing state.
if (step.kind !== "ros") continue
const key = `${step.step.serverId}:${step.step.resource}:${step.step.id ?? "singleton"}`
if (seen.has(key)) continue
seen.add(key)
const newer = await pool.query(`SELECT 1 FROM mcp_operations n JOIN mcp_plans p ON p.id=n.plan_id WHERE n.created_at>(SELECT created_at FROM mcp_operations WHERE id=$1)
AND EXISTS(SELECT 1 FROM mcp_operation_steps s WHERE s.operation_id=n.id AND s.server_id=$2) LIMIT 1`, [row.id, step.step.serverId])
if (newer.rows.length) continue
const receipt = (await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1 AND step_index=$2", [row.id, index])).rows[0]?.result
let steps: ChangeEvidence['steps']
try {
const evidence = JSON.parse(await decryptSecretExisting(row.evidence.sealed)) as ChangeEvidence
if (!Array.isArray(evidence.steps)) continue
steps = evidence.steps
} catch { continue }
const uncertainResources = new Set<string>()
for (let index = steps.length - 1; index >= 0; index--) {
const step = steps[index]
if (step?.kind !== "ros" || !step.step) continue
const { serverId, resource, action, id } = step.step
const resourceKey = `${serverId}:${resource}`
if (uncertainResources.has(resourceKey)) continue
try {
if (await verifyStep(step, receipt)) continue
const inserted = await pool.query(`INSERT INTO mcp_drift(id,operation_id,server_id,expected,observed)
VALUES($1,$2,$3,$4::jsonb,$5::jsonb) ON CONFLICT DO NOTHING RETURNING id`, [randomUUID(), row.id, step.step.serverId,
JSON.stringify(redact({ stepIndex: index, step })), JSON.stringify({ matchesExpected: false, observedAt: new Date().toISOString() })])
if (inserted.rows.length) {
await audit(pool, "mcp-drift", "drift.detected", row.connection_id, { operationId: row.id, stepIndex: index, serverId: step.step.serverId })
await pool.query("UPDATE mcp_routing_profiles SET enabled=false,state=state || $2::jsonb WHERE connection_id=$1 AND enabled", [row.connection_id, JSON.stringify({ pausedReason: "Обнаружен дрейф конфигурации" })])
const result = action === "create"
? (await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1 AND step_index=$2 AND server_id=$3", [row.id, index, serverId])).rows[0]?.result
: undefined
const nativeId = action === "create" ? acknowledgedCreateId(result) : id
if (action === "create" && !nativeId) {
// A later create without a durable native identity can shadow an older target.
uncertainResources.add(resourceKey)
continue
}
} catch { /* Device unavailability is unknown, never misreported as drift. */ }
const targetKey = JSON.stringify([serverId, resource, nativeId ?? null])
if (seen.has(targetKey)) continue
seen.add(targetKey)
await withConfigMutationLock([serverId], async () => {
const expected = JSON.stringify(redact(step))
const args = [row.id, index, generation, row.evidence.sealed, serverId, expected, row.connection_id]
const { rows: [checkpoint] } = await pool.query<DriftCheckpoint>(driftCheckpointSql, args)
if (!checkpoint || await accessGeneration() !== generation) return
const before = JSON.parse(await decryptSecretExisting(checkpoint.before_sealed))
const after = JSON.parse(await decryptSecretExisting(checkpoint.after_sealed))
const expectation = proveRosDriftExpectation(step.step, checkpoint.result, before, after)
if (!expectation || expectation.nativeId !== nativeId) return
const current = await readRosSnapshot(serverId, resource, undefined, expectation.sourceProof)
if (expectation.matches(current) !== false) return
const observedAt = new Date().toISOString()
await withPartitionWrite(async ({ client }) => {
const { rows: [locked] } = await client.query<DriftCheckpoint>(
`${driftCheckpointSql} FOR SHARE OF o,p,c,s,k,device`, args)
if (!isDeepStrictEqual(locked, checkpoint) || await accessGeneration() !== generation) return
const inserted = await client.query(`INSERT INTO mcp_drift(id,operation_id,server_id,expected,observed)
VALUES($1,$2,$3,$4::jsonb,$5::jsonb) ON CONFLICT DO NOTHING RETURNING id`, [randomUUID(), row.id, serverId,
JSON.stringify(redact({ stepIndex: index, step })), JSON.stringify({ matchesExpected: false, observedAt })])
if (inserted.rows.length) {
await audit(client, "mcp-drift", "drift.detected", row.connection_id, { operationId: row.id, stepIndex: index, serverId })
await client.query("UPDATE mcp_routing_profiles SET enabled=false,state=state || $2::jsonb WHERE connection_id=$1 AND enabled", [row.connection_id, JSON.stringify({ pausedReason: "Обнаружен дрейф конфигурации" })])
}
})
})
} catch { /* Unavailable proof or persistence leaves the next candidate eligible. */ }
}
}
}
@@ -54,31 +107,39 @@ export async function detectMcpDrift() {
const mcpRetentionBatchSize = 1000
async function retainMcpData() {
const settings = await getMcpSettings()
await pool.query(`WITH expired AS (
SELECT id FROM mcp_requests WHERE created_at<now()-make_interval(days=>$1) AND status NOT IN ('running')
ORDER BY created_at,id LIMIT $2 FOR UPDATE
) DELETE FROM mcp_requests USING expired WHERE mcp_requests.id=expired.id`, [settings.requestsRetentionDays, mcpRetentionBatchSize])
await pool.query(`WITH expired AS (
SELECT id FROM mcp_audit WHERE created_at<now()-make_interval(days=>$1)
ORDER BY created_at,id LIMIT $2 FOR UPDATE
) DELETE FROM mcp_audit USING expired WHERE mcp_audit.id=expired.id`, [settings.auditRetentionDays, mcpRetentionBatchSize])
await pool.query(`WITH expired AS (
await withPartitionWrite(async ({ client }) => {
const { requestsRetentionDays } = await getMcpSettings(client)
await client.query(`WITH expired AS (
SELECT id FROM mcp_requests WHERE created_at<now()-make_interval(days=>$1) AND status NOT IN ('running')
ORDER BY created_at,id LIMIT $2 FOR UPDATE SKIP LOCKED
) DELETE FROM mcp_requests USING expired WHERE mcp_requests.id=expired.id`, [requestsRetentionDays, mcpRetentionBatchSize])
})
await withPartitionWrite(async ({ client }) => {
const { auditRetentionDays } = await getMcpSettings(client)
await client.query(`WITH expired AS (
SELECT id FROM mcp_audit WHERE created_at<now()-make_interval(days=>$1)
ORDER BY created_at,id LIMIT $2 FOR UPDATE SKIP LOCKED
) DELETE FROM mcp_audit USING expired WHERE mcp_audit.id=expired.id`, [auditRetentionDays, mcpRetentionBatchSize])
})
await withPartitionWrite(({ client }) => client.query(`WITH expired AS (
SELECT id FROM mcp_artifacts WHERE expires_at<now()
ORDER BY expires_at,id LIMIT $1 FOR UPDATE
) DELETE FROM mcp_artifacts USING expired WHERE mcp_artifacts.id=expired.id`, [mcpRetentionBatchSize])
await pool.query("DELETE FROM mcp_oauth_requests WHERE expires_at<now()-interval '1 day'")
await pool.query("DELETE FROM mcp_budget_buckets WHERE bucket<now()-interval '2 days'")
await routingReservationTransaction(db => retainManualInverseChargesWithinOwner(db, settings.requestsRetentionDays))
await pool.query(`WITH candidates AS MATERIALIZED (SELECT source_operation_id FROM mcp_routing_forward_charges
WHERE created_at<now()-make_interval(days=>$1) ORDER BY created_at,source_operation_id LIMIT 1000),
expired AS (SELECT c.source_operation_id FROM candidates c WHERE NOT EXISTS(SELECT 1 FROM mcp_operations o
WHERE o.id=c.source_operation_id AND o.state IN('pending','running')))
DELETE FROM mcp_routing_forward_charges c USING expired e WHERE c.source_operation_id=e.source_operation_id`, [settings.requestsRetentionDays])
await pool.query("DELETE FROM mcp_routing_observations WHERE created_at<now()-interval '7 days'")
await pool.query("DELETE FROM mcp_path_checks WHERE created_at<now()-interval '7 days'")
await pool.query("DELETE FROM mcp_unauthorized_buckets WHERE bucket<now()-interval '7 days'")
await pool.query("DELETE FROM mcp_portal_events WHERE created_at<now()-interval '7 days'")
) DELETE FROM mcp_artifacts USING expired WHERE mcp_artifacts.id=expired.id`, [mcpRetentionBatchSize]))
await withPartitionWrite(({ client }) => client.query("DELETE FROM mcp_oauth_requests WHERE expires_at<now()-interval '1 day'"))
await withPartitionWrite(({ client }) => client.query("DELETE FROM mcp_budget_buckets WHERE bucket<now()-interval '2 days'"))
await withPartitionWrite(({ client }) => retainManualInverseChargesWithinOwner(client))
await withPartitionWrite(async ({ client }) => {
const { requestsRetentionDays } = await getMcpSettings(client)
await client.query(`WITH candidates AS MATERIALIZED (SELECT source_operation_id FROM mcp_routing_forward_charges
WHERE created_at<now()-make_interval(days=>$1) ORDER BY created_at,source_operation_id LIMIT 1000),
expired AS (SELECT c.source_operation_id FROM candidates c WHERE NOT EXISTS(SELECT 1 FROM mcp_operations o
WHERE o.id=c.source_operation_id AND o.state IN('pending','running')))
DELETE FROM mcp_routing_forward_charges c USING expired e WHERE c.source_operation_id=e.source_operation_id`, [requestsRetentionDays])
})
await withPartitionWrite(({ client }) => client.query("DELETE FROM mcp_routing_observations WHERE created_at<now()-interval '7 days'"))
await withPartitionWrite(({ client }) => client.query("DELETE FROM mcp_path_checks WHERE created_at<now()-interval '7 days'"))
await withPartitionWrite(({ client }) => client.query("DELETE FROM mcp_unauthorized_buckets WHERE bucket<now()-interval '7 days'"))
await withPartitionWrite(({ client }) => client.query("DELETE FROM mcp_portal_events WHERE created_at<now()-interval '7 days'"))
}
export async function tickMcpWorkers() {
@@ -11,6 +11,7 @@ import {
import { toServerRead } from "../mapper/servers-mapper.js"
import type { ServerCreate, ServerRead, ServerUpdate, SnapshotRead } from "../../../types/server.js"
import { executeGuardedApplicationWrite } from "../../../services/config-mutation-policy.js"
import { withConfigMutationLock } from "../../../services/config-mutation-lock.js"
export async function listServersRead(): Promise<ServerRead[]> {
const rows = await listServerRows()
@@ -36,7 +37,9 @@ async function createServerImpl(input: ServerCreate): Promise<ServerRead> {
return toServerRead(inserted, undefined)
}
export function updateServer(id: number, input: ServerUpdate): Promise<ServerRead> { return executeGuardedApplicationWrite({ kind: "server", id: String(id) }, [id], () => updateServerImpl(id, input)) }
export function updateServer(id: number, input: ServerUpdate): Promise<ServerRead> {
return executeGuardedApplicationWrite({ kind: "server", id: String(id) }, [id], () => withConfigMutationLock([id], () => updateServerImpl(id, input)))
}
async function updateServerImpl(id: number, input: ServerUpdate): Promise<ServerRead> {
const { wanUplinks, ...rest } = input
const setPayload: Record<string, unknown> = { updatedAt: new Date().toISOString() }
@@ -50,7 +53,9 @@ async function updateServerImpl(id: number, input: ServerUpdate): Promise<Server
return toServerRead(updated, await getLatestSnapshot(updated.id))
}
export function deleteServer(id: number): Promise<void> { return executeGuardedApplicationWrite({ kind: "server", id: String(id) }, [id], () => deleteServerImpl(id)) }
export function deleteServer(id: number): Promise<void> {
return executeGuardedApplicationWrite({ kind: "server", id: String(id) }, [id], () => withConfigMutationLock([id], () => deleteServerImpl(id)))
}
async function deleteServerImpl(id: number): Promise<void> {
await deleteServerRowById(id)
}
@@ -1,4 +1,5 @@
import { randomUUID } from "node:crypto"
import { isDeepStrictEqual } from "node:util"
import { executeGuardedApplicationWrite } from "../../../services/config-mutation-policy.js"
import { and, desc, eq } from "drizzle-orm"
import type {
@@ -137,7 +138,15 @@ async function userMutationDependencies(id: string, added: number[] = []) {
const user = await getUserById(id)
return [...new Set([...added, ...(user?.servers ?? []).map(server => Number(server.serverId)), ...(user?.bindings ?? []).map(binding => binding.serverId)])]
}
export function createUser(input: AppUserCreate): Promise<AppUserRead> { return executeGuardedApplicationWrite({ kind: "user", id: "new" }, (input.servers ?? []).map(server => Number(server.serverId)), () => createUserImpl(input)) }
type UserWriteOptions = { canManagePermissions?: boolean }
const permissionChangeDenied = () => new UsersServiceError("Для изменения роли и прав требуется mm:settings:admin", 403)
export function createUser(input: AppUserCreate, { canManagePermissions = true }: UserWriteOptions = {}): Promise<AppUserRead> {
if (!canManagePermissions && ((input.role ?? "viewer") !== "viewer" || input.sections?.length || input.servers?.length)) {
return Promise.reject(permissionChangeDenied())
}
return executeGuardedApplicationWrite({ kind: "user", id: "new" }, (input.servers ?? []).map(server => Number(server.serverId)), () => createUserImpl(input))
}
async function createUserImpl(input: AppUserCreate): Promise<AppUserRead> {
const login = input.login.trim()
if (await getUserRowByLogin(login)) {
@@ -161,10 +170,17 @@ async function createUserImpl(input: AppUserCreate): Promise<AppUserRead> {
return await toUserDto(row, [])
}
export function updateUser(id: string, input: AppUserUpdate): Promise<AppUserRead> { return executeGuardedApplicationWrite({ kind: "user", id }, () => userMutationDependencies(id, (input.servers ?? []).map(server => Number(server.serverId))), () => updateUserImpl(id, input)) }
async function updateUserImpl(id: string, input: AppUserUpdate): Promise<AppUserRead> {
export function updateUser(id: string, input: AppUserUpdate, { canManagePermissions = true }: UserWriteOptions = {}): Promise<AppUserRead> {
return executeGuardedApplicationWrite({ kind: "user", id }, () => userMutationDependencies(id, (input.servers ?? []).map(server => Number(server.serverId))), () => updateUserImpl(id, input, canManagePermissions))
}
async function updateUserImpl(id: string, input: AppUserUpdate, canManagePermissions: boolean): Promise<AppUserRead> {
const existing = await getUserRowById(id)
if (!existing) throw new UsersServiceError("Пользователь не найден", 404)
if (!canManagePermissions && (
(input.role != null && input.role !== existing.role)
|| (input.sections != null && !isDeepStrictEqual(input.sections, asPermArray<SectionPerm>(existing.sectionsJson, [])))
|| (input.servers != null && !isDeepStrictEqual(input.servers, asPermArray<ServerPerm>(existing.serversJson, [])))
)) throw permissionChangeDenied()
if (input.login != null) {
const other = await getUserRowByLogin(input.login.trim())
if (other && other.id !== id) throw new UsersServiceError("Логин уже занят", 409)
@@ -173,12 +189,14 @@ async function updateUserImpl(id: string, input: AppUserUpdate): Promise<AppUser
if (input.name != null) patch.name = input.name.trim()
if (input.login != null) patch.login = input.login.trim()
if (input.email != null) patch.email = input.email.trim()
if (input.role != null) patch.role = input.role
if (input.active != null) patch.active = input.active
if (input.avatar != null) patch.avatar = input.avatar.trim() || existing.avatar
if (input.lastSeen !== undefined) patch.lastSeen = input.lastSeen
if (input.sections != null) patch.sectionsJson = input.sections
if (input.servers != null) patch.serversJson = input.servers
if (canManagePermissions) {
if (input.role != null) patch.role = input.role
if (input.sections != null) patch.sectionsJson = input.sections
if (input.servers != null) patch.serversJson = input.servers
}
const updated = await updateUserRowById(id, patch)
return await toUserDto(updated, await listBindingRowsByUser(id))
}
@@ -369,9 +387,12 @@ export async function getInterfaceCatalogState(serverId: number): Promise<Interf
return { serverId, source, sampledAt, inventoryComplete: inventory.known, interfaces: entries.sort((a, b) => a.name.localeCompare(b.name)), failures }
}
export async function listInterfaceCatalog(serverId: number): Promise<CatalogInterface[]> {
const state = await getInterfaceCatalogState(serverId)
export function projectLegacyInterfaceCatalog(state: InterfaceCatalogState): CatalogInterface[] {
return state.interfaces.filter(iface => !(iface.type === "ipsec" && iface.name === "ipsec-vpn" && !iface.peers?.length)).map(iface => ({ ...iface, running: iface.running ?? false, disabled: iface.disabled ?? false }))
}
export async function listInterfaceCatalog(serverId: number): Promise<CatalogInterface[]> {
return projectLegacyInterfaceCatalog(await getInterfaceCatalogState(serverId))
}
export { parseRawInterfaces, mapRosInterfaceType }
+4
View File
@@ -38,6 +38,10 @@ export async function makeMcpServer(principal: McpPrincipal, signal?: AbortSigna
* these same authorized action objects; runtime still revalidates live policy. */
function makeMcpServerWithBindings(principal: McpPrincipal, bindings: ReturnType<typeof listActions>, signal?: AbortSignal, flowOwner?: FlowReadResponseOwner, wireEra?: "legacy" | "modern", genericOwner?: McpInFlightOwner, sdkBudget?: SdkTerminalBudget): McpServer {
const server = sdkBudget ? new SdkBudgetedMcpServer(sdkBudget) : new McpServer(MCP_SERVER_INFO)
if (bindings.length === 0) {
server.server.registerCapabilities({ tools: {} })
server.server.setRequestHandler("tools/list", () => ({ tools: [] }))
}
for (const action of bindings) {
const descriptor = action.operation
server.registerTool(descriptor.toolName, {
+2 -1
View File
@@ -416,7 +416,8 @@ export function createMcpRoutes(checkClaims = assertPortalClaimsCurrent, options
await checkClaims(actorOf(request.authUser))
const fresh=await getConnection(connection.id)
if(fresh.generation!==await accessGeneration() || fresh.policy.status!=="active" || fresh.policy.version!==metadata.access_scope.policyVersion || !hasPermission(fresh.policy.permissions,"mm:servers:read") || metadata.access_scope.serverIds.some(id=>!fresh.policy.scope.serverIds.includes(id)))throw new McpError("PERMISSION_DENIED","Доступ к файлу отозван")
return reply.type(artifact.media_type).header("Content-Disposition", `attachment; filename="${artifact.filename.replace(/[^a-zA-Z0-9._-]/g, "_")}"`).send(content)
return reply.type(artifact.media_type).header("Cache-Control", "private, no-store")
.header("Content-Disposition", `attachment; filename="${artifact.filename.replace(/[^a-zA-Z0-9._-]/g, "_")}"`).send(content)
})
}
+7 -2
View File
@@ -1,4 +1,5 @@
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { hasPermission } from "../lib/permissions.js"
import {
interfaceCatalogStateSchema,
type InterfaceCatalogState,
@@ -74,7 +75,9 @@ export function createUsersRoutes(deps: { getCatalogState?: (serverId: number) =
schema: { body: appUserCreateSchema },
}, async (req, reply) => {
try {
const user = await createUser(req.body as AppUserCreate)
const user = await createUser(req.body as AppUserCreate, {
canManagePermissions: !req.authUser || hasPermission(req.authUser.permissions, "mm:settings:admin"),
})
return reply.status(201).send({ user })
} catch (err) {
return sendServiceError(reply, err)
@@ -86,7 +89,9 @@ export function createUsersRoutes(deps: { getCatalogState?: (serverId: number) =
}, async (req, reply) => {
const { id } = req.params as { id: string }
try {
const user = await updateUser(id, req.body as AppUserUpdate)
const user = await updateUser(id, req.body as AppUserUpdate, {
canManagePermissions: !req.authUser || hasPermission(req.authUser.permissions, "mm:settings:admin"),
})
return reply.send({ user })
} catch (err) {
return sendServiceError(reply, err)
+8 -12
View File
@@ -1,5 +1,4 @@
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"
import { fromJsonSchema } from "@modelcontextprotocol/server"
import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/stdio"
import { createStdioSdkTerminalGuard, StdioSdkBudgetedMcpServer } from "../modules/mcp/stdio-terminal-budget.js"
@@ -16,17 +15,14 @@ const wire = new StdioServerTransport()
guard.decorateWire(wire)
const handle = serveStdio(async () => {
const server = new StdioSdkBudgetedMcpServer(guard)
let cursor: string | undefined
do {
const page = await remote.listTools(cursor ? { cursor } : undefined)
for (const tool of page.tools) server.registerTool(tool.name, {
title: tool.title, description: tool.description, inputSchema: fromJsonSchema(tool.inputSchema as Parameters<typeof fromJsonSchema>[0]), annotations: tool.annotations,
}, async (input, context) => {
guard.protectCallback(context.mcpReq.id)
return remote.callTool({ name: tool.name, arguments: input as Record<string, unknown> })
})
cursor = page.nextCursor
} while (cursor)
server.server.registerCapabilities({ tools: {} })
// The remote server supplies current policy and validates tool arguments.
server.server.setRequestHandler("tools/list", ({ params }, context) =>
remote.listTools({ cursor: params?.cursor }, { signal: context.mcpReq.signal }))
server.server.setRequestHandler("tools/call", ({ params }, context) => {
guard.protectCallback(context.mcpReq.id)
return remote.callTool({ name: params.name, arguments: params.arguments })
})
return server
}, { transport: wire, onerror: () => process.stderr.write("Ошибка связи с MCP; проверьте доступ и журнал обращений\n") })
process.once("SIGTERM", () => { void Promise.all([handle.close(), remote.close()]) })
@@ -0,0 +1,223 @@
{
"kind": "V33 selected loop bridge DELETE observation; not raw17",
"serverId": 2,
"inputs": {
"beforeSha256": "09a4a1a16dee2554bf809d88328b276f0289e81e29fa94b18f7a61ffa709d0e4",
"afterSha256": "b5116e76452573cb0b6e532afb81d16ffc8360710da5e59ac40da67341e1adae"
},
"limitations": [
"Safe projected selected rows only; omitted native fields unavailable",
"Tests reconstruct main VRF/table/singleton settings and restrict target membership to this bridge",
"Full admission/classifier on reconstructed test graph is synthetic, not full actual raw17 replay"
],
"before": {
"bridge": [
{
".id": "*2C",
"actual-mtu": "1500",
"comment": "mcp-route-3efd451365d2",
"disabled": "false",
"dynamic": "false",
"mac-address": "82:97:C3:74:4C:A5",
"mtu": "auto",
"name": "mcp-route-3efd451365d2-loop",
"protocol-mode": "none",
"running": "true"
}
],
"interface_inventory": [
{
".id": "*2C",
"actual-mtu": "1500",
"caps": " ARP DHCP USERARP VLAN IS_BRIDGE MESH",
"comment": "mcp-route-3efd451365d2",
"disabled": "false",
"dynamic": "false",
"flags": " UP BROADCAST RUNNING MULTICAST LOWER_UP (0x11043)",
"fp-rps-drop": "0",
"id": "44",
"ifindex": "46",
"ifname": "br0",
"last-link-up-time": "2026-11-05 19:52:16",
"link-downs": "0",
"mac-address": "82:97:C3:74:4C:A5",
"mtu": "auto",
"name": "mcp-route-3efd451365d2-loop",
"running": "true",
"status": "--LERUI-O-----------V---",
"type": "bridge",
"vrf": "mcp-route-3efd451365d2-vrf"
},
{
".id": "*2D",
"actual-mtu": "65536",
"caps": "",
"disabled": "false",
"flags": " UP RUNNING NOARP MASTER LOWER_UP (0x104c1)",
"fp-rps-drop": "0",
"id": "45",
"ifindex": "47",
"ifname": "vrf1",
"last-link-up-time": "2026-11-05 19:55:16",
"link-downs": "0",
"mac-address": "7A:F0:19:DD:88:39",
"mtu": "65536",
"name": "mcp-route-3efd451365d2-vrf",
"running": "true",
"status": "--LERU--O---------------",
"type": "vrf",
"vrf": "main"
}
],
"vrf": [
{
".id": "*1",
"comment": "mcp-route-3efd451365d2",
"disabled": "false",
"interfaces": "mcp-route-3efd451365d2-gre-a,mcp-route-3efd451365d2-gre-b,mcp-route-3efd451365d2-loop",
"name": "mcp-route-3efd451365d2-vrf"
}
],
"ipv4_address": [
{
".id": "*31",
"actual-interface": "mcp-route-3efd451365d2-loop",
"address": "198.18.245.17/32",
"comment": "mcp-route-3efd451365d2",
"disabled": "false",
"dynamic": "false",
"interface": "mcp-route-3efd451365d2-loop",
"invalid": "false",
"vrf": "mcp-route-3efd451365d2-vrf"
}
],
"ipv6_address": [
{
".id": "*3D",
"actual-interface": "mcp-route-3efd451365d2-loop",
"address": "fe80::8097:c3ff:fe74:4ca5/64",
"auto-link-local": "true",
"disabled": "false",
"dynamic": "true",
"interface": "mcp-route-3efd451365d2-loop",
"invalid": "false",
"link-local": "true",
"vrf": "main"
}
],
"ipv4_route": [
{
".id": "*20145080",
"connect": "true",
"distance": "0",
"dst-address": "198.18.245.17/32",
"dynamic": "true",
"gateway": "mcp-route-3efd451365d2-loop@mcp-route-3efd451365d2-vrf",
"immediate-gw": "mcp-route-3efd451365d2-loop",
"local-address": "198.18.245.17%mcp-route-3efd451365d2-loop@mcp-route-3efd451365d2-vrf",
"routing-table": "mcp-route-3efd451365d2-vrf",
"scope": "10",
"target-scope": "5"
}
],
"ipv6_route": [
{
".id": "*20145070",
"connect": "true",
"distance": "0",
"dst-address": "fe80::/64",
"dynamic": "true",
"gateway": "mcp-route-3efd451365d2-loop@mcp-route-3efd451365d2-vrf",
"immediate-gw": "mcp-route-3efd451365d2-loop",
"routing-table": "mcp-route-3efd451365d2-vrf",
"scope": "10",
"target-scope": "5"
}
],
"routing_table": [
{
".id": "*1",
"dynamic": "true",
"fib": "",
"invalid": "false",
"name": "mcp-route-3efd451365d2-vrf"
}
],
"bridge_port": [],
"bridge_vlan": []
},
"after": {
"bridge": [
{
".id": "*2C",
"actual-mtu": "1500",
"comment": "mcp-route-3efd451365d2",
"disabled": "false",
"dynamic": "false",
"mac-address": "82:97:C3:74:4C:A5",
"mtu": "auto",
"name": "mcp-route-3efd451365d2-loop",
"protocol-mode": "none",
"running": "true"
}
],
"interface_inventory": [
{
".id": "*2C",
"actual-mtu": "1500",
"caps": " ARP DHCP USERARP VLAN IS_BRIDGE MESH",
"comment": "mcp-route-3efd451365d2",
"disabled": "false",
"dynamic": "false",
"flags": " UP BROADCAST RUNNING MULTICAST LOWER_UP (0x11043)",
"fp-rps-drop": "0",
"id": "44",
"ifindex": "46",
"ifname": "br0",
"last-link-up-time": "2026-11-05 19:52:16",
"link-downs": "0",
"mac-address": "82:97:C3:74:4C:A5",
"mtu": "auto",
"name": "mcp-route-3efd451365d2-loop",
"running": "true",
"status": "--LERUI-O---------------",
"type": "bridge",
"vrf": "main"
}
],
"vrf": [],
"ipv4_address": [
{
".id": "*31",
"actual-interface": "mcp-route-3efd451365d2-loop",
"address": "198.18.245.17/32",
"comment": "mcp-route-3efd451365d2",
"disabled": "false",
"dynamic": "false",
"interface": "mcp-route-3efd451365d2-loop",
"invalid": "false",
"vrf": "main"
}
],
"ipv6_address": [],
"ipv4_route": [
{
".id": "*201450D0",
"connect": "true",
"distance": "0",
"dst-address": "198.18.245.17/32",
"dynamic": "true",
"gateway": "mcp-route-3efd451365d2-loop",
"immediate-gw": "mcp-route-3efd451365d2-loop",
"local-address": "198.18.245.17%mcp-route-3efd451365d2-loop",
"routing-table": "main",
"scope": "10",
"target-scope": "5"
}
],
"ipv6_route": [],
"routing_table": [],
"bridge_port": [],
"bridge_vlan": []
}
}
@@ -0,0 +1,349 @@
{
"kind": "actual-selected-V32-guarded-inverse-pair-with-reconstructed-root-config",
"inputs": {
"preDetail": "12195049ae548776d47449a86f849ebf19776c9d3d562bb425f138c24a0a0922",
"postDetail": "d96200bd6590d69e484d4535f28b113bdd49944de161eadfe6ab4fc506affbe8",
"cleanupV2": "094f4b59569386149288f4696fbcd7105a0d049a035bc825f7753c1ffc537c88",
"receipt": "ebbe523caa1217ea4fab393b7d9238e23f164df8544fe6a29f3752400d81dd63"
},
"before": {
"catalogs": {
"vrf": [
{
".id": "*1",
"name": "mcp-route-46e4f3b60294-vrf",
"interfaces": "mcp-route-46e4f3b60294-gre-a,mcp-route-46e4f3b60294-gre-b",
"disabled": "false"
},
{
".id": "*0",
"name": "main",
"interfaces": "all",
"disabled": "false"
}
],
"interface_inventory": [
{
".id": "*30",
"name": "mcp-route-46e4f3b60294-gre-a",
"type": "gre-tunnel",
"disabled": "false",
"running": "true",
"mtu": "1476",
"actual-mtu": "1476",
"vrf": "mcp-route-46e4f3b60294-vrf",
"caps": "",
"flags": " UP POINTOPOINT RUNNING NOARP LOWER_UP (0x100d1)",
"fp-rps-drop": "0",
"id": "48",
"ifindex": "49",
"ifname": "greip4-48",
"last-link-up-time": "2026-11-05 19:42:15",
"last-link-down-time": "2026-11-05 19:42:15",
"link-downs": "1",
"status": "--L-RUI-O-----------V---"
},
{
".id": "*31",
"name": "mcp-route-46e4f3b60294-gre-b",
"type": "gre-tunnel",
"disabled": "false",
"running": "true",
"mtu": "1476",
"actual-mtu": "1476",
"vrf": "mcp-route-46e4f3b60294-vrf",
"caps": "",
"flags": " UP POINTOPOINT RUNNING NOARP LOWER_UP (0x100d1)",
"fp-rps-drop": "0",
"id": "49",
"ifindex": "50",
"ifname": "greip4-49",
"last-link-up-time": "2026-11-05 19:42:15",
"last-link-down-time": "2026-11-05 19:42:15",
"link-downs": "1",
"status": "--L-RUI-O-----------V---"
},
{
".id": "*32",
"name": "mcp-route-46e4f3b60294-vrf",
"type": "vrf",
"mac-address": "16:A1:FC:19:3A:0B",
"disabled": "false",
"running": "true",
"mtu": "65536",
"actual-mtu": "65536",
"vrf": "main",
"caps": "",
"flags": " UP RUNNING NOARP MASTER LOWER_UP (0x104c1)",
"fp-rps-drop": "0",
"id": "50",
"ifindex": "51",
"ifname": "vrf1",
"last-link-up-time": "2026-11-05 19:42:15",
"link-downs": "0",
"status": "--LERU--O---------------"
},
{
".id": "*2F",
"name": "mcp-route-46e4f3b60294-wg",
"type": "wg",
"disabled": "false",
"running": "true",
"mtu": "1420",
"actual-mtu": "1420",
"vrf": "main",
"caps": "",
"flags": " UP POINTOPOINT RUNNING NOARP LOWER_UP (0x100d1)",
"fp-rps-drop": "0",
"id": "47",
"ifindex": "48",
"ifname": "wg47",
"last-link-up-time": "2026-11-05 19:32:05",
"link-downs": "0",
"status": "--L-RUI-O---------------"
}
],
"gre": [
{
".id": "*30",
"name": "mcp-route-46e4f3b60294-gre-a",
"disabled": "false",
"running": "true",
"mtu": "1476",
"actual-mtu": "1476",
"local-address": "198.18.245.9",
"remote-address": "198.18.245.10",
"allow-fast-path": "false",
"clamp-tcp-mss": "true",
"keepalive": "10s,10",
"comment": "mcp-route-46e4f3b60294"
},
{
".id": "*31",
"name": "mcp-route-46e4f3b60294-gre-b",
"disabled": "false",
"running": "true",
"mtu": "1476",
"actual-mtu": "1476",
"local-address": "198.18.245.13",
"remote-address": "198.18.245.14",
"allow-fast-path": "false",
"clamp-tcp-mss": "true",
"keepalive": "10s,10",
"comment": "mcp-route-46e4f3b60294"
}
],
"ipv4_address": [
{
".id": "*3A",
"address": "198.18.245.9/30",
"interface": "mcp-route-46e4f3b60294-wg",
"actual-interface": "mcp-route-46e4f3b60294-wg",
"dynamic": "false",
"disabled": "false",
"invalid": "false",
"vrf": "main",
"network": "198.18.245.8"
},
{
".id": "*3B",
"address": "198.18.245.13/30",
"interface": "mcp-route-46e4f3b60294-wg",
"actual-interface": "mcp-route-46e4f3b60294-wg",
"dynamic": "false",
"disabled": "false",
"invalid": "false",
"vrf": "main",
"network": "198.18.245.12"
},
{
".id": "*3C",
"address": "198.18.245.1/30",
"interface": "mcp-route-46e4f3b60294-gre-a",
"actual-interface": "mcp-route-46e4f3b60294-gre-a",
"dynamic": "false",
"disabled": "false",
"invalid": "false",
"vrf": "mcp-route-46e4f3b60294-vrf",
"network": "198.18.245.0"
},
{
".id": "*3D",
"address": "198.18.245.5/30",
"interface": "mcp-route-46e4f3b60294-gre-b",
"actual-interface": "mcp-route-46e4f3b60294-gre-b",
"dynamic": "false",
"disabled": "false",
"invalid": "false",
"vrf": "mcp-route-46e4f3b60294-vrf",
"network": "198.18.245.4"
}
],
"wireguard": [
{
".id": "*2F",
"name": "mcp-route-46e4f3b60294-wg"
}
]
}
},
"after": {
"catalogs": {
"vrf": [
{
".id": "*0",
"name": "main",
"interfaces": "all",
"disabled": "false"
}
],
"interface_inventory": [
{
".id": "*30",
"name": "mcp-route-46e4f3b60294-gre-a",
"type": "gre-tunnel",
"disabled": "false",
"running": "true",
"mtu": "1476",
"actual-mtu": "1476",
"vrf": "main",
"caps": "",
"flags": " UP POINTOPOINT RUNNING NOARP LOWER_UP (0x100d1)",
"fp-rps-drop": "0",
"id": "48",
"ifindex": "49",
"ifname": "greip4-48",
"last-link-up-time": "2026-11-05 20:30:42",
"last-link-down-time": "2026-11-05 20:30:41",
"link-downs": "2",
"status": "--L-RUI-O---------------"
},
{
".id": "*31",
"name": "mcp-route-46e4f3b60294-gre-b",
"type": "gre-tunnel",
"disabled": "false",
"running": "true",
"mtu": "1476",
"actual-mtu": "1476",
"vrf": "main",
"caps": "",
"flags": " UP POINTOPOINT RUNNING NOARP LOWER_UP (0x100d1)",
"fp-rps-drop": "0",
"id": "49",
"ifindex": "50",
"ifname": "greip4-49",
"last-link-up-time": "2026-11-05 20:30:42",
"last-link-down-time": "2026-11-05 20:30:41",
"link-downs": "2",
"status": "--L-RUI-O---------------"
},
{
".id": "*2F",
"name": "mcp-route-46e4f3b60294-wg",
"type": "wg",
"disabled": "false",
"running": "true",
"mtu": "1420",
"actual-mtu": "1420",
"vrf": "main",
"caps": "",
"flags": " UP POINTOPOINT RUNNING NOARP LOWER_UP (0x100d1)",
"fp-rps-drop": "0",
"id": "47",
"ifindex": "48",
"ifname": "wg47",
"last-link-up-time": "2026-11-05 19:32:05",
"link-downs": "0",
"status": "--L-RUI-O---------------"
}
],
"gre": [
{
".id": "*30",
"name": "mcp-route-46e4f3b60294-gre-a",
"disabled": "false",
"running": "true",
"mtu": "1476",
"actual-mtu": "1476",
"local-address": "198.18.245.9",
"remote-address": "198.18.245.10",
"allow-fast-path": "false",
"clamp-tcp-mss": "true",
"keepalive": "10s,10",
"comment": "mcp-route-46e4f3b60294"
},
{
".id": "*31",
"name": "mcp-route-46e4f3b60294-gre-b",
"disabled": "false",
"running": "true",
"mtu": "1476",
"actual-mtu": "1476",
"local-address": "198.18.245.13",
"remote-address": "198.18.245.14",
"allow-fast-path": "false",
"clamp-tcp-mss": "true",
"keepalive": "10s,10",
"comment": "mcp-route-46e4f3b60294"
}
],
"ipv4_address": [
{
".id": "*3A",
"address": "198.18.245.9/30",
"interface": "mcp-route-46e4f3b60294-wg",
"actual-interface": "mcp-route-46e4f3b60294-wg",
"dynamic": "false",
"disabled": "false",
"invalid": "false",
"vrf": "main",
"network": "198.18.245.8"
},
{
".id": "*3B",
"address": "198.18.245.13/30",
"interface": "mcp-route-46e4f3b60294-wg",
"actual-interface": "mcp-route-46e4f3b60294-wg",
"dynamic": "false",
"disabled": "false",
"invalid": "false",
"vrf": "main",
"network": "198.18.245.12"
},
{
".id": "*3C",
"address": "198.18.245.1/30",
"interface": "mcp-route-46e4f3b60294-gre-a",
"actual-interface": "mcp-route-46e4f3b60294-gre-a",
"dynamic": "false",
"disabled": "false",
"invalid": "false",
"vrf": "main",
"network": "198.18.245.0"
},
{
".id": "*3D",
"address": "198.18.245.5/30",
"interface": "mcp-route-46e4f3b60294-gre-b",
"actual-interface": "mcp-route-46e4f3b60294-gre-b",
"dynamic": "false",
"disabled": "false",
"invalid": "false",
"vrf": "main",
"network": "198.18.245.4"
}
],
"wireguard": [
{
".id": "*2F",
"name": "mcp-route-46e4f3b60294-wg"
}
]
}
},
"rootWireguardConfigurationReconstructed": true,
"fullRaw17": false,
"forwardCreateCycleProven": false,
"exclusiveCausalAttributionProven": false
}
@@ -0,0 +1,811 @@
{
"kind": "genuine-pre-V3-output-with-synthetic-GRE-WG-peer",
"sourceProjectionSha256": "01366cbf771dec96af7441696763e19b0276cfb312df4715bc3bb0941cdd2b80",
"before": {
"projection": "raw-native-v1",
"serverId": 7,
"sourceProof": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"catalogs": {
"bridge": [],
"bridge_port": [],
"bridge_vlan": [],
"vrf": [
{
".id": "*0",
"name": "main",
"interfaces": "all",
"disabled": "false",
"builtin": "true"
}
],
"ipv4_address": [
{
".id": "*A",
"actual-interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"address": "198.18.137.1/32",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"disabled": "false",
"dynamic": "false",
"interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"invalid": "false",
"network": "198.18.137.1",
"slave": "false",
"vrf": "main"
},
{
".id": "*B",
"actual-interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"address": "198.18.137.2/32",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"disabled": "false",
"dynamic": "false",
"interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"invalid": "false",
"network": "198.18.137.2",
"slave": "false",
"vrf": "main"
},
{
".id": "*ff20",
"interface": "gre-source-wg",
"address": "198.18.245.9/30",
"dynamic": "false",
"disabled": "false",
"vrf": "main"
},
{
".id": "*ff21",
"interface": "gre-source-wg",
"address": "198.18.245.13/30",
"dynamic": "false",
"disabled": "false",
"vrf": "main"
}
],
"ipv6_address": [
{
".id": "*19",
"actual-interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"address": "fe80::2:c612:f50a/64",
"advertise": "false",
"auto-link-local": "true",
"deprecated": "false",
"disabled": "false",
"dynamic": "true",
"eui-64": "false",
"from-pool": "",
"interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"invalid": "false",
"link-local": "true",
"no-dad": "false",
"slave": "false",
"vrf": "main"
},
{
".id": "*1A",
"actual-interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"address": "fe80::2:c612:f50e/64",
"advertise": "false",
"auto-link-local": "true",
"deprecated": "false",
"disabled": "false",
"dynamic": "true",
"eui-64": "false",
"from-pool": "",
"interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"invalid": "false",
"link-local": "true",
"no-dad": "false",
"slave": "false",
"vrf": "main"
},
{
".id": "*1B",
"actual-interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"address": "fd6d:6370:2043:fbc2::1/128",
"advertise": "false",
"auto-link-local": "true",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"deprecated": "false",
"disabled": "false",
"dynamic": "false",
"eui-64": "false",
"from-pool": "",
"global": "true",
"interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"invalid": "false",
"no-dad": "false",
"slave": "false",
"vrf": "main"
},
{
".id": "*1C",
"actual-interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"address": "fd6d:6370:2043:fbc2::2/128",
"advertise": "false",
"auto-link-local": "true",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"deprecated": "false",
"disabled": "false",
"dynamic": "false",
"eui-64": "false",
"from-pool": "",
"global": "true",
"interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"invalid": "false",
"no-dad": "false",
"slave": "false",
"vrf": "main"
}
],
"ipv4_route": [
{
".id": "*20145060",
"active": "true",
"connect": "true",
"distance": "0",
"dst-address": "198.18.137.1/32",
"dynamic": "true",
"gateway": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"immediate-gw": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"inactive": "false",
"local-address": "198.18.137.1%mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"routing-table": "main",
"scope": "10",
"target-scope": "5"
},
{
".id": "*20145080",
"active": "true",
"connect": "true",
"distance": "0",
"dst-address": "198.18.137.2/32",
"dynamic": "true",
"gateway": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"immediate-gw": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"inactive": "false",
"local-address": "198.18.137.2%mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"routing-table": "main",
"scope": "10",
"target-scope": "5"
}
],
"ipv6_route": [
{
".id": "*20145070",
"active": "true",
"connect": "true",
"distance": "0",
"dst-address": "fd6d:6370:2043:fbc2::1/128",
"dynamic": "true",
"gateway": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"immediate-gw": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"inactive": "false",
"routing-table": "main",
"scope": "10",
"target-scope": "5"
},
{
".id": "*20145090",
"active": "true",
"connect": "true",
"distance": "0",
"dst-address": "fd6d:6370:2043:fbc2::2/128",
"dynamic": "true",
"gateway": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"immediate-gw": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"inactive": "false",
"routing-table": "main",
"scope": "10",
"target-scope": "5"
},
{
".id": "*20145040",
"active": "true",
"connect": "true",
"distance": "0",
"dst-address": "fe80::/64",
"dynamic": "true",
"gateway": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"immediate-gw": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"inactive": "false",
"routing-table": "main",
"scope": "10",
"target-scope": "5"
},
{
".id": "*20145050",
"active": "true",
"connect": "true",
"distance": "0",
"dst-address": "fe80::/64",
"dynamic": "true",
"gateway": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"immediate-gw": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"inactive": "false",
"routing-table": "main",
"scope": "10",
"target-scope": "5"
}
],
"routing_table": [
{
".id": "*0",
"name": "main",
"dynamic": "true",
"fib": "true"
}
],
"ethernet": [],
"ip_filter": [],
"ipv6_filter": [],
"routing_rule": [],
"ip_settings": [
{}
],
"ipv6_settings": [
{}
],
"interface_inventory": [
{
".id": "*D",
"actual-mtu": "1500",
"caps": " ARP DHCP USERARP VLAN IS_BRIDGE MESH",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"disabled": "false",
"dynamic": "false",
"flags": " UP BROADCAST RUNNING MULTICAST LOWER_UP (0x11043)",
"fp-rps-drop": "0",
"fp-rx-byte": "0",
"fp-rx-packet": "0",
"fp-tx-byte": "0",
"fp-tx-packet": "0",
"id": "13",
"ifindex": "16",
"ifname": "br0",
"l2mtu": "65535",
"last-link-up-time": "2026-10-24 15:32:46",
"link-downs": "0",
"mtu": "auto",
"name": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"running": "true",
"rx-byte": "0",
"rx-drop": "0",
"rx-error": "0",
"rx-packet": "0",
"status": "--LERUI-O---------------",
"tx-byte": "1194",
"tx-drop": "0",
"tx-error": "0",
"tx-packet": "11",
"tx-queue-drop": "0",
"type": "gre-tunnel",
"vrf": "main"
},
{
".id": "*E",
"actual-mtu": "1500",
"caps": " ARP DHCP USERARP VLAN IS_BRIDGE MESH",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"disabled": "false",
"dynamic": "false",
"flags": " UP BROADCAST RUNNING MULTICAST LOWER_UP (0x11043)",
"fp-rps-drop": "0",
"fp-rx-byte": "0",
"fp-rx-packet": "0",
"fp-tx-byte": "0",
"fp-tx-packet": "0",
"id": "14",
"ifindex": "17",
"ifname": "br1",
"l2mtu": "65535",
"last-link-up-time": "2026-10-24 15:33:04",
"link-downs": "0",
"mtu": "auto",
"name": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"running": "true",
"rx-byte": "0",
"rx-drop": "0",
"rx-error": "0",
"rx-packet": "0",
"status": "--LERUI-O---------------",
"tx-byte": "1194",
"tx-drop": "0",
"tx-error": "0",
"tx-packet": "11",
"tx-queue-drop": "0",
"type": "gre-tunnel",
"vrf": "main"
},
{
".id": "*ff11",
"name": "gre-source-wg",
"type": "wg",
"vrf": "main"
}
],
"interface_list_members": [],
"gre": [
{
".id": "*D",
"name": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"local-address": "198.18.245.9",
"remote-address": "198.18.245.10"
},
{
".id": "*E",
"name": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"local-address": "198.18.245.13",
"remote-address": "198.18.245.14"
}
],
"wireguard": [
{
".id": "*ff11",
"name": "gre-source-wg",
"private-key": "TEST_PRIVATE_KEY"
}
],
"wireguard_peer": [
{
".id": "*ff31",
"interface": "gre-source-wg",
"public-key": "TEST_PUBLIC_KEY",
"preshared-key": "TEST_PSK",
"endpoint-address": "192.0.2.2",
"endpoint-port": "55101",
"current-endpoint-address": "192.0.2.2",
"current-endpoint-port": "55101",
"allowed-address": "198.18.245.10/32",
"persistent-keepalive": "10s",
"disabled": "false",
"running": "true",
"rx": "100",
"tx": "200",
"last-handshake": "1s",
"opaque": "configuration"
}
]
}
},
"after": {
"projection": "raw-native-v1",
"serverId": 7,
"sourceProof": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"catalogs": {
"bridge": [],
"bridge_port": [],
"bridge_vlan": [],
"vrf": [
{
".id": "*4",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"disabled": "false",
"interfaces": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"name": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v"
},
{
".id": "*0",
"name": "main",
"interfaces": "all",
"disabled": "false",
"builtin": "true"
}
],
"ipv4_address": [
{
".id": "*A",
"actual-interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"address": "198.18.137.1/32",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"disabled": "false",
"dynamic": "false",
"interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"invalid": "false",
"network": "198.18.137.1",
"slave": "false",
"vrf": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v"
},
{
".id": "*B",
"actual-interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"address": "198.18.137.2/32",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"disabled": "false",
"dynamic": "false",
"interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"invalid": "false",
"network": "198.18.137.2",
"slave": "false",
"vrf": "main"
},
{
".id": "*ff20",
"interface": "gre-source-wg",
"address": "198.18.245.9/30",
"dynamic": "false",
"disabled": "false",
"vrf": "main"
},
{
".id": "*ff21",
"interface": "gre-source-wg",
"address": "198.18.245.13/30",
"dynamic": "false",
"disabled": "false",
"vrf": "main"
}
],
"ipv6_address": [
{
".id": "*1A",
"actual-interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"address": "fe80::2:c612:f50e/64",
"advertise": "false",
"auto-link-local": "true",
"deprecated": "false",
"disabled": "false",
"dynamic": "true",
"eui-64": "false",
"from-pool": "",
"interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"invalid": "false",
"link-local": "true",
"no-dad": "false",
"slave": "false",
"vrf": "main"
},
{
".id": "*1B",
"actual-interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"address": "fd6d:6370:2043:fbc2::1/128",
"advertise": "false",
"auto-link-local": "true",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"deprecated": "false",
"disabled": "false",
"dynamic": "false",
"eui-64": "false",
"from-pool": "",
"global": "true",
"interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"invalid": "false",
"no-dad": "false",
"slave": "false",
"vrf": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v"
},
{
".id": "*1C",
"actual-interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"address": "fd6d:6370:2043:fbc2::2/128",
"advertise": "false",
"auto-link-local": "true",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"deprecated": "false",
"disabled": "false",
"dynamic": "false",
"eui-64": "false",
"from-pool": "",
"global": "true",
"interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"invalid": "false",
"no-dad": "false",
"slave": "false",
"vrf": "main"
},
{
".id": "*1D",
"actual-interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"address": "fe80::2:c612:f50a/64",
"advertise": "false",
"auto-link-local": "true",
"deprecated": "false",
"disabled": "false",
"dynamic": "true",
"eui-64": "false",
"from-pool": "",
"interface": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"invalid": "false",
"link-local": "true",
"no-dad": "false",
"slave": "false",
"vrf": "main"
}
],
"ipv4_route": [
{
".id": "*20145080",
"active": "true",
"connect": "true",
"distance": "0",
"dst-address": "198.18.137.2/32",
"dynamic": "true",
"gateway": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"immediate-gw": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"inactive": "false",
"local-address": "198.18.137.2%mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"routing-table": "main",
"scope": "10",
"target-scope": "5"
},
{
".id": "*201450B0",
"active": "true",
"connect": "true",
"distance": "0",
"dst-address": "198.18.137.1/32",
"dynamic": "true",
"gateway": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a@mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v",
"immediate-gw": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"inactive": "false",
"local-address": "198.18.137.1%mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a@mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v",
"routing-table": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v",
"scope": "10",
"target-scope": "5"
}
],
"ipv6_route": [
{
".id": "*20145090",
"active": "true",
"connect": "true",
"distance": "0",
"dst-address": "fd6d:6370:2043:fbc2::2/128",
"dynamic": "true",
"gateway": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"immediate-gw": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"inactive": "false",
"routing-table": "main",
"scope": "10",
"target-scope": "5"
},
{
".id": "*20145050",
"active": "true",
"connect": "true",
"distance": "0",
"dst-address": "fe80::/64",
"dynamic": "true",
"gateway": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"immediate-gw": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"inactive": "false",
"routing-table": "main",
"scope": "10",
"target-scope": "5"
},
{
".id": "*201450C0",
"active": "true",
"connect": "true",
"distance": "0",
"dst-address": "fd6d:6370:2043:fbc2::1/128",
"dynamic": "true",
"gateway": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a@mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v",
"immediate-gw": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"inactive": "false",
"routing-table": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v",
"scope": "10",
"target-scope": "5"
},
{
".id": "*20145040",
"active": "true",
"connect": "true",
"distance": "0",
"dst-address": "fe80::/64",
"dynamic": "true",
"gateway": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a@mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v",
"immediate-gw": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"inactive": "false",
"routing-table": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v",
"scope": "10",
"target-scope": "5"
}
],
"routing_table": [
{
".id": "*0",
"name": "main",
"dynamic": "true",
"fib": "true"
},
{
".id": "*4",
"dynamic": "true",
"fib": "",
"invalid": "false",
"name": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v"
}
],
"ethernet": [],
"ip_filter": [],
"ipv6_filter": [],
"routing_rule": [],
"ip_settings": [
{}
],
"ipv6_settings": [
{}
],
"interface_inventory": [
{
".id": "*D",
"actual-mtu": "1500",
"caps": " ARP DHCP USERARP VLAN IS_BRIDGE MESH",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"disabled": "false",
"dynamic": "false",
"flags": " UP BROADCAST RUNNING MULTICAST LOWER_UP (0x11043)",
"fp-rps-drop": "0",
"fp-rx-byte": "0",
"fp-rx-packet": "0",
"fp-tx-byte": "0",
"fp-tx-packet": "0",
"id": "13",
"ifindex": "16",
"ifname": "br0",
"l2mtu": "65535",
"last-link-up-time": "2026-10-24 15:32:46",
"link-downs": "0",
"mtu": "auto",
"name": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"running": "true",
"rx-byte": "0",
"rx-drop": "0",
"rx-error": "0",
"rx-packet": "0",
"status": "--LERUI-O-----------V---",
"tx-byte": "2146",
"tx-drop": "0",
"tx-error": "0",
"tx-packet": "19",
"tx-queue-drop": "0",
"type": "gre-tunnel",
"vrf": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v"
},
{
".id": "*E",
"actual-mtu": "1500",
"caps": " ARP DHCP USERARP VLAN IS_BRIDGE MESH",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797",
"disabled": "false",
"dynamic": "false",
"flags": " UP BROADCAST RUNNING MULTICAST LOWER_UP (0x11043)",
"fp-rps-drop": "0",
"fp-rx-byte": "0",
"fp-rx-packet": "0",
"fp-tx-byte": "0",
"fp-tx-packet": "0",
"id": "14",
"ifindex": "17",
"ifname": "br1",
"l2mtu": "65535",
"last-link-up-time": "2026-10-24 15:33:04",
"link-downs": "0",
"mtu": "auto",
"name": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"running": "true",
"rx-byte": "0",
"rx-drop": "0",
"rx-error": "0",
"rx-packet": "0",
"status": "--LERUI-O---------------",
"tx-byte": "1194",
"tx-drop": "0",
"tx-error": "0",
"tx-packet": "11",
"tx-queue-drop": "0",
"type": "gre-tunnel",
"vrf": "main"
},
{
".id": "*F",
"actual-mtu": "65536",
"caps": "",
"disabled": "false",
"flags": " UP RUNNING NOARP MASTER LOWER_UP (0x104c1)",
"fp-rps-drop": "0",
"fp-rx-byte": "0",
"fp-rx-packet": "0",
"fp-tx-byte": "0",
"fp-tx-packet": "0",
"id": "15",
"ifindex": "18",
"ifname": "vrf4",
"last-link-up-time": "2026-10-24 15:34:48",
"link-downs": "0",
"mac-address": "FA:D1:FD:E0:45:DD",
"mtu": "65536",
"name": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v",
"running": "true",
"rx-byte": "0",
"rx-drop": "0",
"rx-error": "0",
"rx-packet": "0",
"status": "--LERU--O---------------",
"tx-byte": "0",
"tx-drop": "0",
"tx-error": "0",
"tx-packet": "0",
"tx-queue-drop": "0",
"type": "vrf",
"vrf": "main"
},
{
".id": "*ff11",
"name": "gre-source-wg",
"type": "wg",
"vrf": "main"
}
],
"interface_list_members": [],
"gre": [
{
".id": "*D",
"name": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"local-address": "198.18.245.9",
"remote-address": "198.18.245.10"
},
{
".id": "*E",
"name": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-b",
"local-address": "198.18.245.13",
"remote-address": "198.18.245.14"
}
],
"wireguard": [
{
".id": "*ff11",
"name": "gre-source-wg",
"private-key": "TEST_PRIVATE_KEY"
}
],
"wireguard_peer": [
{
".id": "*ff31",
"interface": "gre-source-wg",
"public-key": "TEST_PUBLIC_KEY",
"preshared-key": "TEST_PSK",
"endpoint-address": "192.0.2.2",
"endpoint-port": "55101",
"current-endpoint-address": "192.0.2.2",
"current-endpoint-port": "55101",
"allowed-address": "198.18.245.10/32",
"persistent-keepalive": "10s",
"disabled": "false",
"running": "true",
"rx": "100",
"tx": "200",
"last-handshake": "1s",
"opaque": "configuration"
}
]
}
},
"binding": {
"serverId": 7,
"sourceProof": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"actorId": "legacy-human",
"planId": "legacy-plan"
},
"request": {
"action": "create",
"fields": {
"name": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-v",
"interfaces": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797-a",
"comment": "mcp-test-vrf-companion-3890a4fa-2f69-4fcc-b11e-5ea4d1245797"
},
"placeBeforeId": "*0"
},
"scope": {
"serverId": 7,
"sourceProof": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"actorId": "legacy-human",
"nativeId": "*4"
},
"now": 1000,
"secretFill": 7,
"acknowledgedId": "*4",
"admissionV2": "eyJiaW5kaW5nIjp7ImFjdG9ySWQiOiJsZWdhY3ktaHVtYW4iLCJwbGFuSWQiOiJsZWdhY3ktcGxhbiIsInNlcnZlcklkIjo3LCJzb3VyY2VQcm9vZiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiY29tcGFuaW9uRXhlY3V0aW9uIjoicmVxdWlyZXMtbmF0aXZlLXRyYW5zaXRpb24tcHJvb2YiLCJleHBpcmVzQXQiOjMwMTAwMCwiaXNzdWVkQXQiOjEwMDAsIm1lbWJlcnMiOlt7ImFkZHJlc3NlcyI6W3siY29uZmlndXJhdGlvbkhhc2giOiI1MDI3ZDZiZTMwN2ZlYTYxYTY3NTE2N2FhMjExMjUwZDc1NTNhMTViNThjY2FmMDAxNDE3YzIyYjgzOTdkOGVmIiwiaWQiOiIqQSIsInBhcmVudElkIjoiKkQiLCJyZXNvdXJjZSI6ImlwdjRfYWRkcmVzcyJ9LHsiY29uZmlndXJhdGlvbkhhc2giOiI2MDU1NzBmYmE1MzBhYmQ3YzM5NjFiODhmMWZkN2YyNjhiNzA1MTUxZTlhNDY1NzA2MmFiMWVhMmY4MmE3NTU5IiwiaWQiOiIqZmYyMCIsInBhcmVudElkIjoiKmZmMTEiLCJyZXNvdXJjZSI6ImlwdjRfYWRkcmVzcyJ9LHsiY29uZmlndXJhdGlvbkhhc2giOiI4YjJmZjMzYmE4MTUxNDQyZmI0Yjk5Y2Q5Y2YxMWVkOWJlYjE0ZTI4NTgxNTJjZDI0YTk0ZDdlYThmY2ViM2IxIiwiaWQiOiIqZmYyMSIsInBhcmVudElkIjoiKmZmMTEiLCJyZXNvdXJjZSI6ImlwdjRfYWRkcmVzcyJ9LHsiY29uZmlndXJhdGlvbkhhc2giOiIxMTIwZGE0OTViMjVmN2RhN2U0NDQwNzUxMjNhOWU0NzhhZjlhOTFjYjk5ZTE2MTcwNDhhNDAwZTQ1MzYzZjRmIiwiaWQiOiIqMTkiLCJwYXJlbnRJZCI6IipEIiwicmVzb3VyY2UiOiJpcHY2X2FkZHJlc3MifSx7ImNvbmZpZ3VyYXRpb25IYXNoIjoiNTdlMWZiYmMwOGMzZTNiYWNkZTg0YjcyZDM2NWQ3YjVjNDY4MTEyNTM0MWZhNDM1NTI4ZmRmMzAzOTllMDdhMiIsImlkIjoiKjFCIiwicGFyZW50SWQiOiIqRCIsInJlc291cmNlIjoiaXB2Nl9hZGRyZXNzIn1dLCJjb25maWd1cmF0aW9uSGFzaCI6ImVjZDYxMDllZTJkMzQ1NjAxOGM3Mzg2ZmZkY2FmZTRjNzMyZDZjMDdlMGVhMDNhOTVjOTYwMWM4ZTczYTM2ZjAiLCJpZCI6IipEIiwibmFtZSI6Im1jcC10ZXN0LXZyZi1jb21wYW5pb24tMzg5MGE0ZmEtMmY2OS00ZmNjLWIxMWUtNWVhNGQxMjQ1Nzk3LWEiLCJwYXJlbnRzIjpbeyJjb25maWd1cmF0aW9uSGFzaCI6ImNhYjg0MzdlYzY5OWE0N2Y1NjI1YTk2ZmQ0NzM4YzNiNDhlZDNhNTI4YjNlMTZkMzM3NjE5YjY2YmUwOWY2YjMiLCJjb25maWd1cmF0aW9uSWQiOiIqRCIsImlkIjoiKkQiLCJuYW1lIjoibWNwLXRlc3QtdnJmLWNvbXBhbmlvbi0zODkwYTRmYS0yZjY5LTRmY2MtYjExZS01ZWE0ZDEyNDU3OTctYSIsInJlc291cmNlIjoiZ3JlIn0seyJjb25maWd1cmF0aW9uSGFzaCI6ImZiNGExZmMyZWVjYzZlYWRmM2U0Y2YwNDE0ZjVhMDMwNjI3ODkxNjY1NWYxMDVjOGM5YmQwODU5ODU2ODdmNzUiLCJjb25maWd1cmF0aW9uSWQiOiIqZmYxMSIsImlkIjoiKmZmMTEiLCJuYW1lIjoiZ3JlLXNvdXJjZS13ZyIsInJlc291cmNlIjoid2lyZWd1YXJkIn1dfV0sInJlcXVlc3QiOnsiYWN0aW9uIjoiY3JlYXRlIiwiZmllbGRzIjp7ImNvbW1lbnQiOiJtY3AtdGVzdC12cmYtY29tcGFuaW9uLTM4OTBhNGZhLTJmNjktNGZjYy1iMTFlLTVlYTRkMTI0NTc5NyIsImludGVyZmFjZXMiOiJtY3AtdGVzdC12cmYtY29tcGFuaW9uLTM4OTBhNGZhLTJmNjktNGZjYy1iMTFlLTVlYTRkMTI0NTc5Ny1hIiwibmFtZSI6Im1jcC10ZXN0LXZyZi1jb21wYW5pb24tMzg5MGE0ZmEtMmY2OS00ZmNjLWIxMWUtNWVhNGQxMjQ1Nzk3LXYifSwicGxhY2VCZWZvcmVJZCI6IiowIn0sInNuYXBzaG90SGFzaCI6IjYxZDRjZmM2MTEyNjdlYjgwMWUyMTJhOTk1ZjE2NTY4ZGQyZjFhOWNhZWU0M2FmN2JiNmQ1MTIxYzgyM2UxYjIiLCJ0aWNrZXRJZCI6ImNlNTI1MWViLTUwY2ItNDU2YS05ZjM0LTFmNWE5YjIwZTBhMyIsInZlcnNpb24iOjJ9.962f7d4246e57f9e660c3e250fbd088e7b73b3ddd5863b5456004fc53a8f6908",
"checkpointProofV1": "eyJ2ZXJzaW9uIjoyLCJzY29wZSI6eyJzZXJ2ZXJJZCI6Nywic291cmNlUHJvb2YiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWN0b3JJZCI6ImxlZ2FjeS1odW1hbiIsIm5hdGl2ZUlkIjoiKjQifSwiaGlzdG9yeSI6eyJzbmFwc2hvdCI6eyJraW5kIjoiY29uZmlndXJhdGlvbi1zaGEyNTYtdjEiLCJzZXJ2ZXJJZCI6Nywic291cmNlUHJvb2YiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiY2F0YWxvZ0RpZ2VzdHMiOnsiYnJpZGdlIjoiMzlmNzdkYjVlMzdlM2IzMGRiMjlhZTU3MjBkYTFjZTk1ODdkNzBlM2M4NTEyNmJhMTA2ZjJmYzM1NjI5MzUyNyIsImJyaWRnZV9wb3J0IjoiYWExY2IwZjI0ZjE5ZWVlYThiMTZkNjU4NWY3Mzg4ZTQ4MzI3NTczMjY3NmJhYzJlZTcxMWMxMGFmNmE3YjI2NCIsImJyaWRnZV92bGFuIjoiNzZjYzJmMTM5Nzk4Mzk0ODEwZDBkOTA5MzMzNmZmYzI1ZWMxOWMzNzM3ZTU5ZDgxYmNmMTJiM2NjNmZhMWMyOCIsImV0aGVybmV0IjoiZmYxOWVkYWIxODFkYzUyYmUyYTg3M2UyOGExYTUzODA5YTU1YWMwYjgwY2M3YTQ4MTIzNWM2NWU2ZGZkNTk1YyIsImdyZSI6ImI4OTEyNzA4YjAzNDk4NzA4NjQ4NTMzMGEzMjVlYTMzNWFiYjhkMmY0OWQyOGNkNjFhMjVmOTYzZDg1NDljYmYiLCJpbnRlcmZhY2VfaW52ZW50b3J5IjoiYmEzZWVlOGQxZGZkODA4MmUxZjZlY2Y5ZjdlYzA4NDFkNWNhMDc1ZmVkZTdiYmNmNzYyMzZjMmVmY2U0MmY4ZiIsImludGVyZmFjZV9saXN0X21lbWJlcnMiOiI1ODlmMmNkMzA5OWY1MWE3MzM4OGRkMzQxZGRjOWY4MmIxMTIyNjg4NDgxZTdhMjcxOTk5OWUxODYyZWFjZDkwIiwiaXBfZmlsdGVyIjoiNWI2NmI1YmZmYzNkOGU0MjVlMjRlNmJmYTNkYjE2MDkyYmQ3MWFmODVlMjc3NGFjNzFhOWE4YTk4ZGU0ZDVjOSIsImlwX3NldHRpbmdzIjoiZjc1NTNjODdiZThjZmViMTNhNWY1MTIwMjliZGEwZmMzNGFkMDUzY2UyZjcxMjg5ZTIwODk0OTI0MDNhMzliNyIsImlwdjRfYWRkcmVzcyI6ImI0YTQyYTQzN2NlZTgzYmZlY2RjNDQwZWVmNGEwNDA1NWY5MzYyNzI5M2VjNmExMTMyOWMyMWFjYjNhNzJhMWMiLCJpcHY0X3JvdXRlIjoiYTZiM2JlZTQyMWJlYzEyNzViNThlODMwYWY4MTY0NjI1N2NhOWRjNGY2YjY3NmE0ZTg0YzQxOTk4NDU3NzJlOCIsImlwdjZfYWRkcmVzcyI6ImY1YTJlMWFjN2NhNThhMmYxMTFlN2I0ZmFmMmZmNTE4MmNhM2Q0YzRiYjM3MWExZDRjODAyY2EzNjk0NTYzZWUiLCJpcHY2X2ZpbHRlciI6IjIzNzVmMDJmMDhlZTc2NjdlYWY5OWE3OTQwZjA3ZWVmOGI4OTMwNmIwMGI0YzQ5NTU4ODc3ZTM2YmMwN2Q2ZjQiLCJpcHY2X3JvdXRlIjoiOGY0ZDA3NjY0NzI0NmQyY2IxNTg2NTg4NjE5ZjUzNWQ0NWMwMTczYWQyOWRkYzNlZjExNmI0NjViYjY4NGUyMCIsImlwdjZfc2V0dGluZ3MiOiIzZDVhZWFjYzgxZjZjMTAxYzk5MTFmZmE5NmZjZmY5YmQ2MDM0M2UxZDFkNTlmNTRkNTNjZmJlM2JjMzYxYjIwIiwicm91dGluZ19ydWxlIjoiMDY3MGQ1OTIzMGVkNmJhNTczYjhlMzkwMDdiODJiMGI2ODNhMDkwYmFmZTY1YTBmNzEyMzBhZWZjYjhhZDhhMiIsInJvdXRpbmdfdGFibGUiOiJlOTMzMWRiMjU2ODczNjIwZmRkY2NmYzk2MmE3MjlmNjU5NjU2MzhjZjBiZWJiYzRmYTg0MjdlMGRiYjQ0OTEwIiwidnJmIjoiMzQ5OTlkZTIyM2FjNDU5MDc2OGUwNzY3ODE3NmNkMTVmYTA4OTgxMzRlYTU0ZTlmNDhmZWJkYTA3NTI5OGJiMyIsIndpcmVndWFyZCI6ImFjZTYyZGIyNTBlYjkxOGRjOWY0YWI3MzU2N2U5ODRhY2UyYjE2ZDhhN2U0OWIzYmM3MGY1MWQ1NmJlNWI0MzkiLCJ3aXJlZ3VhcmRfcGVlciI6IjBlOTg3MmQ5NWQzNTAwMTRkNjgzN2EyZjk0YTY1NzczYzA3NDZiMzVmMzJlNWM3ZmYyNzBlNjZlNzE0OGNlZTcifX0sInZyZklkIjoiKjQiLCJhY3RpdmVWaXJ0dWFsIjp7Ii5pZCI6IipGIiwiYWN0dWFsLW10dSI6IjY1NTM2IiwiY2FwcyI6IiIsImRpc2FibGVkIjoiZmFsc2UiLCJmbGFncyI6IiBVUCBSVU5OSU5HIE5PQVJQIE1BU1RFUiBMT1dFUl9VUCAoMHgxMDRjMSkiLCJmcC1ycHMtZHJvcCI6IjAiLCJmcC1yeC1ieXRlIjoiMCIsImZwLXJ4LXBhY2tldCI6IjAiLCJmcC10eC1ieXRlIjoiMCIsImZwLXR4LXBhY2tldCI6IjAiLCJpZCI6IjE1IiwiaWZpbmRleCI6IjE4IiwiaWZuYW1lIjoidnJmNCIsImxhc3QtbGluay11cC10aW1lIjoiMjAyNi0xMC0yNCAxNTozNDo0OCIsImxpbmstZG93bnMiOiIwIiwibWFjLWFkZHJlc3MiOiJGQTpEMTpGRDpFMDo0NTpERCIsIm10dSI6IjY1NTM2IiwibmFtZSI6Im1jcC10ZXN0LXZyZi1jb21wYW5pb24tMzg5MGE0ZmEtMmY2OS00ZmNjLWIxMWUtNWVhNGQxMjQ1Nzk3LXYiLCJydW5uaW5nIjoidHJ1ZSIsInJ4LWJ5dGUiOiIwIiwicngtZHJvcCI6IjAiLCJyeC1lcnJvciI6IjAiLCJyeC1wYWNrZXQiOiIwIiwic3RhdHVzIjoiLS1MRVJVLS1PLS0tLS0tLS0tLS0tLS0tIiwidHgtYnl0ZSI6IjAiLCJ0eC1kcm9wIjoiMCIsInR4LWVycm9yIjoiMCIsInR4LXBhY2tldCI6IjAiLCJ0eC1xdWV1ZS1kcm9wIjoiMCIsInR5cGUiOiJ2cmYiLCJ2cmYiOiJtYWluIn0sImFic2VudExsIjpbXSwicHJvdmVuYW5jZSI6eyJtaXNzaW5nTWVtYmVycyI6W10sImdyZU1lbWJlcnMiOlt7Ii5pZCI6IipEIiwibmFtZSI6Im1jcC10ZXN0LXZyZi1jb21wYW5pb24tMzg5MGE0ZmEtMmY2OS00ZmNjLWIxMWUtNWVhNGQxMjQ1Nzk3LWEiLCJ0eXBlIjoiZ3JlLXR1bm5lbCIsImdyZVBhcmVudCI6eyJjb25maWd1cmF0aW9uIjp7Ii5pZCI6IipEIiwibmFtZSI6Im1jcC10ZXN0LXZyZi1jb21wYW5pb24tMzg5MGE0ZmEtMmY2OS00ZmNjLWIxMWUtNWVhNGQxMjQ1Nzk3LWEiLCJsb2NhbC1hZGRyZXNzIjoiMTk4LjE4LjI0NS45IiwicmVtb3RlLWFkZHJlc3MiOiIxOTguMTguMjQ1LjEwIn0sInNvdXJjZUFkZHJlc3MiOnsiLmlkIjoiKmZmMjAiLCJpbnRlcmZhY2UiOiJncmUtc291cmNlLXdnIiwiYWRkcmVzcyI6IjE5OC4xOC4yNDUuOS8zMCIsImR5bmFtaWMiOiJmYWxzZSIsImRpc2FibGVkIjoiZmFsc2UiLCJ2cmYiOiJtYWluIn0sInNvdXJjZUludmVudG9yeSI6eyIuaWQiOiIqZmYxMSIsIm5hbWUiOiJncmUtc291cmNlLXdnIiwidHlwZSI6IndnIn0sInNvdXJjZUNvbmZpZ3VyYXRpb24iOnsiLmlkIjoiKmZmMTEiLCJuYW1lIjoiZ3JlLXNvdXJjZS13ZyJ9LCJzb3VyY2VSZXNvdXJjZSI6IndpcmVndWFyZCJ9fV19fX0.5e08c0fe549e780a6f0662649078b173eda76e166d254f8565e645f2504915e7"
}
+2 -2
View File
@@ -726,8 +726,8 @@ export async function listIpsecClientsForCatalog(serverId: number): Promise<{
}),
])
return { clients: mapClients(state) }
} catch (e) {
return { clients: [], error: e instanceof Error ? e.message : String(e) }
} catch {
return { clients: [], error: "Не удалось получить список клиентов IPsec." }
}
}
+20 -8
View File
@@ -5,6 +5,7 @@ import { readOspfIdentityCatalog } from "./ospf-identity-catalog.js"
import { observedBoolean } from "./ospf-observed-values.js"
import { ospfCostConnectionProof } from "./ospf-template-cost-write.js"
import { readRoutingObservations } from "../modules/mcp/routing-observations.js"
import { latestRoutingMetric } from "../modules/mcp/routing-observations-model.js"
import { createRoutingSourceContext, readRoutingEndpoints } from "../modules/mcp/routing-observation-source.js"
import { fresh } from "../modules/mcp/routing-model.js"
import { fingerprint, McpError } from "../modules/mcp/security.js"
@@ -12,7 +13,7 @@ import { configurationRows, readRosSnapshot, resourceById, routerClient, type Ro
import { ospfTable, requireOspfBfdSession } from "../modules/mcp/ospf-health.js"
import type { ChangeEvidence } from "../modules/mcp/action-types.js"
export interface OspfRank { id: string; interface: string; currentCost: number; proposedCost: number; score: number; pingMs: number | null; lossPct: number | null; dlMbps: number | null; ulMbps: number | null; observationId: string; peerId: number; validUntil: string }
export interface OspfRank { id: string; interface: string; currentCost: number; proposedCost: number; score: number; pingMs: number | null; lossPct: number | null; dlMbps: number | null; ulMbps: number | null; observationId: string; bandwidthObservationId: string | null; peerId: number; validUntil: string }
/** Seal a current source-bound snapshot only if it still matches the catalog
* used for ranking/identity resolution. Never attach new provenance to old rows. */
@@ -87,16 +88,22 @@ export async function buildOspfCostOptimizationEvidence(input: Record<string, un
const owner = resolveOspfLinkOwner(serverId, links.get(serverId)!, instance[".id"], matches[0][".id"], [...links].map(([id, catalog]) => ({ serverId: id, catalog })))
if (!owner) return []
const peer = owner.serverId
const observation = observations.find(observation => observation.srcServerId === serverId && observation.dstServerId === peer && observation.srcInterface === template.interface && observation.dstInterface === owner.interface && observation.family === input.family && observation.table === input.table)
if (!observation || !observation.contextVerified || !fresh(observation.pingAt, 300, now) || observation.pingMs === null || observation.lossPct === null || observation.lossPct > Number(input.maxLossPct) || Number(input.pingWeight) !== 100 && (!fresh(observation.bandwidthAt, 3600, now) || observation.dlMbps === null || observation.ulMbps === null)) return []
const score = Number(input.pingWeight) / 100 * Math.max(0, 100 - observation.pingMs! * 0.6) + (1 - Number(input.pingWeight) / 100) * Math.min(100, ((observation.dlMbps ?? 0) + (observation.ulMbps ?? 0)) / 18)
const validUntil = Math.min(Date.parse(observation.pingAt!) + 300000, Number(input.pingWeight) === 100 ? Infinity : Date.parse(observation.bandwidthAt!) + 3600000)
const linkObservations = observations.filter(row => row.srcServerId === serverId && row.dstServerId === peer && row.srcInterface === template.interface && row.dstInterface === owner.interface && row.family === input.family && row.table === input.table)
const ping = latestRoutingMetric(linkObservations, "pingAt")
if (!ping?.contextVerified || !fresh(ping.pingAt, 300, now) || ping.pingMs === null || ping.lossPct === null || ping.lossPct > Number(input.maxLossPct)) return []
const pingWeight = Number(input.pingWeight) / 100
const bandwidth = pingWeight === 1 ? undefined : latestRoutingMetric(linkObservations, "bandwidthAt")
if (pingWeight !== 1 && (!bandwidth?.contextVerified || !fresh(bandwidth.bandwidthAt, 3600, now) || bandwidth.dlMbps === null || bandwidth.ulMbps === null)) return []
const score = pingWeight * Math.max(0, 100 - ping.pingMs * 0.6) + (1 - pingWeight) * Math.min(100, ((bandwidth?.dlMbps ?? 0) + (bandwidth?.ulMbps ?? 0)) / 18)
const pingUntil = Date.parse(ping.pingAt!) + 300000
const bandwidthUntil = bandwidth ? Date.parse(bandwidth.bandwidthAt!) + 3600000 : Infinity
const validUntil = Math.min(pingUntil, bandwidthUntil)
const bfd = bfdFlags.get(template.id) ? requireOspfBfdSession(bfdSessions as unknown as Record<string, unknown>[], matches[0].address, template.interface, String(input.table)) : null
return [{ template, owner, score, observation, observationId: observation.id, validUntil, peerId: peer!, neighborId: String(matches[0][".id"]), bfd }]
return [{ template, owner, score, ping, bandwidth, pingUntil, bandwidthUntil, validUntil, peerId: peer, neighborId: String(matches[0][".id"]), bfd }]
}).sort((a, b) => b.score - a.score || a.template.interface.localeCompare(b.template.interface))
if (!ranked.length) throw new McpError("MEASUREMENT_UNKNOWN", "Нет однозначных шаблонов и свежих измерений нужного instance/family/VRF", 409)
const optimalCost = (index: number) => index < Number(input.ecmpPaths) ? 10 : (index - Number(input.ecmpPaths) + 2) * 10
const before = { ranking: ranked.map((item, index) => ({ id: item.template.id, interface: item.template.interface, currentCost: item.template.cost, proposedCost: optimalCost(index), score: item.score, pingMs: item.observation.pingMs, lossPct: item.observation.lossPct, dlMbps: item.observation.dlMbps, ulMbps: item.observation.ulMbps, observationId: item.observationId, peerId: item.peerId, peerInstance: item.owner.instanceId, peerVrf: item.owner.vrf, peerInterface: item.owner.interface, validUntil: new Date(item.validUntil).toISOString() })) }
const before = { ranking: ranked.map((item, index) => ({ id: item.template.id, interface: item.template.interface, currentCost: item.template.cost, proposedCost: optimalCost(index), score: item.score, pingMs: item.ping.pingMs, lossPct: item.ping.lossPct, dlMbps: item.bandwidth?.dlMbps ?? null, ulMbps: item.bandwidth?.ulMbps ?? null, observationId: item.ping.id, bandwidthObservationId: item.bandwidth?.id ?? null, peerId: item.peerId, peerInstance: item.owner.instanceId, peerVrf: item.owner.vrf, peerInterface: item.owner.interface, validUntil: new Date(item.validUntil).toISOString() })) }
const steps: ChangeEvidence["steps"] = ranked.flatMap((item, index) => {
const cost = optimalCost(index)
return cost === item.template.cost ? [] : [{ kind: "ros" as const, step: { serverId, resource: "ospf_template", action: "update" as const, id: item.template.id, fields: { cost: String(cost) }, expectedCost: item.template.cost, expectedServerProof } }]
@@ -108,7 +115,12 @@ export async function buildOspfCostOptimizationEvidence(input: Record<string, un
const healthChecks: NonNullable<ChangeEvidence["healthChecks"]> = ranked.map(item => ({ serverId, resource: "ospf_neighbor", id: item.neighborId, field: "state", identity: { interface: item.template.interface, instance: String(neighbors.find(row => row[".id"] === item.neighborId)!.instance) } }))
for (const item of ranked) if (item.bfd) healthChecks.push({ serverId, resource: "bfd_session", id: String(item.bfd[".id"]), field: "bfd-up", identity: Object.fromEntries(["remote-address", "local-address", "vrf", "interface"].filter(key => typeof item.bfd![key] === "string").map(key => [key, String(item.bfd![key])])) })
const bfdSnapshots = bfdRequired ? [await readRosSnapshot(serverId, "bfd_configuration", beforeRead), await capture(serverId, "bfd_session", bfdSessions)] : []
const observationChecks = ranked.map(item => ({ id: item.observation.id, srcServerId: serverId, dstServerId: item.peerId, srcInterface: item.template.interface, family: String(input.family), table: String(input.table), fingerprint: fingerprint(item.observation), validUntil: new Date(item.validUntil).toISOString() }))
const observationChecks = ranked.flatMap<NonNullable<ChangeEvidence["observationChecks"]>[number]>(item => {
const link = { srcServerId: serverId, dstServerId: item.peerId, srcInterface: item.template.interface, dstInterface: item.owner.interface, family, table: String(input.table) }
const ping = { ...link, id: item.ping.id, metric: "pingAt" as const, fingerprint: fingerprint(item.ping), validUntil: new Date(item.pingUntil).toISOString() }
if (!item.bandwidth) return [ping]
return [ping, { ...link, id: item.bandwidth.id, metric: "bandwidthAt" as const, fingerprint: fingerprint(item.bandwidth), validUntil: new Date(item.bandwidthUntil).toISOString() }]
})
await beforeRead()
return { before, observationChecks, ospfOwnerGraph: { family, sourceSecretProofs: sourceContext.expected, instances: [...ownerSnapshots.values()].map(row => ({ serverId: row.serverId, fingerprint: fingerprint({ native: row.fingerprint, identity: identityFingerprints.get(row.serverId) }) })) }, target: { scope: "servers", serverIds: dependencyIds }, objects: ranked.map(item => ({ kind: "ospf_template", id: item.template.id, serverId })), snapshots: [snapshot, await capture(serverId, "ospf_neighbor", neighbors), await capture(serverId, "ospf_instance", primaryIdentity.raw), await capture(serverId, "ospf_area", areas), ...dependencySnapshots, ...bfdSnapshots], steps, healthChecks, validUntil: new Date(Math.min(...ranked.map(item => item.validUntil))).toISOString(), warnings: ["Рейтинг локальных интерфейсов не доказывает глобальный оптимум.", "Неизвестные шаблоны не меняются.", "Full выбранных соседств и Up включённых BFD-сессий проверяются до и после cost-записей. Сквозная связность требует контрольных проб; после истечения измерений нужен новый план."], requiresHuman: true }
}
@@ -45,7 +45,7 @@ function remaining(deadlineAt: number) {
* Its bounded server timeout also applies when a surrounding Drizzle tx owns
* the lifecycle. The surrounding owner must roll back/destroy on rejection.
*/
export async function admitPartitionWriter(client: Pick<PoolClient, "query">, options: Omit<PartitionWriteOptions, "connect"> = {}): Promise<void> {
export async function admitPartitionWriter(client: { query(text: string, values?: [number]): Promise<unknown> }, options: Omit<PartitionWriteOptions, "connect"> = {}): Promise<void> {
const current = settings(options)
const check = async () => {
if (current.signal?.aborted) throw new PartitionWriteError()
@@ -7,3 +7,9 @@ export function canonicalRoutingFilterBody(body: string) {
export function routingFilterBodiesMatch(actual: unknown, expected: string) {
return typeof actual === "string" && canonicalRoutingFilterBody(actual) === canonicalRoutingFilterBody(expected)
}
/** Compare complete managed bodies, preserving bytes outside the compiler's interface assignment. */
export function managedRoutingFilterBodiesMatch(actual: unknown, expected: string) {
const normalize = (body: string) => body.replace(/^ set gw-interface "([A-Za-z_][A-Za-z0-9_.-]{0,99})";$/gm, " set gw-interface $1;")
return typeof actual === "string" && (actual === expected || normalize(actual) === normalize(expected))
}
@@ -3,6 +3,9 @@ import { AsyncLocalStorage } from "node:async_hooks"
import { PARTITION_SPECS } from "../db/partitions.js"
import type { ServerPollSqlConnection } from "./server-poll-production.js"
import type { ServerPollBoundaryCoordinator } from "./server-poll-boundary.js"
import type { ProbeStorageOptions, ProbeStorageSql } from "./server-probe-journal-maintenance.js"
import { createProbeStorageConnectionConnector } from "./server-probe-storage-connection.js"
import { runPollStorageTransaction } from "./server-poll-storage.js"
export const SERVER_PARTITION_WRITER_LOCK=8723110
export interface ServerPartitionLeaseOptions {signal?:AbortSignal;deadlineAt:number;serverIds:number[]}
@@ -48,11 +51,17 @@ ALTER TABLE public."${parent}" ENABLE ALWAYS TRIGGER server_partition_writer_gua
`
interface Holder {connection:ServerPollSqlConnection;options:ServerPartitionLeaseOptions;active:boolean;accepting:boolean;released:boolean;tail:Promise<void>;destroy:()=>void}
interface TransactionScope {holder:Holder;active:boolean;readOnly:boolean;savepoint:number;tail:Promise<void>;parent?:TransactionScope}
export interface ServerPollJournalOwner {
transaction<T>(options:ProbeStorageOptions,authorize:()=>Promise<void>,work:(sql:ProbeStorageSql)=>Promise<T>):Promise<T>
assertClosedForQuarantine():void
}
const journalOwners=new WeakSet<ServerPollJournalOwner>()
export const isServerPollJournalOwner=(owner:ServerPollJournalOwner)=>journalOwners.has(owner)
/** One backend owns the session exclusive lock AND every maintenance SQL
* transaction. PostgreSQL shared advisory acquisition in its row triggers is
* reentrant on that same backend. No GUC, pool patch or bypass token exists. */
export function createServerPartitionMaintenanceLease(deps:ServerPartitionMaintenanceLeaseDependencies){
const now=deps.now??Date.now,held=new AsyncLocalStorage<Holder>(),transactions=new AsyncLocalStorage<TransactionScope>()
const now=deps.now??Date.now,held=new AsyncLocalStorage<Holder>(),transactions=new AsyncLocalStorage<TransactionScope>(),journal=new AsyncLocalStorage<Holder>()
const fail=(unknown=false):never=>{throw new ServerPartitionLeaseError(unknown)}
const checkOptions=(options:ServerPartitionTransactionOptions)=>{if(!Number.isSafeInteger(options.deadlineAt)||options.deadlineAt<=now()||options.deadlineAt-now()>SERVER_POLL_WALL_BUDGET_MS||options.signal?.aborted)fail()}
const local=(holder:Holder,options:ServerPartitionTransactionOptions)=>{
@@ -116,6 +125,7 @@ export function createServerPartitionMaintenanceLease(deps:ServerPartitionMainte
const transaction=async<T>(readOnly:boolean,fence:()=>Promise<void>,options:ServerPartitionTransactionOptions,work:(query:ServerPartitionQuery)=>Promise<T>):Promise<T>=>{
checkOptions(options);const holder=held.getStore(),parent=transactions.getStore()
if(holder){
if(journal.getStore()===holder)fail()
if(!holder.active||!holder.accepting&&!parent)fail()
if(parent){
if(!parent.active||parent.holder!==holder||!readOnly)fail()
@@ -135,6 +145,48 @@ export function createServerPartitionMaintenanceLease(deps:ServerPartitionMainte
const standalone=await holderFor({...options,serverIds:[]})
try{return await transact(standalone,true,fence,options,work)}finally{if(!standalone.released){standalone.active=false;standalone.released=true;standalone.connection.release(false)}}
}
const journalOwner:ServerPollJournalOwner=Object.freeze({
async transaction<T>(options:ProbeStorageOptions,authorize:()=>Promise<void>,work:(sql:ProbeStorageSql)=>Promise<T>):Promise<T>{
const holder=held.getStore()
if(!holder?.accepting||transactions.getStore()||journal.getStore())return fail()
const signals=[...new Set([options.signal,holder.options.signal].filter((signal):signal is AbortSignal=>!!signal))]
const limits={deadlineAt:Math.min(options.deadlineAt,holder.options.deadlineAt),signal:signals.length?AbortSignal.any(signals):undefined}
local(holder,limits)
let borrowed=false
const connect=createProbeStorageConnectionConnector(async()=>{
if(borrowed)fail();borrowed=true
const previous=holder.tail
let finish!:()=>void
const done=new Promise<void>(resolve=>{finish=resolve})
holder.tail=previous.then(()=>done)
let lent=false,closed=false
try{
await previous;local(holder,limits)
if(!holder.accepting)fail()
lent=true
return {
async query({text,values,name,query_timeout}){
local(holder,limits);if(closed)fail()
const result=await holder.connection.query(text,values??[],{signal:limits.signal,timeoutMs:query_timeout,statementName:name})
if(typeof result.command!=="string")return fail(true)
return {...result,command:result.command}
},
release(unsafe=false){if(closed)return;closed=true;try{if(unsafe)holder.destroy()}finally{finish()}},
on:(event,listener)=>holder.connection.on?.(event,listener),
removeListener:(event,listener)=>holder.connection.removeListener?.(event,listener),
}
}finally{if(!lent)finish()}
})
// Only the existing durable runner owns BEGIN/COMMIT/ROLLBACK. The
// connector checks the actual PostgreSQL COMMIT ACK before releasing this turn.
return journal.run(holder,()=>runPollStorageTransaction({connect,now},limits,authorize,work))
},
assertClosedForQuarantine(){
const holder=held.getStore()
if(!holder||holder.active||!holder.released||journal.getStore())fail()
},
})
journalOwners.add(journalOwner)
const withLease=async<T>(work:()=>Promise<T>,options:ServerPartitionLeaseOptions):Promise<T>=>{
if(held.getStore()||transactions.getStore())fail()
const frozen={...options,serverIds:[...options.serverIds]};checkOptions(frozen)
@@ -153,5 +205,5 @@ export function createServerPartitionMaintenanceLease(deps:ServerPartitionMainte
holder.active=false;holder.released=true;holder.connection.release(false);return result
}catch(error){holder.destroy();throw error}finally{clearTimeout(timer);frozen.signal?.removeEventListener("abort",abort);if(!holder.released)holder.destroy()}
}
return {withLease,transaction}
return {withLease,transaction,journalOwner}
}
@@ -35,7 +35,7 @@ async function compact(n: number, owner: string): Promise<Entry> {
storage_state: "compacted", compact_at: new Date("2026-10-29T02:00:00Z"), admission_archive_bytes: archive.ciphertext_bytes,
admission_activity_at: archive.original_activity_at, admission_expires_at: archive.original_expires_at, asserted_ciphertext_bytes: archive.ciphertext_bytes, has_children: false } }
}
function fixture(entries: Entry[], retiredCharge?: Parameters<typeof assertServerPollArchiveAccountingForTests>[2]) {
function fixture(entries: Entry[], retiredFactory?: Parameters<typeof assertServerPollArchiveAccountingForTests>[2]) {
const totals = new Map<string, { heavy: number; compact: number; archiveBytes: number; bytes: number; rows: number }>()
for (const entry of entries) {
for (const scope of ["global", "owner:" + entry.row.admission_owner_id]) {
@@ -74,7 +74,7 @@ function fixture(entries: Entry[], retiredCharge?: Parameters<typeof assertServe
} }
return { entries, usage, state, queries, batches, usageBatches, counts: () => ({ factories, charges }), run: () => assertServerPollArchiveAccountingForTests(sql, async () => {
factories++; return async row => { charges++; return codec.verifyCharge(row) }
}, retiredCharge) }
}, retiredFactory) }
}
const refused = (work: Promise<unknown>) => assert.rejects(work, PollStorageRefused)
@@ -91,16 +91,23 @@ test("mixed V1/V2 allocations authenticate actual stored ciphertext and never fa
storage_state: "compacted", admission_bytes: 32768, charged_rows: 2 })
second.archive = { ...metadata, ciphertext, archive_proof: archiveProof }
Object.assign(second.row, { version: 2, archive_proof: archiveProof })
const f = fixture([first, second], v2.verifyCharge)
let retiredFactories = 0, retiredCharges = 0
const f = fixture([first, second], async () => {
retiredFactories++
return async row => { retiredCharges++; return v2.verifyCharge(row) }
})
await f.run()
assert.deepEqual(f.counts(), { factories: 1, charges: 1 })
assert.deepEqual({ retiredFactories, retiredCharges }, { retiredFactories: 1, retiredCharges: 1 })
second.archive.ciphertext = "A" + ciphertext.slice(1)
await refused(f.run())
assert.deepEqual(f.counts(), { factories: 2, charges: 2 })
assert.deepEqual({ retiredFactories, retiredCharges }, { retiredFactories: 2, retiredCharges: 2 })
second.archive.ciphertext = ciphertext
second.row.version = 3
await refused(f.run())
assert.deepEqual(f.counts(), { factories: 3, charges: 3 })
assert.deepEqual({ retiredFactories, retiredCharges }, { retiredFactories: 2, retiredCharges: 2 })
})
test("empty/global0 and full128 ledgers use complete exact global/owner counters without archive crypto", async () => {
@@ -112,10 +119,27 @@ test("empty/global0 and full128 ledgers use complete exact global/owner counters
test("compact336 plus full reservations and multiowner usage paginate in128-row bounded metadata batches", async () => {
const entries = await Promise.all(Array.from({ length: 336 }, (_, n) => compact(n + 1, "compact-owner-" + n)))
const { createServerPollRetiredArchiveForTests } = await import("./server-poll-retired-archive.js")
const unexpected = async () => { throw new Error("Metadata accounting must not open or write archives") }
const v2 = createServerPollRetiredArchiveForTests({ proof, decrypt: unexpected, encrypt: unexpected, openV1: unexpected, verifyHistory: unexpected })
for (const index of [127, 128, 335]) {
const entry = entries[index], { archive_proof: _v1Proof, ciphertext, ...previous } = entry.archive!
const metadata = { ...previous, version: 2 as const }
const archiveProof = await proof("server-poll-retired-archive-record-v2", { ...metadata, identity_kind: "mcp",
storage_state: "compacted", admission_bytes: 32768, charged_rows: 2 })
entry.archive = { ...metadata, ciphertext, archive_proof: archiveProof }
Object.assign(entry.row, { version: 2, archive_proof: archiveProof })
}
entries.push(...Array.from({ length: 8 }, (_, n) => full(1000 + n, "full-owner")))
const f = fixture(entries); await f.run()
let retiredFactories = 0, retiredCharges = 0
const f = fixture(entries, async () => {
retiredFactories++
return async row => { retiredCharges++; return v2.verifyCharge(row) }
})
await f.run()
assert.deepEqual(f.batches, [128, 128, 88]); assert.deepEqual(f.usageBatches, [128, 128, 82])
assert.deepEqual(f.counts(), { factories: 1, charges: 336 })
assert.deepEqual(f.counts(), { factories: 1, charges: 333 })
assert.deepEqual({ retiredFactories, retiredCharges }, { retiredFactories: 1, retiredCharges: 3 })
})
test("forged full/compacted flags, lowering counters, missing global/owner usage and orphan archive cannot create credit", async () => {
@@ -3,7 +3,7 @@
import type { ProbeStorageSql } from "./server-probe-journal-maintenance.js"
import { SERVER_POLL_STORAGE_POLICY as policy, PollStorageRefused } from "./server-poll-storage-model.js"
import { createServerPollArchiveChargeVerifier, type verifyServerPollArchiveCharge } from "./server-poll-terminal-archive.js"
import type { verifyServerPollRetiredArchiveCharge } from "./server-poll-retired-archive.js"
import type { createServerPollRetiredArchiveChargeVerifier } from "./server-poll-retired-archive.js"
const BATCH = 128
const MAX_ENTRIES = Math.floor(policy.globalBytes / policy.admissionBytes)
@@ -16,8 +16,8 @@ function integer(value: unknown): number {
}
interface Total { heavy: number; compact: number; archiveBytes: number; rows: number; bytes: number }
const zero = (): Total => ({ heavy: 0, compact: 0, archiveBytes: 0, rows: 0, bytes: 0 })
const verifyRetiredCharge: typeof verifyServerPollRetiredArchiveCharge = async row =>
(await import("./server-poll-retired-archive.js")).verifyServerPollRetiredArchiveCharge(row)
const createRetiredChargeVerifier: typeof createServerPollRetiredArchiveChargeVerifier = async () =>
(await import("./server-poll-retired-archive.js")).createServerPollRetiredArchiveChargeVerifier()
const QUERY = `SELECT a.receipt_id AS admission_receipt_id,a.identity_kind,a.execution_id AS admission_execution_id,
a.connection_id AS admission_connection_id,a.owner_id AS admission_owner_id,a.binding_proof AS admission_binding_proof,
a.context_proof AS admission_context_proof,a.storage_state,a.compact_at,a.archive_bytes AS admission_archive_bytes,
@@ -38,10 +38,11 @@ const QUERY = `SELECT a.receipt_id AS admission_receipt_id,a.identity_kind,a.exe
WHERE ($1::uuid IS NULL OR a.receipt_id>$1::uuid) ORDER BY a.receipt_id LIMIT 128`
async function verify(sql: ProbeStorageSql, factory: () => Promise<typeof verifyServerPollArchiveCharge>,
retiredCharge: typeof verifyServerPollRetiredArchiveCharge) {
retiredFactory: typeof createServerPollRetiredArchiveChargeVerifier) {
const global = zero(), owners = new Map<string, Total>()
const seenReceipts = new Set<string>()
let cursor: string | null = null, count = 0, charge: typeof verifyServerPollArchiveCharge | undefined
let retiredCharge: Awaited<ReturnType<typeof retiredFactory>> | undefined
// All queries are fixed. Only metadata, hashes and actual lengths cross the
// driver boundary; even a full global history never becomes a JS blob.
for (;;) {
@@ -70,6 +71,7 @@ async function verify(sql: ProbeStorageSql, factory: () => Promise<typeof verify
charge ??= await factory()
verified = await charge(row)
} else if (row.version === 2) {
retiredCharge ??= await retiredFactory()
verified = await retiredCharge(row)
} else return fail()
addition = { heavy: 0, compact: 1, archiveBytes: verified.metadata.ciphertext_bytes, rows: verified.rows, bytes: verified.bytes }
@@ -111,9 +113,9 @@ async function verify(sql: ProbeStorageSql, factory: () => Promise<typeof verify
/** Caller must already own server-poll-storage-v1. Any malformed/missing MAC or
* counter refuses admission before an INSERT, credential read or native I/O. */
export async function assertServerPollArchiveAccounting(sql: ProbeStorageSql): Promise<void> {
try { await verify(sql, createServerPollArchiveChargeVerifier, verifyRetiredCharge) } catch { return fail() }
try { await verify(sql, createServerPollArchiveChargeVerifier, createRetiredChargeVerifier) } catch { return fail() }
}
export async function assertServerPollArchiveAccountingForTests(sql: ProbeStorageSql, factory: () => Promise<typeof verifyServerPollArchiveCharge>,
retiredCharge = verifyRetiredCharge): Promise<void> {
try { await verify(sql, factory, retiredCharge) } catch { return fail() }
retiredFactory = createRetiredChargeVerifier): Promise<void> {
try { await verify(sql, factory, retiredFactory) } catch { return fail() }
}
@@ -19,6 +19,11 @@ const {createStorageDurableServerPollSession:createDurableServerPollSession}=awa
const {SERVER_PARTITION_WRITER_LOCK}=await import("./server-partition-maintenance-lease.js")
const {withPartitionWrite,PartitionWriteError}=await import("./partition-write-transaction.js")
const {ensurePartitionFor,PARTITION_SPECS}=await import("../db/partitions.js")
const {compactCompletedServerPollStorage}=await import("./server-poll-terminal-compaction.js")
const {collectExpiredServerPollStorage}=await import("./server-poll-storage-retention.js")
const {tickMcpWorkers}=await import("../modules/mcp/workers.js")
const {ServerPollJournalOutcomeUnknown}=await import("./server-poll-journal.js")
const {Client}=await import("pg")
const security=await import("../modules/mcp/security.js")
let serverId:number|undefined,requests=0,releasedNative=false,writerPid=0
let releaseNative!:()=>void,firstNative!:()=>void
@@ -40,6 +45,20 @@ try{
assert.equal((await pool.query("SELECT count(*)::int AS n FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname='public' AND c.relname=ANY($1) AND c.relkind='r'",[["server_poll_executions","server_poll_intents","server_poll_acks","server_poll_receipts","server_poll_quarantines"]])).rows[0].n,5)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid WHERE t.tgname='server_partition_writer_guard' AND t.tgenabled='A' AND c.relkind='p'")).rows[0].n,PARTITION_SPECS.length)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM servers")).rows[0].n,0,"Fresh isolated database required; never remove foreign fixtures")
await pool.query(`CREATE TABLE owned_poll_journal_pid(relation text,pid int,xid text,exclusive_writer boolean);
CREATE FUNCTION owned_poll_journal_observe() RETURNS trigger LANGUAGE plpgsql AS $$BEGIN
INSERT INTO public.owned_poll_journal_pid SELECT TG_TABLE_NAME,pg_backend_pid(),pg_current_xact_id()::text,
EXISTS(SELECT 1 FROM pg_catalog.pg_locks WHERE pid=pg_backend_pid() AND locktype='advisory' AND classid=0
AND objid=8723110 AND objsubid=1 AND mode='ExclusiveLock' AND granted);
RETURN NULL;
END$$;`)
for(const name of ["server_poll_storage_admissions","server_poll_storage_usage","server_poll_executions","server_poll_intents","server_poll_acks","server_poll_receipts","server_poll_quarantines"]){
await pool.query(`CREATE TRIGGER candidate_application_statement_guard BEFORE INSERT OR UPDATE OR DELETE ON public.${name}
FOR EACH STATEMENT EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public.${name} ENABLE ALWAYS TRIGGER candidate_application_statement_guard;
CREATE TRIGGER owned_poll_journal_observe AFTER INSERT OR UPDATE OR DELETE ON public.${name}
FOR EACH STATEMENT EXECUTE FUNCTION public.owned_poll_journal_observe();`)
}
await new Promise<void>((resolve,reject)=>{listener.once("error",reject);listener.listen(0,"127.0.0.1",()=>{listener.off("error",reject);resolve()})})
const port=(listener.address()as{port:number}).port
serverId=Number((await pool.query("INSERT INTO servers(name,host,port,enabled,username,password,use_ssl,verify_ssl) VALUES('poll-owned-before','127.0.0.1',$1,true,'SYNTHETIC','SYNTHETIC',false,true) RETURNING id",[port])).rows[0].id)
@@ -78,6 +97,22 @@ try{
assert(plan.maintenance.some(effect=>effect.kind==="partition.drop"&&effect.name===expired))
const pending=session.execute(plan);void pending.catch(()=>{})
await Promise.race([started,pending.then(()=>assert.fail("Native request must occur before completion"))])
const {rows:[holder]}=await pool.query(`SELECT pid FROM pg_catalog.pg_locks WHERE locktype='advisory' AND classid=0
AND objid=$1::oid AND objsubid=1 AND mode='ExclusiveLock' AND granted`,[SERVER_PARTITION_WRITER_LOCK])
assert(Number.isInteger(holder?.pid))
const prefix=(await pool.query("SELECT relation,pid,xid,exclusive_writer FROM owned_poll_journal_pid")).rows
assert(prefix.some(row=>row.relation==="server_poll_executions"));assert(prefix.some(row=>row.relation==="server_poll_intents"))
assert(prefix.every(row=>row.pid===holder.pid&&row.exclusive_writer===true))
const anchor=prefix.find(row=>row.relation==="server_poll_executions")!
assert(prefix.some(row=>row.relation==="server_poll_storage_admissions"&&row.xid!==anchor.xid),"Reservation and consume/anchor are independently committed")
assert(prefix.some(row=>row.relation==="server_poll_storage_admissions"&&row.xid===anchor.xid),"Consume and anchor share one transaction")
const workerErrors:unknown[]=[]
await tickMcpWorkers().catch(error=>workerErrors.push(error))
assert.deepEqual(workerErrors,[],"An actual worker tick must defer maintenance without an error while native POLL owns the exclusive lock")
assert.equal((await compactCompletedServerPollStorage()).compacted,0)
assert.equal((await collectExpiredServerPollStorage()).collected,0)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM pg_locks WHERE locktype='advisory' AND pid=$1 AND objid=$2::oid AND mode='ExclusiveLock' AND granted",[holder.pid,SERVER_PARTITION_WRITER_LOCK])).rows[0].n,1)
assert.equal((await pool.query("SELECT pg_try_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0)) AS free")).rows[0].free,true)
// Actual ordinary writer arrives while FULL poll holds the exclusive lease.
// Inspect lock evidence before releasing HTTP; no target relation is touched.
const controller=new AbortController()
@@ -88,6 +123,8 @@ try{
assert.equal((await pool.query("SELECT count(*)::int AS n FROM pg_locks l JOIN pg_class c ON c.oid=l.relation WHERE l.pid=$1 AND l.locktype='relation' AND (c.relname='traffic_samples' OR c.relname LIKE 'traffic_samples_w_%')",[writerPid])).rows[0].n,0)
}finally{controller.abort();await assert.rejects(blocked,(error:unknown)=>error instanceof PartitionWriteError&&!error.outcomeUnknown);releaseNative()}
const receipt=await pending
assert.equal((await compactCompletedServerPollStorage()).compacted,0)
assert.equal((await collectExpiredServerPollStorage()).collected,0)
assert.equal(receipt.complete,true);assert.equal(receipt.maintenanceComplete,true);assert.equal(receipt.nativeReads,4)
assert.equal(receipt.completedPublications,plan.maintenance.length+4)
assert.deepEqual([...paths].sort(),["/rest/interface","/rest/ip/address","/rest/system/identity","/rest/system/resource"].sort())
@@ -98,6 +135,11 @@ try{
assert.equal((await pool.query("SELECT count(*)::int AS n FROM server_poll_intents WHERE receipt_id=$1",[receiptId])).rows[0].n,receipt.completedPublications+4)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM server_poll_acks WHERE receipt_id=$1",[receiptId])).rows[0].n,receipt.completedPublications+4)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM server_poll_receipts WHERE receipt_id=$1",[receiptId])).rows[0].n,1)
const fullJournal=(await pool.query("SELECT relation,pid,xid,exclusive_writer FROM owned_poll_journal_pid")).rows
assert(fullJournal.every(row=>row.pid===holder.pid&&row.exclusive_writer===true))
assert(fullJournal.some(row=>row.relation==="server_poll_acks")&&fullJournal.some(row=>row.relation==="server_poll_receipts"))
assert(new Set(fullJournal.filter(row=>row.relation==="server_poll_intents").map(row=>row.xid)).size===receipt.completedPublications+4)
assert.deepEqual(await session.readExisting(plan),receipt)
// Measure a COMPLETE real journal, including ciphertext, proofs, timestamps,
// admission and every stage. This is sizing evidence, not an archive format
// or permission to delete/compact rows. Never print the captured payload.
@@ -118,6 +160,39 @@ try{
chargedBytes:Number(stored.admission.used_bytes),fullJsonBytes:fullBytes.length,gzipBytes:compressed.length,gzipRoundTripExact:true,
sampleFits128KiB:compressed.length<=131072,identityKind:stored.admission.identity_kind,compactionPerformed:false,
archiveAuthenticationAccepted:false,historicalVerifierAccepted:false,productionCapacityAccepted:false}))
const lostRequest={serverId,receiptId:randomUUID()},lostIdentity={...identity,executionId:randomUUID(),requestId:randomUUID()}
const originalAuthority=sessionInput.authorize
const lostSession=createDurableServerPollSession({request:lostRequest,identity:lostIdentity,authorize:async(request,ids,signal)=>{
assert.deepEqual(request,lostRequest)
return originalAuthority(sessionRequest,ids,signal)
}})
const lostPlan=await lostSession.prepare(),originalQuery=Client.prototype.query
type IdentifiedClient=InstanceType<typeof Client>&{processID?:number}
let reservedBackend:IdentifiedClient|undefined,acksLost=0
try{
Client.prototype.query=function(this:InstanceType<typeof Client>,...args:unknown[]){
const request=args[0] as {text?:string;values?:unknown[]}
if(request?.text?.startsWith("INSERT INTO server_poll_storage_admissions")&&request.values?.[0]===lostRequest.receiptId)reservedBackend=this
if(this===reservedBackend&&request?.text==="COMMIT"&&acksLost===0){
return Promise.resolve(Reflect.apply(originalQuery,this,args)).then((result:{command:string})=>{
assert.equal(result.command,"COMMIT");acksLost++;throw Error("owned real reservation COMMIT ACK lost")
})
}
return Reflect.apply(originalQuery,this,args)
} as typeof Client.prototype.query
await assert.rejects(lostSession.execute(lostPlan),error=>error instanceof ServerPollJournalOutcomeUnknown&&error.automaticReplay===false&&!error.requiresDurableQuarantine)
}finally{Client.prototype.query=originalQuery}
assert.equal(acksLost,1);assert.equal(requests,4)
const {rows:[lostState]}=await pool.query(`SELECT
(SELECT count(*)::int FROM server_poll_storage_admissions WHERE receipt_id=$1) AS charged,
(SELECT count(*)::int FROM server_poll_executions WHERE receipt_id=$1) AS anchored,
(SELECT count(*)::int FROM server_poll_quarantines WHERE receipt_id=$1) AS quarantined`,[lostRequest.receiptId])
assert.deepEqual(lostState,{charged:1,anchored:0,quarantined:1})
const {rows:[quarantine]}=await pool.query("SELECT pid,exclusive_writer FROM owned_poll_journal_pid WHERE relation='server_poll_quarantines'")
assert(Number.isInteger(reservedBackend?.processID))
assert.notEqual(quarantine.pid,reservedBackend?.processID);assert.equal(quarantine.exclusive_writer,false)
assert.equal(await lostSession.readExisting(lostPlan),undefined)
console.log("Guarded durable POLL journal acceptance:",JSON.stringify({sameHolder:true,independentCommits:true,concurrentMaintenanceNoEarlyChanges:true,workerErrors:workerErrors.length,readExisting:true,realLostCommitAckNoReplay:true,independentQuarantine:true}))
sessionRequest.serverId++
identity.ownerId="mutated-outer-input";identity.expiresAt=0
sessionInput.authorize=async()=>{throw Error("Outer caller replaced authority provider")}
@@ -125,7 +200,7 @@ try{
await assert.rejects(session.execute({...plan,bindingProof:"0".repeat(64)}));assert.equal(requests,4)
await withPartitionWrite(async owner=>{await owner.client.query("INSERT INTO traffic_samples(server_id,interface_name,sampled_at,rx_bytes) VALUES($1,'after-full-poll',now(),1)",[serverId])})
revoked=true;await assert.rejects(session.execute(plan));assert.equal(requests,4)
console.log("Full durable poll actual SQL/HTTP acceptance:",JSON.stringify({fourNativeGets:requests,maintenanceEffects:plan.maintenance.length,parents:PARTITION_SPECS.length,completedPublications:receipt.completedPublications,sameBackendLeaseAndTransactions:true,ordinaryWriterBlockedBeforeRelationSql:true,realJournal:true,repeatedApplyNoNewNative:true,outerInputMutationCannotReplaceCapturedIdentityOrAuthority:true,changedPlanBindingRefused:true,lateRevocationRefused:true,actualHttpAuthenticationMiddleware:false,productionRegistration:false,externalNetwork:false,authorityReads}))
console.log("Full durable poll actual SQL/HTTP acceptance:",JSON.stringify({fourNativeGets:requests,maintenanceEffects:plan.maintenance.length,parents:PARTITION_SPECS.length,completedPublications:receipt.completedPublications,sameBackendLeaseAndTransactions:true,guardedSameHolderJournal:true,separateDurableJournalCommits:true,independentQuarantineAfterRealLostCommitAck:true,ordinaryWriterBlockedBeforeRelationSql:true,realJournal:true,repeatedApplyNoNewNative:true,outerInputMutationCannotReplaceCapturedIdentityOrAuthority:true,changedPlanBindingRefused:true,lateRevocationRefused:true,actualHttpAuthenticationMiddleware:false,productionRegistration:false,externalNetwork:false,authorityReads}))
}finally{
if(!releasedNative)releaseNative()
listener.closeAllConnections();if(listener.listening)await new Promise<void>(resolve=>listener.close(()=>resolve()))
@@ -5,7 +5,7 @@ import { createServerPartitionMaintenanceLease } from "./server-partition-mainte
import { createSqlServerPollJournal, parseServerPollJournalIdentity, ServerPollJournalConflict, type ServerPollJournalIdentity } from "./server-poll-journal.js"
import { pollProblem } from "./server-poll-execution-model.js"
import type { ServerPollDependencies, ServerPollPlan, ServerPollRequest } from "./server-poll-execution.js"
import { createServerPollStorageJournal } from "./server-poll-storage-journal.js"
import { createHolderBoundServerPollStorageJournal, createServerPollStorageJournal } from "./server-poll-storage-journal.js"
import { createServerPollBoundaryCoordinator } from "./server-poll-boundary.js"
export interface DurableServerPollSessionInput {
@@ -64,7 +64,7 @@ function durableSession(input:DurableServerPollSessionInput,storageBound:boolean
// accidental unbound execution impossible; they never invent an ACK/claim.
const unavailable=async():Promise<never>=>{throw new ServerPollJournalConflict()}
const prepare=async(signal?:AbortSignal)=>build({claim:unavailable,intent:unavailable,acknowledge:unavailable,complete:unavailable}).prepare(request,signal)
const journalFor=async(raw:ServerPollPlan,signal?:AbortSignal)=>{
const journalFor=async(raw:ServerPollPlan,mode:"execute"|"read",signal?:AbortSignal)=>{
const plan=structuredClone(raw)
if(plan.request.serverId!==request.serverId||plan.request.receiptId!==request.receiptId)pollProblem("STATE_CONFLICT","План не принадлежит durable poll session")
const deadlineAt=Math.min(identity.expiresAt,Date.now()+SERVER_POLL_WALL_BUDGET_MS)
@@ -74,14 +74,17 @@ function durableSession(input:DurableServerPollSessionInput,storageBound:boolean
if(grant.authorityProof!==context.plan.authorityProof)pollProblem("STATE_CONFLICT","Полномочия durable poll изменились")
}
const context={identity,plan}
const storage=storageBound?await createServerPollStorageJournal(context,{assertAuthority,deadlineAt,signal}):undefined
const storageOptions={assertAuthority,deadlineAt,signal}
const storage=storageBound?await (mode==="execute"
?createHolderBoundServerPollStorageJournal(context,storageOptions,lease.journalOwner)
:createServerPollStorageJournal(context,storageOptions)):undefined
const journal=createSqlServerPollJournal(context,{
executionProof:proof,deadlineAt,assertAuthority,storage,
})
return {journal,plan}
}
const execute=async(raw:ServerPollPlan,signal?:AbortSignal)=>{const {journal,plan}=await journalFor(raw,signal);return build(journal).execute(plan,signal)}
const readExisting=async(raw:ServerPollPlan,signal?:AbortSignal)=>{const {journal}=await journalFor(raw,signal);return journal.readExisting()}
const execute=async(raw:ServerPollPlan,signal?:AbortSignal)=>{const {journal,plan}=await journalFor(raw,"execute",signal);return build(journal).execute(plan,signal)}
const readExisting=async(raw:ServerPollPlan,signal?:AbortSignal)=>{const {journal}=await journalFor(raw,"read",signal);return journal.readExisting()}
return {prepare,execute,readExisting}
}
/** Historical private assembly for the enrolled 0038 fixture; public adapters
@@ -3,6 +3,7 @@
* production crypto and all database guards enabled. */
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { setTimeout as delay } from "node:timers/promises"
import { assertOwnedBandwidthSql } from "./bandwidth-admission-process.sql-helper.js"
assertOwnedBandwidthSql()
@@ -14,16 +15,55 @@ const { mcpConnectionCreateSchema } = await import("@mmapp/contracts/mcp")
const { invokeMcpTool } = await import("../modules/mcp/runtime.js")
const { encryptSecret,privateSecretFingerprint,fingerprint,redact } = await import("../modules/mcp/security.js")
const { encodeServerPollJournalAnchor } = await import("./server-poll-journal-anchor.js")
const { compactCompletedServerPollStorage,SERVER_POLL_ARCHIVE_READ_SQL } = await import("./server-poll-terminal-compaction.js")
const { compactCompletedServerPollStorage,compactCompletedServerPollStorageForTests,SERVER_POLL_ARCHIVE_READ_SQL } = await import("./server-poll-terminal-compaction.js")
const { openServerPollTerminalArchive } = await import("./server-poll-terminal-archive.js")
const { retireCompletedServerPollOperation } = await import("./server-poll-operation-retirement.js")
const { collectExpiredServerPollStorage } = await import("./server-poll-storage-retention.js")
const { PollStorageUnknown } = await import("./server-poll-storage.js")
const { PARTITION_WRITER_ADVISORY_KEY } = await import("./partition-write-transaction.js")
const { collectExpiredServerPollStorage,collectExpiredServerPollStorageForTests } = await import("./server-poll-storage-retention.js")
const { connectProbeStorage } = await import("./server-probe-storage-connection.js")
const { PollStorageRefused,PollStorageUnknown } = await import("./server-poll-storage.js")
const { tickMcpWorkers } = await import("../modules/mcp/workers.js")
const { Client } = await import("pg")
const proof=(domain:string,value:unknown)=>privateSecretFingerprint(fingerprint(value),domain)
const encrypt=(value:unknown)=>encryptSecret(JSON.stringify(value))
function loseMaintenanceCommit(at:number,afterFirst?:()=>Promise<void>){
const transactions:{commands:string[];release?:boolean}[]=[]
let commits=0
return {transactions,deps:{now:Date.now,connect:async(options:Parameters<typeof connectProbeStorage>[0])=>{
const physical=await connectProbeStorage(options),trace:{commands:string[];release?:boolean}={commands:[]}
transactions.push(trace)
return {async query(...args:Parameters<typeof physical.query>){
const [text]=args
trace.commands.push(text)
const result=await physical.query(...args)
if(text==="COMMIT"){
commits++
if(commits===1)await afterFirst?.()
if(commits===at)throw Error("owned POLL maintenance COMMIT ACK lost")
}
return result
},release(unsafe?:boolean){trace.release=unsafe;physical.release(unsafe)}}
}}}
}
function assertMaintenanceOrder(fault:ReturnType<typeof loseMaintenanceCommit>,phaseCount:number){
assert.deepEqual(fault.transactions.map(tx=>tx.release),[true],"One physical owner, destroyed once on unknown COMMIT")
const [{commands}]=fault.transactions
assert.equal(commands.includes("ROLLBACK"),false)
assert.equal(commands.some(text=>text.includes("pg_advisory_unlock_shared")),false)
assert.equal(commands.filter(text=>text==="BEGIN").length,phaseCount)
assert.equal(commands.filter(text=>text==="COMMIT").length,phaseCount)
let previousCommit=-1
for(let phase=0;phase<phaseCount;phase++){
const commit=commands.indexOf("COMMIT",previousCommit+1),sql=commands.slice(previousCommit+1,commit)
const writer=sql.indexOf("SELECT pg_advisory_xact_lock_shared($1::bigint)")
const restored=sql.indexOf("SELECT pg_catalog.set_config('lock_timeout',$1,true)")
const storage=sql.indexOf("SELECT pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0))")
assert(writer>0&&writer<restored&&restored<storage)
previousCommit=commit
}
}
async function historical(days:number,serverId:number) {
const base=Date.now()-days*86400000, stamp=(offset:number)=>new Date(base+offset).toISOString()
const ownerId=randomUUID(),executionId=randomUUID(),receiptId=randomUUID(),planId=randomUUID()
@@ -100,11 +140,68 @@ try{
await pool.query("UPDATE mcp_settings SET enabled=true,requests_retention_days=365 WHERE id=1")
const {rows:[server]}=await pool.query("INSERT INTO servers(name,host,enabled) VALUES('owned historical poll','127.0.0.1',false) RETURNING id")
const retained=await historical(181,Number(server.id)), expired=await historical(366,Number(server.id))
for(const table of ["server_poll_storage_admissions","server_poll_storage_usage","server_poll_archives","server_poll_retirement_claims"]){
await pool.query(`CREATE TRIGGER owned_poll_worker_fence BEFORE INSERT OR UPDATE OR DELETE ON public.${table}
FOR EACH STATEMENT EXECUTE FUNCTION public.server_partition_writer_guard();
ALTER TABLE public.${table} ENABLE ALWAYS TRIGGER owned_poll_worker_fence`)
}
for(const fixture of [expired,retained]){
assert.equal((await compactCompletedServerPollStorage()).compacted,1)
if(fixture===expired){
const rival=await pool.connect()
let waiting:Promise<unknown>|undefined
try{
const {rows:[owner]}=await rival.query("SELECT pg_backend_pid() AS pid")
const fault=loseMaintenanceCommit(2,async()=>{
waiting=rival.query("SELECT pg_advisory_lock($1::bigint)",[PARTITION_WRITER_ADVISORY_KEY]);void waiting.catch(()=>{})
let queued=false
for(const end=Date.now()+2000;!queued&&Date.now()<end;){
const {rows:[lock]}=await pool.query("SELECT EXISTS(SELECT 1 FROM pg_locks WHERE locktype='advisory' AND pid=$1 AND objid=$2::oid AND NOT granted) AS queued",[owner.pid,PARTITION_WRITER_ADVISORY_KEY])
queued=lock.queued;if(!queued)await delay(10)
}
assert.equal(queued,true,"Exclusive POLL must wait across the acknowledged compaction claim")
})
await assert.rejects(compactCompletedServerPollStorageForTests(fault.deps),error=>error instanceof PollStorageUnknown&&!error.automaticReplay)
assertMaintenanceOrder(fault,2)
await waiting
assert.equal((await rival.query("SELECT pg_advisory_unlock($1::bigint) AS released",[PARTITION_WRITER_ADVISORY_KEY])).rows[0].released,true)
}finally{rival.release(true)}
}else assert.equal((await compactCompletedServerPollStorage()).compacted,1)
const {rows:[v1]}=await pool.query(SERVER_POLL_ARCHIVE_READ_SQL,[fixture.receiptId])
await openServerPollTerminalArchive(v1,Date.now())
const before=(await pool.query("SELECT gc_not_before,authority_expires_at,retention_days,original_activity_at FROM server_poll_storage_admissions WHERE receipt_id=$1",[fixture.receiptId])).rows[0]
if(fixture===expired){
const exclusive=await pool.connect()
let blocked:ReturnType<typeof retireCompletedServerPollOperation>|undefined
try{
await exclusive.query("SELECT pg_advisory_lock($1::bigint)",[PARTITION_WRITER_ADVISORY_KEY])
await exclusive.query("BEGIN")
blocked=retireCompletedServerPollOperation({receiptId:fixture.receiptId,oldArchiveProof:v1.archive_proof})
void blocked.catch(()=>{})
let waiting=false
for(const deadline=Date.now()+3000;!waiting&&Date.now()<deadline;){
waiting=(await exclusive.query(`SELECT EXISTS(SELECT 1 FROM pg_catalog.pg_locks
WHERE locktype='advisory' AND database=(SELECT oid FROM pg_catalog.pg_database WHERE datname=current_database())
AND classid=0 AND objid=$1::oid AND objsubid=1 AND mode='ShareLock' AND NOT granted) AS waiting`,[PARTITION_WRITER_ADVISORY_KEY])).rows[0].waiting
if(!waiting)await delay(10)
}
assert.equal(waiting,true,"POLL retirement must wait on the common writer fence")
for(const key of ["server-poll-storage-v1","mcp-operation:"+fixture.executionId,"server-poll-execution-v1:mcp:"+fixture.executionId,"server-poll-receipt-v1:"+fixture.receiptId]){
assert.equal((await exclusive.query("SELECT pg_try_advisory_xact_lock(hashtextextended($1,0)) AS locked",[key])).rows[0].locked,true,key)
}
assert.equal((await exclusive.query("SELECT id FROM public.mcp_connections WHERE id=$1 FOR UPDATE NOWAIT",[fixture.connectionId])).rows.length,1)
await assert.rejects(blocked,PollStorageRefused)
assert.equal((await exclusive.query("SELECT 1 FROM server_poll_retirement_claims WHERE receipt_id=$1",[fixture.receiptId])).rows.length,0)
assert.equal((await exclusive.query("SELECT 1 FROM mcp_operation_replay_tombstones WHERE operation_id=$1",[fixture.executionId])).rows.length,0)
assert.equal((await exclusive.query(SERVER_POLL_ARCHIVE_READ_SQL,[fixture.receiptId])).rows[0].archive_proof,v1.archive_proof)
}finally{
try{await blocked?.catch(()=>{})}finally{
try{
await exclusive.query("ROLLBACK")
assert.equal((await exclusive.query("SELECT pg_advisory_unlock($1::bigint) AS unlocked",[PARTITION_WRITER_ADVISORY_KEY])).rows[0].unlocked,true)
}finally{exclusive.release(true)}
}
}
}
await assert.rejects(retireCompletedServerPollOperation({receiptId:fixture.receiptId,oldArchiveProof:"0".repeat(64)}))
const hold=randomUUID()
await pool.query("INSERT INTO mcp_drift(id,operation_id,expected,observed) VALUES($1,$2,'{}','{}')",[hold,fixture.executionId])
@@ -130,8 +227,15 @@ try{
await assert.rejects(invokeMcpTool(foreign,"mm_global_operation_reconcile",{operationId:fixture.executionId}),
(error:unknown)=>(error as {code?:string}).code==="NOT_FOUND")
}
assert.equal((await collectExpiredServerPollStorage()).collected,1)
const expiredV1=await historical(367,Number(server.id))
assert.equal((await compactCompletedServerPollStorage()).compacted,1)
assert.equal((await pool.query(SERVER_POLL_ARCHIVE_READ_SQL,[expiredV1.receiptId])).rows[0].version,1)
const gcFault=loseMaintenanceCommit(1)
await assert.rejects(collectExpiredServerPollStorageForTests(gcFault.deps),error=>error instanceof PollStorageUnknown&&!error.automaticReplay)
assertMaintenanceOrder(gcFault,1)
assert.equal((await collectExpiredServerPollStorage()).collected,0)
assert.equal((await pool.query("SELECT 1 FROM mcp_operation_replay_tombstones WHERE operation_id=$1",[expired.executionId])).rows.length,1)
assert.equal((await pool.query("SELECT 1 FROM mcp_operations WHERE id=$1",[expiredV1.executionId])).rows.length,1)
const {rows}=await pool.query("SELECT r.receipt_id,r.ciphertext_bytes,u.reserved_bytes,u.reserved_rows FROM server_poll_archives r CROSS JOIN server_poll_storage_usage u WHERE u.scope='global'")
assert.equal(rows.length,1);assert.equal(rows[0].receipt_id,retained.receiptId)
assert.equal(Number(rows[0].reserved_bytes),32768+rows[0].ciphertext_bytes);assert.equal(Number(rows[0].reserved_rows),2)
@@ -146,10 +250,15 @@ try{
await pool.query("INSERT INTO mcp_drift(id,operation_id,expected,observed) VALUES($1,$2,'{}','{}')",[randomUUID(),held.executionId])
const {rows:[spentV1]}=await pool.query(SERVER_POLL_ARCHIVE_READ_SQL,[spent.receiptId])
const originalQuery=Client.prototype.query
const admissionOrder:string[]=[]
let commits=0
try{
Client.prototype.query=function(this:InstanceType<typeof Client>,...args:unknown[]){
if((args[0] as {text?:string})?.text==="COMMIT" && ++commits===2) {
const text=(args[0] as {text?:string})?.text
if(text==="SELECT pg_advisory_xact_lock_shared($1::bigint)")admissionOrder.push("common")
if(text==="SELECT pg_catalog.set_config('lock_timeout',$1,true)")admissionOrder.push("restore timeout")
if(text==="SELECT pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0))")admissionOrder.push("storage")
if(text==="COMMIT" && ++commits===2) {
// No COMMIT ACK can reach the owner. Closing the real checked-out
// backend rolls back uncommitted work, leaving the earlier claim spent.
return Promise.reject(Error("owned poll retirement COMMIT transport loss"))
@@ -160,6 +269,7 @@ try{
(error:unknown)=>error instanceof PollStorageUnknown && error.automaticReplay===false)
}finally{Client.prototype.query=originalQuery}
assert.equal(commits,2)
assert.deepEqual(admissionOrder,["common","restore timeout","storage","common","restore timeout","storage"])
assert.equal((await pool.query(SERVER_POLL_ARCHIVE_READ_SQL,[spent.receiptId])).rows[0].version,1)
assert.equal((await pool.query("SELECT 1 FROM server_poll_retirement_claims WHERE receipt_id=$1",[spent.receiptId])).rows.length,1)
const workerProjection=async()=>(await pool.query(`SELECT a.receipt_id,r.version,r.archive_proof,a.archive_bytes,
@@ -179,5 +289,14 @@ try{
await tickMcpWorkers()
assert.deepEqual(await workerProjection(),afterWorker)
assert.equal((await pool.query("SELECT server_poll_storage_accounting_valid() AS valid")).rows[0].valid,true)
console.log("Owned PostgreSQL: authenticated V1/V2 transition, retained history, once-only refund, and actual worker skips held/spent rows")
const lostClaim=await historical(187,Number(server.id)),claimFault=loseMaintenanceCommit(1)
await assert.rejects(compactCompletedServerPollStorageForTests(claimFault.deps),error=>error instanceof PollStorageUnknown&&!error.automaticReplay)
assertMaintenanceOrder(claimFault,1)
const {rows:[claimState]}=await pool.query(`SELECT storage_state,compaction_claim IS NOT NULL AND compaction_claim_xid IS NOT NULL AS spent,
NOT EXISTS(SELECT 1 FROM server_poll_archives WHERE receipt_id=$1) AS unarchived,
server_poll_storage_accounting_valid() AS accounting FROM server_poll_storage_admissions WHERE receipt_id=$1`,[lostClaim.receiptId])
assert.deepEqual(claimState,{storage_state:"full",spent:true,unarchived:true,accounting:true})
assert.deepEqual(await compactCompletedServerPollStorage(),{scanned:0,compacted:0,skipped:0,automaticReplay:false})
console.log("Owned PostgreSQL: guarded compaction and mixed V1/V2 GC, physical claim/publish/refund ACK losses, retained history, and actual worker skips held/spent rows")
}finally{await closePool()}
@@ -10,6 +10,7 @@ import { PollStorageRefused, runPollStorageTransaction } from "./server-poll-sto
import type { ProbeStorageSql } from "./server-probe-journal-maintenance.js"
import { connectProbeStorage } from "./server-probe-storage-connection.js"
import { sharedDatabaseGate } from "./shared-database-gate.js"
import { admitPartitionWriter } from "./partition-write-transaction.js"
const request = z.object({ receiptId: z.string().uuid(), oldArchiveProof: z.string().regex(/^[a-f0-9]{64}$/) }).strict()
const requireThat: (value: unknown) => asserts value = value => {
@@ -50,8 +51,16 @@ const CANDIDATES = `SELECT a.receipt_id,r.archive_proof,o.finished_at::text AS f
ORDER BY o.finished_at,a.receipt_id LIMIT 8`
const deps = { connect:connectProbeStorage,now:Date.now }
const transaction = <T>(deadlineAt:number, work:(sql:ProbeStorageSql)=>Promise<T>) =>
runPollStorageTransaction(deps,{deadlineAt},async()=>{},work)
async function admitRetirementWriter(sql:ProbeStorageSql,deadlineAt:number) {
const settings=(await sql.query("SELECT pg_catalog.current_setting('lock_timeout') AS lock_timeout")).rows
requireThat(settings.length===1 && typeof settings[0].lock_timeout==="string")
// The common fence precedes the POLL storage lock. Its bounded LOCAL
// lock_timeout must not change how later POLL locks observe contention.
await admitPartitionWriter(sql,{deadlineAt:Math.min(deadlineAt,Date.now()+5000)})
await sql.query("SELECT pg_catalog.set_config('lock_timeout',$1,true)",[settings[0].lock_timeout])
}
const transaction = <T>(deadlineAt:number,mode:"scan"|"write",work:(sql:ProbeStorageSql)=>Promise<T>) =>
runPollStorageTransaction(deps,{deadlineAt},async()=>{},work,mode==="write"?sql=>admitRetirementWriter(sql,deadlineAt):undefined)
type Target = z.infer<typeof request>
async function prepare(sql: ProbeStorageSql,{receiptId,oldArchiveProof}:Target) {
@@ -84,7 +93,7 @@ async function prepare(sql: ProbeStorageSql,{receiptId,oldArchiveProof}:Target)
async function retire(target:Target,deadlineAt:number) {
const {receiptId,oldArchiveProof}=target
const token=randomUUID()
await transaction(deadlineAt,async sql=>{
await transaction(deadlineAt,"write",async sql=>{
// Check the live hold predicates, all locks and both archive proofs before
// spending the irreversible one-shot claim.
await prepare(sql,target)
@@ -94,7 +103,7 @@ async function retire(target:Target,deadlineAt:number) {
})
// No destructive transaction after an unacknowledged claim. A claim is
// never cleared, even when a later checkout/hold/COMMIT is inconclusive.
return transaction(deadlineAt,async sql=>{
return transaction(deadlineAt,"write",async sql=>{
const claimed=await sql.query(`SELECT receipt_id FROM public.server_poll_retirement_claims
WHERE receipt_id=$1 AND old_archive_proof=$2 AND claim_token=$3 AND claim_xid<>pg_current_xact_id()`,
[receiptId,oldArchiveProof,token])
@@ -141,7 +150,7 @@ let cursor:{finishedAt:string;receiptId:string}|undefined
export function retireCompletedServerPollOperations() {
return sharedDatabaseGate.request(async()=>{
const deadlineAt=Date.now()+20000
const rows=await transaction(deadlineAt,async sql=>(await sql.query(CANDIDATES,
const rows=await transaction(deadlineAt,"scan",async sql=>(await sql.query(CANDIDATES,
[cursor?.finishedAt??null,cursor?.receiptId??null])).rows)
requireThat(rows.length<=8)
let retired=0,scanned=0
@@ -11,8 +11,10 @@ import { createServerPollExecution,type ServerPollDependencies,type ServerPollGr
import { ServerPollProblem,pollProblem,validateServerPollContext,planServerPollMaintenance,type ServerPollContext,type PollRow,type ServerPollAck } from "./server-poll-execution-model.js"
export interface ServerPollSqlConnection {
query(text:string,values:unknown[],options:{signal?:AbortSignal;timeoutMs:number}):Promise<{rows:Record<string,unknown>[];rowCount?:number|null}>
query(text:string,values:unknown[],options:{signal?:AbortSignal;timeoutMs:number;statementName?:string}):Promise<{rows:Record<string,unknown>[];rowCount?:number|null;command?:string}>
release(unsafe?:boolean):void
on?(event:"error",listener:(error:Error)=>void):unknown
removeListener?(event:"error",listener:(error:Error)=>void):unknown
}
export interface ServerPollProductionIo {
connect(options:{signal?:AbortSignal;deadlineAt:number}):Promise<ServerPollSqlConnection>
@@ -146,8 +148,8 @@ export function createServerPollProductionIo():ServerPollProductionIo{
queryOptions.signal?.throwIfAborted();if(released)pollProblem("STATE_UNAVAILABLE","SQL connection poll закрыт")
const cancel=()=>{unsafe=true;release(true)};queryOptions.signal?.addEventListener("abort",cancel,{once:true})
if(queryOptions.signal?.aborted){cancel();pollProblem("ABORTED","SQL server poll отменён до dispatch")}
try{const request:QueryConfig&{query_timeout:number}={text,values,query_timeout:queryOptions.timeoutMs};return await client.query<Record<string,unknown>>(request)}catch(error){unsafe=true;throw error}finally{queryOptions.signal?.removeEventListener("abort",cancel)}
},release}
try{const request:QueryConfig&{query_timeout:number}={text,values,name:queryOptions.statementName,query_timeout:queryOptions.timeoutMs};return await client.query<Record<string,unknown>>(request)}catch(error){unsafe=true;throw error}finally{queryOptions.signal?.removeEventListener("abort",cancel)}
},release,on:(event,listener)=>client.on(event,listener),removeListener:(event,listener)=>client.removeListener(event,listener)}
},
applicationLock:async work=>(await import("./config-mutation-lock.js")).withApplicationMutationLock(work),
serverLocks:async(ids,work)=>(await import("./config-mutation-lock.js")).withConfigMutationLock(ids,work),
@@ -1,8 +1,11 @@
import assert from "node:assert/strict"
import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "node:crypto"
import { mkdtemp, rmdir, unlink, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { test } from "node:test"
import { fingerprint } from "../modules/mcp/security.js"
import { createServerPollRetiredArchiveForTests } from "./server-poll-retired-archive.js"
import { createServerPollRetiredArchiveChargeVerifier, createServerPollRetiredArchiveForTests } from "./server-poll-retired-archive.js"
import { createServerPollTerminalArchiveForTests, ServerPollArchiveRefused } from "./server-poll-terminal-archive.js"
import type { VerifiedServerPollHistory } from "./server-poll-history-verification.js"
@@ -20,6 +23,37 @@ const gcNotBefore = "2026-11-29T03:20:00.123460Z"
const now = Date.parse("2026-11-30T00:00:00Z")
const key = randomBytes(32), mac = randomBytes(32)
const proof = async (purpose: string, value: unknown) => createHmac("sha256", mac).update(purpose).update(fingerprint(value)).digest("hex")
test("retired archive accounting derives the existing key anew for each scan and cannot bootstrap it", async () => {
const directory = await mkdtemp(join(tmpdir(), "mcp-retired-archive-key-"))
const filename = join(directory, "encryption-key"), previous = process.env.MCP_STATE_DIR
try {
process.env.MCP_STATE_DIR = directory
await assert.rejects(createServerPollRetiredArchiveChargeVerifier(), { code: "STATE_UNAVAILABLE" })
await writeFile(filename, key, { flag: "wx" })
const verifier = await createServerPollRetiredArchiveChargeVerifier()
const metadata = {
version: 2, receipt_id: receiptId, execution_id: executionId, connection_id: connectionId, owner_id: ownerId,
binding_proof: "a".repeat(64), context_proof: "b".repeat(64), terminal_digest: "c".repeat(64),
original_activity_at: terminalAt, original_expires_at: expiredAt, retention_days: 30, safety_ms: 60000,
source_digest: "d".repeat(64), source_sha256: "e".repeat(64), decoded_bytes: 10, compressed_bytes: 20,
ciphertext_bytes: 64, ciphertext_sha256: "f".repeat(64),
}
const purposeKey = createHmac("sha256", key).update("MCP private fingerprint:server-poll-retired-archive-record-v2").digest()
const archiveProof = createHmac("sha256", purposeKey).update(fingerprint({
...metadata, identity_kind: "mcp", storage_state: "compacted", admission_bytes: 32768, charged_rows: 2,
})).digest("hex")
const row = { ...metadata, archive_proof: archiveProof }
assert.equal((await verifier(row)).bytes, 32832)
await assert.rejects(verifier({ ...row, ciphertext_sha256: "0".repeat(64) }), ServerPollArchiveRefused)
await writeFile(filename, randomBytes(32))
await assert.rejects((await createServerPollRetiredArchiveChargeVerifier())(row), ServerPollArchiveRefused)
} finally {
if (previous === undefined) delete process.env.MCP_STATE_DIR; else process.env.MCP_STATE_DIR = previous
await unlink(filename).catch(error => { if (error.code !== "ENOENT") throw error })
await rmdir(directory)
}
})
const encrypt = async (plain: Buffer) => {
const iv = randomBytes(12), cipher = createCipheriv("aes-256-gcm", key, iv)
const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()])
@@ -5,7 +5,7 @@ import { gzipSync, gunzipSync } from "node:zlib"
import { z } from "zod"
import { canonicalPollArchiveTimestamp, openServerPollTerminalArchive, ServerPollArchiveRefused } from "./server-poll-terminal-archive.js"
import { verifyServerPollHistory } from "./server-poll-history-verification.js"
import { decryptBinary, encryptBinary, fingerprint, privateSecretFingerprint } from "../modules/mcp/security.js"
import { decryptBinary, encryptBinary, fingerprint, privateFingerprinterExisting, privateSecretFingerprint } from "../modules/mcp/security.js"
const MAX_PAYLOAD_BYTES = 24576
const MAX_COMPRESSED_BYTES = MAX_PAYLOAD_BYTES
@@ -156,13 +156,21 @@ function codec(deps: Dependencies) {
return Object.freeze({ project, verifyCharge, open })
}
const production = codec({
const productionDependencies: Dependencies = {
openV1: openServerPollTerminalArchive, verifyHistory: verifyServerPollHistory,
encrypt: encryptBinary, decrypt: decryptBinary,
proof: (purpose, value) => privateSecretFingerprint(fingerprint(value), purpose),
})
}
const production = codec(productionDependencies)
export const projectServerPollRetiredArchive = production.project
export const verifyServerPollRetiredArchiveCharge = production.verifyCharge
export const openServerPollRetiredArchive = production.open
/** One accounting pass reuses the existing record-purpose signer. */
export async function createServerPollRetiredArchiveChargeVerifier() {
const signer = await privateFingerprinterExisting(RECORD_PURPOSE)
return codec({ ...productionDependencies, proof: async (purpose, value) => {
if (purpose !== RECORD_PURPOSE) return refuse()
return signer(fingerprint(value))
} }).verifyCharge
}
/** Isolated component crypto composition. No public/runtime caller selects it. */
export const createServerPollRetiredArchiveForTests = (deps: Dependencies) => codec(Object.freeze({ ...deps }))
@@ -5,7 +5,8 @@ import { fingerprint, privateSecretFingerprint } from "../modules/mcp/security.j
import type { ProbeJournalSql } from "./server-probe-journal.js"
import type { ProbeStorageOptions } from "./server-probe-journal-maintenance.js"
import type { ServerPollJournalContext } from "./server-poll-journal.js"
import { createServerPollStorage, PollStorageRefused, type PollStorageBinding } from "./server-poll-storage.js"
import { createServerPollStorage, createHolderBoundServerPollStorage, PollStorageRefused, type PollStorageBinding } from "./server-poll-storage.js"
import { isServerPollJournalOwner, type ServerPollJournalOwner } from "./server-partition-maintenance-lease.js"
export class PollStorageSpent extends PollStorageRefused {
readonly automaticReplay = false
@@ -124,5 +125,9 @@ async function construct(raw: ServerPollJournalContext, rawOptions: ServerPollSt
}
export const createServerPollStorageJournal = (context: ServerPollJournalContext, options: ServerPollStorageJournalOptions) => construct(context, options, production,true)
export function createHolderBoundServerPollStorageJournal(context:ServerPollJournalContext,options:ServerPollStorageJournalOptions,owner:ServerPollJournalOwner) {
if(!isServerPollJournalOwner(owner))fail()
return construct(context,options,{...production,createStorage:binding=>createHolderBoundServerPollStorage(binding,owner)},true)
}
/** Explicit trusted unit composition only; no request can select these hooks. */
export const createServerPollStorageJournalForTests = (context: ServerPollJournalContext, options: ServerPollStorageJournalOptions, dependencies: Pick<Dependencies, "createStorage"> & Partial<Omit<Dependencies, "createStorage">>) => construct(context, options, { ...production, ...dependencies })
@@ -15,13 +15,15 @@ function fixture(openRetired?: Parameters<typeof collectExpiredServerPollStorage
{ receipt_id: randomUUID(), owner_id: "owned-gc", archive_version: 1 }]
const retiredRows: Record<string, unknown>[] = []
const commands: { text: string; values: unknown[]; timeout: number }[] = [], releases: boolean[] = [], options: ProbeStorageOptions[] = []
let settings: Record<string, unknown>[] = [{ requests_retention_days: 30 }], fail = "", residual = "", removed: Record<string, unknown>[] | undefined, hold: ReturnType<typeof deferred> | undefined
let settings: Record<string, unknown>[] = [{ requests_retention_days: 30 }], fail = "", residual = "", removed: Record<string, unknown>[] | undefined, hold: ReturnType<typeof deferred> | undefined,writer=true
const entered = deferred()
const run = () => collectExpiredServerPollStorageForTests({ now: () => now, connect: async accepted => {
options.push(accepted)
return { async query(text, values = [], queryOptions) {
commands.push({ text, values, timeout: queryOptions!.timeoutMs })
if (text === fail) throw Error("PRIVATE_DRIVER_SECRET")
if (text.includes("pg_try_advisory_xact_lock_shared")) return { rows: [{ acquired:writer }] }
if (text.includes("current_setting('lock_timeout')")) return { rows: [{ lock_timeout: "0" }] }
// GC transaction/ordering fixture only. Authentic compact MAC and complete
// counter conservation are covered by archive-accounting component tests.
if (text.startsWith("SELECT a.receipt_id AS admission_receipt_id") || text.startsWith("SELECT r.receipt_id FROM server_poll_archives")) return { rows: [] }
@@ -35,16 +37,28 @@ function fixture(openRetired?: Parameters<typeof collectExpiredServerPollStorage
return { rows: [] }
}, release(unsafe = false) { releases.push(unsafe) } }
} }, openRetired)
return { run, now, candidates, retiredRows, commands, releases, options, entered: entered.promise, block: () => { hold = deferred(); return hold },
return { run, now, candidates, retiredRows, commands, releases, options, entered: entered.promise, block: () => { hold = deferred(); return hold },setWriter:(value:boolean)=>{writer=value},
setting: (rows: Record<string, unknown>[]) => { settings = rows }, fail: (text: string) => { fail = text }, residual: (table: string) => { residual = table }, removed: (rows: Record<string, unknown>[]) => { removed = rows } }
}
test("only a definite shared xact lock miss defers GC before POLL storage or a retention read",async()=>{
const fixtureBlocked=fixture();fixtureBlocked.setWriter(false)
assert.deepEqual(await fixtureBlocked.run(),{collected:0,automaticReplay:false})
const lock="SELECT pg_catalog.pg_try_advisory_xact_lock_shared($1::bigint) AS acquired"
assert.deepEqual(fixtureBlocked.commands.map(command=>command.text),["BEGIN",lock,"COMMIT"])
const unexpected=fixture();unexpected.fail(lock)
await assert.rejects(unexpected.run(),PollStorageRefused)
assert.deepEqual(unexpected.releases,[true])
})
test("selected GC owns one bounded backend, locks current retention, deletes all children and verifies trigger refund outcome", async () => {
const f = fixture()
assert.deepEqual(await f.run(), { collected: 2, automaticReplay: false })
assert.equal(f.options.length, 1); assert.equal(f.options[0].deadlineAt, f.now + 20000)
assert(f.commands.every(command => command.timeout >= 1 && command.timeout <= 5000))
const texts = f.commands.map(command => command.text), setting = texts.findIndex(text => text.includes("requests_retention_days"))
const writer = texts.findIndex(text => text.includes("pg_advisory_xact_lock_shared(")), storage = texts.findIndex(text => text.includes("server-poll-storage-v1"))
assert(writer > 0 && writer < storage && storage < setting)
assert(texts[setting].endsWith("FOR SHARE")); assert(setting < texts.indexOf("SET LOCAL mm.poll_storage_gc='on'"))
const selection = texts.find(text => text.startsWith("SELECT receipt_id,owner_id"))!
assert(selection.includes("gc_not_before<clock_timestamp()")); assert(selection.includes("server_poll_storage_gc_allowed(receipt_id)")); assert(selection.endsWith("LIMIT 8 FOR UPDATE"))
@@ -1,7 +1,7 @@
/** Private fixed poll GC. Not scheduled or exposed by this opt-in module. */
import { sharedDatabaseGate } from "./shared-database-gate.js"
import { connectProbeStorage } from "./server-probe-storage-connection.js"
import { runPollStorageTransaction, type PollStorageDependencies } from "./server-poll-storage.js"
import { admitPollStorageWriter, runPollStorageTransaction, tryAdmitPollStorageMaintenanceWriter, type PollStorageDependencies } from "./server-poll-storage.js"
import { PollStorageRefused } from "./server-poll-storage-model.js"
import { assertServerPollArchiveAccounting } from "./server-poll-archive-accounting.js"
import type { openServerPollRetiredArchive } from "./server-poll-retired-archive.js"
@@ -27,7 +27,8 @@ const retiredCandidate = `SELECT r.version,r.receipt_id,r.execution_id,r.connect
FOR UPDATE OF r`
async function collect(deps:PollStorageDependencies, openRetired:typeof openServerPollRetiredArchive){
return runPollStorageTransaction(deps,{deadlineAt:deps.now()+20000},async()=>{},async sql=>{
const deadlineAt=deps.now()+20000
return runPollStorageTransaction(deps,{deadlineAt},async()=>{},async sql=>{
// Hold the authoritative setting against concurrent updates until COMMIT.
// Each DELETE guard additionally checks its current value independently.
const setting=(await sql.query("SELECT requests_retention_days FROM mcp_settings WHERE id=1 FOR SHARE")).rows
@@ -62,6 +63,9 @@ async function collect(deps:PollStorageDependencies, openRetired:typeof openServ
if((await sql.query(`SELECT receipt_id FROM ${table} WHERE receipt_id=ANY($1::uuid[]) LIMIT 1`,[ids])).rows.length)fail()
}
return {collected:ids.length,automaticReplay:false as const}
},async sql=>{
if(!await tryAdmitPollStorageMaintenanceWriter(sql,"transaction"))return {deferred:{collected:0,automaticReplay:false as const}}
await admitPollStorageWriter(sql,deadlineAt)
})
}
export const collectExpiredServerPollStorage=()=>sharedDatabaseGate.request(()=>collect(production,openRetiredArchive))
+41 -7
View File
@@ -7,6 +7,8 @@ import { connectProbeStorage } from "./server-probe-storage-connection.js"
import type { ProbeStorageConnection, ProbeStorageOptions, ProbeStorageSql } from "./server-probe-journal-maintenance.js"
import { pollStorageScope, PollStorageRefused, type PollStorageScope } from "./server-poll-storage-model.js"
import { assertServerPollArchiveAccounting } from "./server-poll-archive-accounting.js"
import type { ServerPollJournalOwner } from "./server-partition-maintenance-lease.js"
import { admitPartitionWriter, PARTITION_WRITER_ADVISORY_KEY } from "./partition-write-transaction.js"
export { PollStorageRefused }
export class PollStorageUnknown extends Error {
@@ -29,9 +31,32 @@ const production:PollStorageDependencies={connect:connectProbeStorage,now:Date.n
const fail=():never=>{throw new PollStorageRefused()}
const lock="SELECT pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0))"
/** The writer fence precedes POLL's storage, operation and row locks. Restore
* the lock timeout before the runner sets its usual storage statement budget. */
export async function admitPollStorageWriter(sql:ProbeStorageSql,deadlineAt:number):Promise<void>{
const settings=(await sql.query("SELECT pg_catalog.current_setting('lock_timeout') AS lock_timeout")).rows
if(settings.length!==1||typeof settings[0].lock_timeout!=="string")fail()
await admitPartitionWriter(sql,{deadlineAt:Math.min(deadlineAt,Date.now()+5000)})
await sql.query("SELECT pg_catalog.set_config('lock_timeout',$1,true)",[settings[0].lock_timeout])
}
/** Only a definite false read from this exact lock query can defer maintenance.
* A SQL, provider, timeout, shape or acknowledgement failure stays an error. */
export async function tryAdmitPollStorageMaintenanceWriter(sql:ProbeStorageSql,scope:"session"|"transaction"):Promise<boolean>{
const statement=scope==="session"
?"SELECT pg_catalog.pg_try_advisory_lock_shared($1::bigint) AS acquired"
:"SELECT pg_catalog.pg_try_advisory_xact_lock_shared($1::bigint) AS acquired"
const {rows}=await sql.query(statement,[PARTITION_WRITER_ADVISORY_KEY])
if(rows.length!==1||typeof rows[0].acquired!=="boolean")return fail()
return rows[0].acquired
}
type PollStoragePrelude<T>=(sql:ProbeStorageSql)=>Promise<void|{deferred:T}>
/** Own one connector through callback and COMMIT, including non-SQL awaits.
* Leaked facades expire; no ROLLBACK or retry follows an uncertain COMMIT. */
export async function runPollStorageTransaction<T>(deps:PollStorageDependencies,options:ProbeStorageOptions,authorize:()=>Promise<void>,work:(sql:ProbeStorageSql)=>Promise<T>):Promise<T>{
* Leaked facades expire; no ROLLBACK or retry follows an uncertain COMMIT.
* A trusted maintenance caller can admit its writer on this same checkout before the storage lock. */
export async function runPollStorageTransaction<T>(deps:PollStorageDependencies,options:ProbeStorageOptions,authorize:()=>Promise<void>,work:(sql:ProbeStorageSql)=>Promise<T>,beforeStorageLock?:PollStoragePrelude<T>):Promise<T>{
deps=Object.freeze({connect:deps.connect,now:deps.now})
options=Object.freeze({deadlineAt:options.deadlineAt,signal:options.signal})
if(!Number.isSafeInteger(options.deadlineAt)||options.deadlineAt<=deps.now()||options.deadlineAt-deps.now()>180000||options.signal?.aborted)fail()
@@ -58,13 +83,17 @@ export async function runPollStorageTransaction<T>(deps:PollStorageDependencies,
connection=await deps.connect(options)
if(!active||options.signal?.aborted||deps.now()>=options.deadlineAt){close(true);fail()}
await sql.query("BEGIN");begun=true
await sql.query("SELECT set_config('TimeZone','UTC',true),set_config('statement_timeout','5000ms',true)")
await sql.query(lock)
const owned:ProbeStorageSql=Object.freeze({async query(text:string,values?:unknown[]){
const command=text.replace(/\/\*[\s\S]*?\*\/|--[^\r\n]*/g," ").trim()
if(/^(?:BEGIN|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE|START|PREPARE)\b|^SET\s+TRANSACTION\b/i.test(command)||command.includes(";"))fail()
return sql.query(text,values)
}})
if(beforeStorageLock){
const admission=await beforeStorageLock(owned)
if(admission){await sql.query("COMMIT");return admission.deferred}
}
await sql.query("SELECT set_config('TimeZone','UTC',true),set_config('statement_timeout','5000ms',true)")
await sql.query(lock)
const result=await work(owned);await fence();await sql.query("COMMIT");return result
})
try{return await Promise.race([completion,stopped])}
@@ -75,7 +104,7 @@ export async function runPollStorageTransaction<T>(deps:PollStorageDependencies,
}finally{active=false;clearTimeout(timer);options.signal?.removeEventListener("abort",stop);close(broken||busy||!committed)}
}
function factory(raw:PollStorageBinding,rawDeps:PollStorageDependencies){
function factory(raw:PollStorageBinding,rawDeps:PollStorageDependencies,journalOwner?:ServerPollJournalOwner){
const deps=Object.freeze({connect:rawDeps.connect,now:rawDeps.now})
const scope=pollStorageScope(structuredClone(raw.scope),{now:deps.now()}),authorize=raw.authorize
const connectionId=scope.kind==="mcp"?z.string().uuid().parse(raw.connectionId):raw.connectionId===null?null:fail()
@@ -98,7 +127,10 @@ function factory(raw:PollStorageBinding,rawDeps:PollStorageDependencies){
}
const run=async<T>(options:ProbeStorageOptions,work:(sql:ProbeStorageSql)=>Promise<T>)=>{
const accepted=Object.freeze({signal:options.signal,deadlineAt:Math.min(options.deadlineAt,scope.expiresAt)})
try{return await runPollStorageTransaction(deps,accepted,()=>current(accepted),work)}
try{
const authorize=()=>current(accepted)
return await (journalOwner?journalOwner.transaction(accepted,authorize,work):runPollStorageTransaction(deps,accepted,authorize,work))
}
catch(error){if(error instanceof PollStorageUnknown)poisoned=true;throw error}
}
return Object.freeze({
@@ -132,15 +164,17 @@ function factory(raw:PollStorageBinding,rawDeps:PollStorageDependencies){
* run() has released/destroyed its physical backend before this is called. */
async quarantine(){
const options={deadlineAt:deps.now()+10000}
journalOwner?.assertClosedForQuarantine()
return runPollStorageTransaction(deps,options,async()=>{},async sql=>{
if(!await read(sql))fail()
const rows=(await sql.query("SELECT receipt_id,identity_kind,execution_id,context_proof FROM server_poll_quarantines WHERE receipt_id=$1",[scope.receiptId])).rows
if(rows.length){if(rows.length!==1||rows[0].identity_kind!==scope.kind||rows[0].execution_id!==scope.executionId||rows[0].context_proof!==scope.contextProof)fail();return}
await sql.query("INSERT INTO server_poll_quarantines(receipt_id,identity_kind,execution_id,context_proof) VALUES($1,$2,$3,$4)",[scope.receiptId,scope.kind,scope.executionId,scope.contextProof])
})
},journalOwner?sql=>admitPollStorageWriter(sql,options.deadlineAt):undefined)
},
})
}
export const createServerPollStorage=(binding:PollStorageBinding)=>factory(binding,production)
export const createHolderBoundServerPollStorage=(binding:PollStorageBinding,owner:ServerPollJournalOwner)=>factory(binding,production,owner)
/** Explicit unit/isolated SQL composition, never populated from a request. */
export const createServerPollStorageForTests=(binding:PollStorageBinding,deps:PollStorageDependencies)=>factory(binding,deps)
@@ -77,7 +77,7 @@ async function main() {
}
try {
await applySqlMigrations(pool)
assert.equal(MIGRATION_MANIFEST.at(-1)?.file, "0055_mcp_backup_schedule_retirement.sql")
assert.equal(MIGRATION_MANIFEST.at(-1)?.file, "0056_mcp_manual_inverse_recovery_v8.sql")
assert.equal(MIGRATION_MANIFEST.find(pin=>pin.file==="0041_server_poll_terminal_archives.sql")?.sha256,"fbb8935ce977cfc507801f01beb8f303d4817ed63c6bdd4c05799768dfaac9d4")
assert.equal((await pool.query("SELECT count(*)::int AS n FROM schema_migrations")).rows[0].n, MIGRATION_MANIFEST.length)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM server_poll_storage_admissions")).rows[0].n, 0)
@@ -7,6 +7,7 @@ import ts from "typescript"
import { z } from "zod"
import * as model from "./server-poll-storage-model.js"
import * as errorProvenance from "./server-poll-error-provenance.js"
import { admitPartitionWriter, PARTITION_WRITER_ADVISORY_KEY } from "./partition-write-transaction.js"
import type { PollStorageDependencies } from "./server-poll-storage.js"
/** Actual compactor and transaction dispatcher, explicit SQL/codec/accounting
@@ -29,9 +30,9 @@ function load(code: string, modules: Record<string, unknown>) {
}
function fixture() {
const now = Date.now(), receiptId = randomUUID(), operationId = randomUUID(), connectionId = randomUUID()
let state: State = { claim: null, archive: null, audits: [] }, connects = 0, commits = 0, createCalls = 0, openCalls = 0, accountCalls = 0
let state: State = { claim: null, archive: null, audits: [] }, connects = 0, transactions=0, commits = 0, createCalls = 0, openCalls = 0, accountCalls = 0
let eligible = true, sourceMissing = false, accountingFails = false, preflight: "valid" | "oversize" | "invalid" = "valid", secondOversize = false
let lostCommit = 0, commitPersists = true, badPublishedSize = false, badPublishedText = false, auditFails = false
let lostCommit = 0, commitPersists = true, badPublishedSize = false, badPublishedText = false, auditFails = false, writer: boolean|Error=true
const commands: Command[] = [], releases: { tx: number; unsafe: boolean }[] = [], events: string[] = []
class ArchiveRefused extends Error {}
const forbiddenConnector = async (): Promise<never> => { throw Error("Production connector must never run") }
@@ -40,11 +41,13 @@ function fixture() {
"node:crypto": { randomUUID }, zod: { z }, "./server-probe-storage-connection.js": { connectProbeStorage: forbiddenConnector },
"./server-poll-storage-model.js": model, "./server-poll-archive-accounting.js": { assertServerPollArchiveAccounting: accounting },
"./server-poll-error-provenance.js": errorProvenance,
"./partition-write-transaction.js": { admitPartitionWriter, PARTITION_WRITER_ADVISORY_KEY },
})
const archives = new Map<string, string>()
const api = load(compactorCode, {
"node:crypto": { randomUUID }, "./shared-database-gate.js": { sharedDatabaseGate: { request: (work: () => unknown) => work() } },
"./server-probe-storage-connection.js": { connectProbeStorage: forbiddenConnector }, "./server-poll-storage.js": storage,
"./partition-write-transaction.js": { PARTITION_WRITER_ADVISORY_KEY },
"./server-poll-archive-accounting.js": { assertServerPollArchiveAccounting: accounting },
"./server-poll-terminal-archive.js": {
ServerPollArchiveRefused: ArchiveRefused,
@@ -67,22 +70,26 @@ function fixture() {
},
}) as unknown as Api
const deps: PollStorageDependencies = { now: () => now, connect: async options => {
const tx = ++connects; let transaction: State | undefined, closed = false
assert.equal(options.deadlineAt, now + 20000)
const owner = ++connects; let tx=0, transaction: State | undefined, closed = false
assert.equal(options.deadlineAt, now + 40000)
return { async query(text, values = [], queryOptions) {
values = Array.from(values) // Normalize the VM caller's array realm only.
assert(!closed); assert(queryOptions && queryOptions.timeoutMs >= 1 && queryOptions.timeoutMs <= 5000)
if(text==="BEGIN")tx=++transactions
commands.push({ tx, text, values }); events.push(tx + ":" + text)
if (text === "BEGIN") { transaction = structuredClone(state); return { rows: [] } }
if (text === "ROLLBACK") { transaction = undefined; return { rows: [] } }
if (text.includes("pg_advisory_unlock_shared")) {assert.equal(transaction,undefined);return {rows:[{released:true}]}}
assert(transaction)
if (text.includes("pg_try_advisory_lock_shared")) {if(writer instanceof Error)throw writer;return {rows:[{acquired:writer}]}}
if (text === "COMMIT") {
commits++; if (commits !== lostCommit || commitPersists) state = structuredClone(transaction)
transaction = undefined
if (commits === lostCommit) throw Error("PRIVATE_DRIVER_COMMIT_ACK_LOST")
return { rows: [] }
}
if (text.startsWith("SELECT set_config(") || text.includes("pg_advisory_xact_lock") || text === ACCOUNTING) return { rows: [] }
if (text.includes("current_setting('lock_timeout')")) return { rows: [{ lock_timeout: "0" }] }
if (text.startsWith("SELECT set_config(") || text.startsWith("SELECT pg_catalog.set_config(") || text.startsWith("SET LOCAL ") || text.includes("pg_advisory_xact_lock") || text === ACCOUNTING) return { rows: [] }
if (text.startsWith("SELECT a.receipt_id FROM server_poll_storage_admissions")) {
assert(text.includes("a.compaction_claim IS NULL")); assert(text.includes("a.storage_state='full'"))
return { rows: eligible && !transaction.claim && !transaction.archive ? [{ receipt_id: receiptId }] : [] }
@@ -113,10 +120,10 @@ function fixture() {
assert.equal(values[1], "poll.history.compacted"); transaction.audits.push({ actor: values[0], connection: values[2], payload: JSON.parse(String(values[3])) }); return { rows: [] }
}
throw Error("Unapproved fixture SQL: " + text)
}, release(unsafe = false) { assert(!closed); closed = true; transaction = undefined; releases.push({ tx, unsafe }) } }
}, release(unsafe = false) { assert(!closed); closed = true; transaction = undefined; releases.push({ tx:owner, unsafe }) } }
} }
return { run: () => api.compactCompletedServerPollStorageForTests(deps), commands, releases, events, state: () => structuredClone(state),
counts: () => ({ connects, commits, createCalls, openCalls, accountCalls }), Unknown: storage.PollStorageUnknown as new () => Error,
counts: () => ({ connects, commits, createCalls, openCalls, accountCalls }), Unknown: storage.PollStorageUnknown as new () => Error,setWriter:(value:boolean|Error)=>{writer=value},
configure(options: { eligible?: boolean; sourceMissing?: boolean; accountingFails?: boolean; preflight?: typeof preflight; secondOversize?: boolean; lostCommit?: number; commitPersists?: boolean; badPublishedSize?: boolean; badPublishedText?: boolean; auditFails?: boolean }) {
eligible = options.eligible ?? eligible; sourceMissing = options.sourceMissing ?? sourceMissing; accountingFails = options.accountingFails ?? accountingFails
preflight = options.preflight ?? preflight; secondOversize = options.secondOversize ?? secondOversize; lostCommit = options.lostCommit ?? lostCommit
@@ -133,6 +140,17 @@ test("idle tick skips accounting and codec/key/native dependencies", async () =>
assert.equal(claimed(f).length, 0); assert.equal(published(f).length, 0)
})
test("only a definite shared session lock miss defers before POLL storage, and SQL failures remain errors",async()=>{
const blocked=fixture();blocked.setWriter(false)
assert.deepEqual({...await blocked.run()},{scanned:0,compacted:0,skipped:0,automaticReplay:false})
assert.deepEqual(blocked.commands.map(command=>command.text),["BEGIN","SELECT pg_catalog.pg_try_advisory_lock_shared($1::bigint) AS acquired","COMMIT"])
assert.deepEqual(blocked.releases,[{tx:1,unsafe:false}])
const unexpected=fixture();unexpected.setWriter(Error("owned unexpected provider failure"))
await assert.rejects(unexpected.run(),model.PollStorageRefused)
assert.deepEqual(unexpected.releases,[{tx:1,unsafe:true}])
assert.equal(claimed(unexpected).length,0)
})
test("ineligible SQL source, oversize and invalid preflight skip without consuming claim", async () => {
for (const options of [{ sourceMissing: true }, { preflight: "oversize" as const }, { preflight: "invalid" as const }]) {
const f = fixture(); f.configure(options); const result = await f.run()
@@ -161,7 +179,7 @@ test("phase1 COMMIT ACK loss never starts phase2 or inserts archive, for either
test("normal two acknowledged COMMITs publish exact readback and audit in the same transaction", async () => {
const f = fixture(), result = await f.run()
assert.equal(result.compacted, 1); assert.equal(result.automaticReplay, false)
assert.deepEqual(f.counts(), { connects: 2, commits: 2, createCalls: 2, openCalls: 2, accountCalls: 3 })
assert.deepEqual(f.counts(), { connects: 1, commits: 2, createCalls: 2, openCalls: 2, accountCalls: 3 })
const firstCommit = f.events.indexOf("1:COMMIT"), secondBegin = f.events.indexOf("2:BEGIN")
assert(firstCommit >= 0 && firstCommit < secondBegin)
for (const tx of [1, 2]) {
@@ -175,7 +193,7 @@ test("normal two acknowledged COMMITs publish exact readback and audit in the sa
assert(f.events.indexOf("open:2") < f.events.findIndex(event => event.startsWith("2:INSERT INTO mcp_audit")))
assert.equal(f.state().audits.length, 1); assert.deepEqual(f.state().audits[0].payload, { operationId: f.state().archive!.execution_id, receiptId: f.state().archive!.receipt_id,
archiveBytes: f.state().archive!.ciphertext_bytes, sourceJournalRows: 6, originalRetentionDays: 30, automaticReplay: false })
assert.deepEqual(f.releases, [{ tx: 1, unsafe: false }, { tx: 2, unsafe: false }])
assert.deepEqual(f.releases, [{ tx: 1, unsafe: false }])
})
test("phase2 COMMIT ACK loss leaves spent full or complete archive, never rollback or automatic resume", async () => {
@@ -184,7 +202,7 @@ test("phase2 COMMIT ACK loss leaves spent full or complete archive, never rollba
await assert.rejects(f.run(), f.Unknown)
assert(f.state().claim); assert.equal(!!f.state().archive, commitPersists); assert.equal(f.state().audits.length, commitPersists ? 1 : 0)
assert(!f.commands.some(command => command.text === "ROLLBACK")); assert.equal(published(f).length, 1)
assert.deepEqual(f.releases, [{ tx: 1, unsafe: false }, { tx: 2, unsafe: true }])
assert.deepEqual(f.releases, [{ tx: 1, unsafe: true }])
const counts = f.counts(); const next = await f.run()
assert.equal(next.scanned, 0); assert.equal(next.compacted, 0); assert.equal(claimed(f).length, 1); assert.equal(published(f).length, 1)
assert.equal(f.counts().createCalls, counts.createCalls); assert.equal(f.counts().accountCalls, counts.accountCalls)
@@ -3,7 +3,9 @@
import { randomUUID } from "node:crypto"
import { sharedDatabaseGate } from "./shared-database-gate.js"
import { connectProbeStorage } from "./server-probe-storage-connection.js"
import { runPollStorageTransaction, PollStorageRefused, type PollStorageDependencies } from "./server-poll-storage.js"
import { admitPollStorageWriter, runPollStorageTransaction, tryAdmitPollStorageMaintenanceWriter, PollStorageRefused, type PollStorageDependencies } from "./server-poll-storage.js"
import { PARTITION_WRITER_ADVISORY_KEY } from "./partition-write-transaction.js"
import type { ProbeStorageConnection, ProbeStorageSql } from "./server-probe-journal-maintenance.js"
import { assertServerPollArchiveAccounting } from "./server-poll-archive-accounting.js"
import { createServerPollTerminalArchive, openServerPollTerminalArchive, ServerPollArchiveRefused, type ServerPollTerminalArchive } from "./server-poll-terminal-archive.js"
@@ -39,9 +41,55 @@ function insert(archive: ServerPollTerminalArchive) {
return { text: `INSERT INTO server_poll_archives(${names.join(",")}) VALUES(${names.map((_name, index) => "$" + (index + 1)).join(",")}) RETURNING receipt_id`,
values: names.map(name => archive[name]) }
}
async function compact(deps: PollStorageDependencies) {
/** One physical session keeps the shared writer lock across the independently
* acknowledged claim and publication. An unsafe virtual release destroys the
* physical owner, so a spent or uncertain claim cannot open another backend. */
function compactionWriter(deps:PollStorageDependencies,leaseDeadline:number){
let physical:ProbeStorageConnection|undefined,borrowed=false,acquired=false,closed=false
const release=(unsafe:boolean)=>{
closed=true
const connection=physical;physical=undefined
connection?.release(unsafe)
}
const connect:PollStorageDependencies["connect"]=async options=>{
if(closed||borrowed)fail()
borrowed=true
try{
const connection=physical??await deps.connect({...options,deadlineAt:leaseDeadline})
if(closed){connection.release(true);return fail()}
physical=connection
let released=false
return {
query:(...args)=>{if(released||closed)return Promise.reject(new PollStorageRefused());return connection.query(...args)},
release(unsafe=false){if(released)return;released=true;borrowed=false;if(unsafe)release(true)},
}
}catch(error){borrowed=false;throw error}
}
return {
deps:{now:deps.now,connect},
async tryAcquire(sql:ProbeStorageSql){acquired=await tryAdmitPollStorageMaintenanceWriter(sql,"session");return acquired},
async finish(deadlineAt:number){
if(!physical||borrowed)return fail()
if(acquired){
const remaining=Math.min(5000,deadlineAt-deps.now())
if(remaining<1)return fail()
const {rows}=await physical.query("SELECT pg_catalog.pg_advisory_unlock_shared($1::bigint) AS released",[PARTITION_WRITER_ADVISORY_KEY],{timeoutMs:remaining})
if(rows.length!==1||rows[0].released!==true)return fail()
}
release(false)
},
destroy(){if(!closed)release(true)},
}
}
async function compact(deps:PollStorageDependencies){
const firstDeadline=deps.now()+20000,writer=compactionWriter(deps,firstDeadline+20000)
try{return await compactOnWriter(writer,firstDeadline)}finally{writer.destroy()}
}
async function compactOnWriter(writer:ReturnType<typeof compactionWriter>,firstDeadline:number) {
const {deps}=writer
let scanned = 0, skipped = 0
const firstDeadline = deps.now() + 20000
const claimed = await runPollStorageTransaction(deps, { deadlineAt: firstDeadline }, async () => {}, async sql => {
const candidates = (await sql.query(CANDIDATES)).rows
if (candidates.length > 128) fail()
@@ -75,12 +123,16 @@ async function compact(deps: PollStorageDependencies) {
return { receiptId: candidate.receipt_id, token }
}
return undefined
}, async sql => {
if(!await writer.tryAcquire(sql))return {deferred:undefined}
await admitPollStorageWriter(sql,firstDeadline)
})
if (!claimed) return { scanned, compacted: 0, skipped, automaticReplay: false as const }
if (!claimed){await writer.finish(firstDeadline);return { scanned, compacted: 0, skipped, automaticReplay: false as const }}
// This branch is reached ONLY after the separate claim COMMIT was ACKed.
// Any loss after here leaves either a complete archive or a charged spent
// full row. Fresh workers select compaction_claim IS NULL and never replay it.
const compacted = await runPollStorageTransaction(deps, { deadlineAt: deps.now() + 20000 }, async () => {}, async sql => {
const secondDeadline = deps.now() + 20000
const compacted = await runPollStorageTransaction(deps, { deadlineAt: secondDeadline }, async () => {}, async sql => {
await assertServerPollArchiveAccounting(sql)
const admission = (await sql.query("SELECT receipt_id FROM server_poll_storage_admissions WHERE receipt_id=$1::uuid AND storage_state='full' AND compaction_claim=$2::uuid AND compaction_claimed_at IS NOT NULL AND compaction_claim_xid IS NOT NULL FOR UPDATE", [claimed.receiptId, claimed.token])).rows
if (admission.length !== 1) fail()
@@ -104,7 +156,8 @@ async function compact(deps: PollStorageDependencies) {
originalRetentionDays: archive.retention_days, automaticReplay: false })])
await assertServerPollArchiveAccounting(sql)
return 1
})
}, sql => admitPollStorageWriter(sql, secondDeadline))
await writer.finish(secondDeadline)
return { scanned, compacted, skipped: skipped + (compacted ? 0 : 1), automaticReplay: false as const }
}
export const compactCompletedServerPollStorage = () => sharedDatabaseGate.request(() => compact(production))
@@ -4,6 +4,7 @@
* trusted original-expiry entry gates and the guarded v2 schema proposal. */
import { randomUUID } from "node:crypto"
import { probeMetadataGcEligible } from "./server-probe-journal-maintenance-model.js"
import { admitPartitionWriter } from "./partition-write-transaction.js"
export interface ProbeStorageSql { query(text:string,values?:unknown[],options?:{signal?:AbortSignal;timeoutMs:number}):Promise<{rows:Record<string,unknown>[]}> }
export interface ProbeStorageConnection extends ProbeStorageSql {
@@ -71,16 +72,30 @@ export function createProbeJournalStorage(deps:ProbeStorageDependencies){
must(configured)
must(Number.isSafeInteger(deps.safetyWindowMs)&&deps.safetyWindowMs>=60000&&deps.safetyWindowMs<=86400000)
const policy=validateProbeStoragePolicy({retention_days:configured.retentionDays,global_bytes:configured.globalBytes,owner_bytes:configured.ownerBytes,global_files:configured.globalFiles,owner_files:configured.ownerFiles,global_admissions:configured.globalAdmissions,owner_admissions:configured.ownerAdmissions})
const run=async<T>(ownerId:string,operation:"claim"|"publish"|"read"|"purge"|"cancel",bound:ProbeStorageScope|undefined,options:ProbeStorageOptions,work:(sql:ProbeStorageSql,policy:ProbeStoragePolicy,bind:(value:ProbeStorageScope)=>Promise<void>)=>Promise<T>):Promise<T>=>{
const run=async<T>(ownerId:string,operation:"claim"|"publish"|"read"|"purge"|"cancel",bound:ProbeStorageScope|undefined,options:ProbeStorageOptions,work:(sql:ProbeStorageSql,policy:ProbeStoragePolicy,bind:(value:ProbeStorageScope)=>Promise<void>)=>Promise<T>,mode:"default"|"expiry-write"="default"):Promise<T>=>{
owner(ownerId);must(Number.isFinite(options.deadlineAt)&&options.deadlineAt>now())
let authority:string|undefined
const fence=async()=>{options.signal?.throwIfAborted();must(now()<options.deadlineAt);const proof=await deps.authorize(ownerId,operation,bound,options);must(hash.test(proof)&&(!authority||authority===proof));authority=proof;options.signal?.throwIfAborted();must(now()<options.deadlineAt)}
await fence();let connection:ProbeStorageConnection
try{connection=await deps.connect(options)}catch{throw new ProbeStorageUnknown()}
let commitSent=false,committed=false,began=false,destroy=false
const sql:ProbeStorageSql={query:async(text,values)=>{await fence();const result=await connection.query(text,values,{signal:options.signal,timeoutMs:Math.max(1,Math.min(10000,options.deadlineAt-now()))});await fence();return result}}
const query:ProbeStorageSql["query"]=(text,values)=>connection.query(text,values,{signal:options.signal,timeoutMs:Math.max(1,Math.min(10000,options.deadlineAt-now()))})
const sql:ProbeStorageSql={query:async(text,values)=>{await fence();const result=await query(text,values);await fence();return result}}
try{
await sql.query("BEGIN");began=true
if(mode==="expiry-write"||operation==="claim"||operation==="publish"||operation==="cancel"){
// Acquire the common key before authority checks can retain a relation
// lock on this backend and before the secondary probe-storage lock.
await fence()
await query("BEGIN")
try{await admitPartitionWriter({query},{signal:options.signal,deadlineAt:Date.now()+Math.min(options.deadlineAt-now(),1000)})}
catch{began=true;throw new ProbeStorageRefused()}
if(mode!=="expiry-write"){
began=true
await query("SET LOCAL lock_timeout TO DEFAULT")
}
await fence()
}else await sql.query("BEGIN")
began=true
await sql.query("SET LOCAL TimeZone='UTC'")
await sql.query(`SET LOCAL statement_timeout='${Math.max(1,Math.min(10000,options.deadlineAt-now()))}ms'`)
// Shared ordering across EVERY replica, admission, finish and purge.
@@ -230,7 +245,7 @@ export function createProbeJournalStorage(deps:ProbeStorageDependencies){
await sql.query("/* probe-storage:gc-owner-counter */ DELETE FROM server_probe_storage_usage WHERE scope=$1 AND admissions=0 AND reserved_files=0 AND reserved_bytes=0",["owner:"+ownerId])
const absent=(await sql.query("/* probe-storage:gc-readback */ SELECT a.receipt_id FROM server_probe_storage_admissions a WHERE receipt_id=ANY($1::uuid[]) UNION ALL SELECT e.receipt_id FROM server_probe_executions e WHERE receipt_id=ANY($1::uuid[]) UNION ALL SELECT s.receipt_id FROM server_probe_execution_stages s WHERE receipt_id=ANY($1::uuid[]) UNION ALL SELECT q.receipt_id FROM server_probe_execution_quarantines q WHERE receipt_id=ANY($1::uuid[]) UNION ALL SELECT p.receipt_id FROM server_probe_execution_payloads p WHERE receipt_id=ANY($1::uuid[])",[ids])).rows;must(absent.length===0)
return {collected:ids.length,automaticReplay:false as const}
})
},"expiry-write")
},
/** DRAFT: only expired cancellation records of terminal real own diagnostic
* operations. Does not delete operations, plans, connections or audit. */
Loaded 100 of 125 files, more files were not shown because too many files have changed in this diff. Show more