Compare commits

..
Author SHA1 Message Date
Codex 9240e8be11 fix(netflow): связать отметки потока SSE с диагностикой NetFlow 2026-11-12 17:46:59 +07:00
Denozordec d79fb3815a fix(mcp): упорядочить записи журнала опроса 2026-11-12 10:23:34 +07:00
Codex 765b9a1658 fix(netflow): читать активные записи на одном снимке 2026-11-12 08:40:06 +07:00
Codex 5d181e7cb0 fix(netflow): пропустить загрузку флага неизвестной страны 2026-11-11 23:46:51 +07:00
Codex cf211f7418 fix(netflow): восстановить открытие потока после временного ограничения 2026-11-11 22:54:42 +07:00
Codex 04225886e0 fix(mcp): довести базовые сценарии и проверку маршрутизации 2026-11-11 20:28:37 +07:00
Denozordec 1f0704add0 fix(mcp): включить проверяемый контроллер маршрутизации 2026-11-05 05:36:19 +07:00
Codex 6569dbfc60 fix(mcp): привязать OAuth-согласие и завершать чтение логов при обслуживании 2026-11-05 03:56:17 +07:00
Denozordec 78b268cc39 fix(wireguard): безопасно повторить чтение зависимостей 2026-11-05 01:39:26 +07:00
Denozordec 3d5b7f0ea6 fix(traffic): загружать данные потоков последовательно 2026-11-04 23:51:03 +07:00
Denozordec b4d2d4bc20 fix(mcp): ограничить очередь NetFlow и защитить чтение истории 2026-11-04 21:58:22 +07:00
Denozordec c90a6ca3b8 fix(test): сравнивать тела миграций с учётом окончаний строк 2026-11-04 19:09:45 +07:00
Denozordec bf4b9b5443 fix(mcp): уточнить переключение маршрутов и хранение операций 2026-11-04 19:04:51 +07:00
Denozordec 5ce56b5168 fix(mcp): обновить проверенный снимок маршрутизации и хранения операций 2026-11-04 17:14:57 +07:00
251 changed files with 18137 additions and 850 deletions

No files matched your search

+9 -6
View File
@@ -924,18 +924,21 @@ export default function TrafficPage() {
}) })
return () => { cancelled = true } return () => { cancelled = true }
} }
void Promise.all([ const loadFlowCards = async () => {
getTrafficFlows(backendUrl, range), // These reads share the collector's constrained admission budget.
getFlowExporters(backendUrl, range), const stats = await getTrafficFlows(backendUrl, range)
getFlowClients(backendUrl, range), if (cancelled) return
]).then(([stats, exporters, clients]) => { const exporters = await getFlowExporters(backendUrl, range)
if (cancelled) return
const clients = await getFlowClients(backendUrl, range)
if (cancelled) return if (cancelled) return
setFlowResult({ key: flowKey, receivedAt: Date.now(), stats, exporters: exporters.exporters, clients: clients.clients }) setFlowResult({ key: flowKey, receivedAt: Date.now(), stats, exporters: exporters.exporters, clients: clients.clients })
setSelectedId(prev => { setSelectedId(prev => {
const list = flowScope === "users" ? clients.clients : exporters.exporters const list = flowScope === "users" ? clients.clients : exporters.exporters
return list.some(x => x.id === prev) ? prev : (list[0]?.id ?? "") return list.some(x => x.id === prev) ? prev : (list[0]?.id ?? "")
}) })
}).catch(e => { }
void loadFlowCards().catch(e => {
if (!cancelled) setFlowResult({ key: flowKey, error: e instanceof Error ? e.message : "Не удалось загрузить потоки" }) if (!cancelled) setFlowResult({ key: flowKey, error: e instanceof Error ? e.message : "Не удалось загрузить потоки" })
}) })
return () => { cancelled = true } return () => { cancelled = true }
+10 -1
View File
@@ -1,7 +1,16 @@
@import "tailwindcss"; @import "tailwindcss" source(none);
@import "tw-animate-css"; @import "tw-animate-css";
@import "shadcn/tailwind.css"; @import "shadcn/tailwind.css";
/* Scan application sources only; local test archives and generated bundles
must not become utility classes in the production stylesheet. */
@source "./";
@source "../components";
@source "../entities";
@source "../hooks";
@source "../lib";
@source "../shared";
/* SVG focus follows the object shape, never the text/foreignObject bounding box. */ /* SVG focus follows the object shape, never the text/foreignObject bounding box. */
.network-map-focusable { .network-map-focusable {
outline: none; outline: none;
@@ -0,0 +1,13 @@
BEGIN;
CREATE TABLE public.mcp_operation_replay_tombstones (
operation_id uuid PRIMARY KEY,
plan_id uuid NOT NULL UNIQUE,
connection_id uuid NOT NULL REFERENCES public.mcp_connections(id),
idempotency_digest text NOT NULL CHECK (idempotency_digest ~ '^[0-9a-f]{64}$'),
action_id text NOT NULL CHECK (length(action_id) BETWEEN 1 AND 200),
target_server_ids bigint[] NOT NULL CHECK (array_position(target_server_ids, NULL) IS NULL),
target_global boolean NOT NULL,
UNIQUE (connection_id,idempotency_digest)
);
INSERT INTO public.schema_migrations(id) VALUES ('0045_mcp_operation_replay_tombstones') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,186 @@
-- Inactive poll-history V1-to-V2 retirement. Application owner authenticates
-- both ciphertext versions; SQL independently fences identity and lifetime.
BEGIN;
LOCK TABLE public.server_poll_archives,public.server_poll_storage_admissions IN ACCESS EXCLUSIVE MODE;
ALTER TABLE public.server_poll_archives DROP CONSTRAINT server_poll_archives_version_check;
ALTER TABLE public.server_poll_archives ADD CONSTRAINT server_poll_archives_version_check
CHECK(version=1 OR (version=2 AND decoded_bytes<=24576 AND compressed_bytes<=24576 AND ciphertext_bytes<=33000));
CREATE OR REPLACE FUNCTION public.server_poll_archive_immutable() RETURNS trigger
LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='DELETE' AND public.server_poll_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
IF TG_OP='UPDATE' AND OLD.version=1 AND NEW.version=2
AND current_setting('mm.poll_retire_receipt',true)=OLD.receipt_id::text
AND NEW.ciphertext_bytes<=OLD.ciphertext_bytes
AND NEW.ciphertext ~ '^[A-Za-z0-9+/]+={0,2}$'
AND replace(encode(decode(NEW.ciphertext,'base64'),'base64'),E'\n','')=NEW.ciphertext
AND octet_length(decode(NEW.ciphertext,'base64'))=NEW.compressed_bytes+28
AND (to_jsonb(OLD)-ARRAY['version','source_digest','source_sha256','decoded_bytes','compressed_bytes','ciphertext','ciphertext_bytes','archive_proof'])=
(to_jsonb(NEW)-ARRAY['version','source_digest','source_sha256','decoded_bytes','compressed_bytes','ciphertext','ciphertext_bytes','archive_proof'])
AND EXISTS(
SELECT 1 FROM public.server_poll_storage_admissions a JOIN public.mcp_operations o ON o.id=a.execution_id AND o.connection_id=a.connection_id
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=0
JOIN public.mcp_operation_replay_tombstones t ON t.operation_id=o.id AND t.plan_id=p.id AND t.connection_id=o.connection_id
JOIN public.mcp_connections c ON c.id=o.connection_id AND c.owner_id=a.owner_id
WHERE a.receipt_id=OLD.receipt_id AND a.identity_kind='mcp' AND a.storage_state='compacted'
AND a.execution_id=OLD.execution_id AND a.connection_id=OLD.connection_id AND a.owner_id=OLD.owner_id
AND a.binding_proof=OLD.binding_proof AND a.context_proof=OLD.context_proof AND a.archive_bytes=OLD.ciphertext_bytes
AND a.original_activity_at=OLD.original_activity_at AND a.authority_expires_at=OLD.original_expires_at
AND o.state='applied' AND o.verification='passed' AND o.error_code IS NULL AND isfinite(o.finished_at)
AND o.finished_at<transaction_timestamp()-interval '180 days'
AND p.state='completed' AND p.operation_id='servers.poll' AND p.operation_version=1 AND p.approved_by IS NOT NULL
AND s.state='verified' AND s.finished_at IS NOT NULL AND t.action_id=p.operation_id
AND t.target_global AND cardinality(t.target_server_ids)>0
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'poll archive immutable outside exact retirement or original lifetime GC';
END $$;
CREATE FUNCTION public.server_poll_retirement_final_guard() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
IF NOT EXISTS(SELECT 1 FROM public.mcp_operation_replay_tombstones t
WHERE t.operation_id=NEW.execution_id AND t.connection_id=NEW.connection_id AND t.action_id='servers.poll'
AND t.target_global AND NOT EXISTS(SELECT 1 FROM public.mcp_plans p WHERE p.id=t.plan_id))
OR EXISTS(SELECT 1 FROM public.mcp_operations o WHERE o.id=NEW.execution_id)
OR NOT public.server_poll_storage_accounting_valid()
THEN RAISE EXCEPTION 'poll retirement is incomplete'; END IF;
RETURN NULL;
END $$;
CREATE CONSTRAINT TRIGGER server_poll_retirement_final_guard AFTER UPDATE ON public.server_poll_archives
DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN(OLD.version=1 AND NEW.version=2)
EXECUTE FUNCTION public.server_poll_retirement_final_guard();
CREATE OR REPLACE FUNCTION server_poll_storage_admission_guard() RETURNS trigger LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
DECLARE retention integer; charged integer; r public.server_poll_archives%ROWTYPE;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
IF TG_OP='DELETE' THEN
IF NOT public.server_poll_storage_gc_allowed(OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_executions WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_intents WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_acks WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_receipts WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_quarantines WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_archives WHERE receipt_id=OLD.receipt_id) THEN RAISE EXCEPTION 'poll lifetime or children prevent admission deletion'; END IF;
UPDATE public.server_poll_storage_usage SET admissions=admissions-CASE WHEN OLD.storage_state='full' THEN 1 ELSE 0 END,
compacted_admissions=compacted_admissions-CASE WHEN OLD.storage_state='compacted' THEN 1 ELSE 0 END,
archive_bytes=archive_bytes-COALESCE(OLD.archive_bytes,0),
reserved_rows=reserved_rows-CASE WHEN OLD.storage_state='full' THEN 4019 ELSE 2 END,
reserved_bytes=reserved_bytes-CASE WHEN OLD.storage_state='full' THEN 8421376 ELSE 32768+OLD.archive_bytes END
WHERE scope IN('global','owner:'||OLD.owner_id);
GET DIAGNOSTICS charged=ROW_COUNT; IF charged<>2 THEN RAISE EXCEPTION 'poll quota refund mismatch'; END IF; RETURN OLD;
END IF;
IF TG_OP='UPDATE' THEN
IF OLD.storage_state='compacted' AND NEW.storage_state='compacted'
AND current_setting('mm.poll_retire_receipt',true)=OLD.receipt_id::text
AND (to_jsonb(OLD)-'archive_bytes')=(to_jsonb(NEW)-'archive_bytes') THEN
SELECT * INTO r FROM public.server_poll_archives WHERE receipt_id=OLD.receipt_id AND version=2;
IF NOT FOUND OR r.ciphertext_bytes<>NEW.archive_bytes OR NEW.archive_bytes>OLD.archive_bytes
OR r.execution_id<>OLD.execution_id OR r.connection_id<>OLD.connection_id OR r.owner_id<>OLD.owner_id
THEN RAISE EXCEPTION 'retired poll charge source mismatch'; END IF;
UPDATE public.server_poll_storage_usage
SET archive_bytes=archive_bytes-OLD.archive_bytes+NEW.archive_bytes,
reserved_bytes=reserved_bytes-OLD.archive_bytes+NEW.archive_bytes
WHERE scope IN ('global','owner:'||OLD.owner_id) AND compacted_admissions>=1 AND archive_bytes>=OLD.archive_bytes;
GET DIAGNOSTICS charged=ROW_COUNT;
IF charged<>2 THEN RAISE EXCEPTION 'retired poll charge adjustment mismatch'; END IF;
RETURN NEW;
END IF;
-- This durable one-shot marker is written and ACKed in its OWN transaction
-- before archive construction. A committed/unknown claim never resets and
-- the worker never retries destructive work against an already claimed full
-- source. Claim time is maintenance metadata, not renewed execution activity.
IF OLD.storage_state='full' AND NEW.storage_state='full' AND OLD.compaction_claim IS NULL AND OLD.compaction_claimed_at IS NULL AND OLD.compaction_claim_xid IS NULL
AND NEW.compaction_claim IS NOT NULL AND NEW.compaction_claim_xid IS NULL
AND(to_jsonb(OLD)-ARRAY['compaction_claim','compaction_claimed_at','compaction_claim_xid'])=(to_jsonb(NEW)-ARRAY['compaction_claim','compaction_claimed_at','compaction_claim_xid'])
AND public.server_poll_storage_compact_allowed(OLD.receipt_id)
THEN NEW.compaction_claimed_at:=clock_timestamp(); NEW.compaction_claim_xid:=pg_current_xact_id(); RETURN NEW; END IF;
IF(to_jsonb(OLD)-ARRAY['claim_token','claim_attempted_at','used_rows','used_bytes','storage_state','compact_at','archive_bytes','original_activity_at']) IS DISTINCT FROM
(to_jsonb(NEW)-ARRAY['claim_token','claim_attempted_at','used_rows','used_bytes','storage_state','compact_at','archive_bytes','original_activity_at']) THEN RAISE EXCEPTION 'poll admission identity is immutable'; END IF;
IF OLD.storage_state<>'full' THEN RAISE EXCEPTION 'compacted poll identity cannot reopen or change'; END IF;
IF NEW.storage_state='compacted' THEN
SELECT * INTO r FROM public.server_poll_archives WHERE receipt_id=OLD.receipt_id AND compaction_xid=pg_current_xact_id();
IF NOT FOUND OR NEW.used_rows<>OLD.used_rows OR NEW.used_bytes<>OLD.used_bytes OR NEW.claim_token IS DISTINCT FROM OLD.claim_token
OR NEW.claim_attempted_at IS DISTINCT FROM OLD.claim_attempted_at OR NEW.archive_bytes<>r.ciphertext_bytes OR NEW.original_activity_at<>r.original_activity_at
OR EXISTS(SELECT 1 FROM public.server_poll_executions WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_intents WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_acks WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_receipts WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_quarantines WHERE receipt_id=OLD.receipt_id)
THEN RAISE EXCEPTION 'poll compaction requires the complete current-transaction archive'; END IF;
NEW.compact_at:=r.created_at;
UPDATE public.server_poll_storage_usage SET admissions=admissions-1,compacted_admissions=compacted_admissions+1,
archive_bytes=archive_bytes+r.ciphertext_bytes,reserved_rows=reserved_rows-4019+2,reserved_bytes=reserved_bytes-8421376+32768+r.ciphertext_bytes
WHERE scope IN('global','owner:'||OLD.owner_id) AND admissions>=1;
GET DIAGNOSTICS charged=ROW_COUNT; IF charged<>2 THEN RAISE EXCEPTION 'poll compaction charge mismatch'; END IF;
RETURN NEW;
END IF;
IF NEW.compact_at IS NOT NULL OR NEW.archive_bytes IS NOT NULL OR NEW.original_activity_at IS NOT NULL THEN RAISE EXCEPTION 'full poll cannot carry archive metadata'; END IF;
IF OLD.claim_token IS NOT NULL AND NEW.claim_token IS NULL AND OLD.claim_attempted_at IS NULL AND NEW.claim_attempted_at IS NOT NULL
AND NEW.used_rows=OLD.used_rows AND NEW.used_bytes=OLD.used_bytes AND OLD.authority_expires_at>clock_timestamp()
THEN NEW.claim_attempted_at:=clock_timestamp(); RETURN NEW; END IF;
IF OLD.claim_token IS NOT DISTINCT FROM NEW.claim_token AND OLD.claim_attempted_at IS NOT DISTINCT FROM NEW.claim_attempted_at
AND NEW.used_rows=OLD.used_rows+1 AND NEW.used_bytes>OLD.used_bytes THEN RETURN NEW; END IF;
RAISE EXCEPTION 'poll admission cannot refresh, reopen or refund itself';
END IF;
IF NEW.storage_state<>'full' OR NEW.compact_at IS NOT NULL OR NEW.archive_bytes IS NOT NULL OR NEW.original_activity_at IS NOT NULL
OR NEW.compaction_claim IS NOT NULL OR NEW.compaction_claimed_at IS NOT NULL OR NEW.compaction_claim_xid IS NOT NULL
OR NOT public.server_poll_storage_accounting_valid() THEN RAISE EXCEPTION 'new poll requires full reservation and exact ledger'; END IF;
SELECT requests_retention_days INTO retention FROM public.mcp_settings WHERE id=1;
IF retention IS NULL OR retention NOT BETWEEN 1 AND 365 OR NEW.authority_issued_at>clock_timestamp() OR NEW.authority_expires_at<=clock_timestamp()
OR NEW.claim_token IS NULL OR NEW.claim_attempted_at IS NOT NULL OR NEW.used_rows<>0 OR NEW.used_bytes<>0 THEN RAISE EXCEPTION 'new poll admission needs original live authority'; END IF;
IF EXISTS(SELECT 1 FROM public.server_poll_storage_admissions WHERE receipt_id=NEW.receipt_id OR(identity_kind=NEW.identity_kind AND execution_id=NEW.execution_id)) THEN RAISE EXCEPTION 'poll identity already spent'; END IF;
NEW.created_at:=clock_timestamp(); NEW.retention_days:=retention; NEW.safety_ms:=60000;
NEW.gc_not_before:=NEW.authority_expires_at+make_interval(days=>retention)+interval '60 seconds';
INSERT INTO public.server_poll_storage_usage(scope) VALUES('owner:'||NEW.owner_id) ON CONFLICT DO NOTHING;
IF(SELECT count(*) FROM public.server_poll_storage_usage WHERE scope IN('global','owner:'||NEW.owner_id)
AND admissions<CASE WHEN scope='global' THEN 128 ELSE 8 END
AND reserved_rows<=CASE WHEN scope='global' THEN 514432 ELSE 32152 END-4019
AND reserved_bytes<=CASE WHEN scope='global' THEN 1077936128 ELSE 67371008 END-8421376)<>2 THEN RAISE EXCEPTION 'poll admission quota exhausted'; END IF;
UPDATE public.server_poll_storage_usage SET admissions=admissions+1,reserved_rows=reserved_rows+4019,reserved_bytes=reserved_bytes+8421376 WHERE scope IN('global','owner:'||NEW.owner_id);
GET DIAGNOSTICS charged=ROW_COUNT; IF charged<>2 THEN RAISE EXCEPTION 'poll admission quota unavailable'; END IF;
RETURN NEW;
END $$;
CREATE OR REPLACE FUNCTION server_poll_storage_gc_allowed(id uuid) RETURNS boolean LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
DECLARE a public.server_poll_storage_admissions%ROWTYPE; retention integer; activity timestamptz; terminal_at timestamptz;
BEGIN
IF current_setting('mm.poll_storage_gc',true) IS DISTINCT FROM 'on' THEN RETURN false; END IF;
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
SELECT * INTO a FROM public.server_poll_storage_admissions WHERE receipt_id=id FOR UPDATE;
IF NOT FOUND OR a.authority_expires_at>=clock_timestamp() OR a.gc_not_before>=clock_timestamp() THEN RETURN false; END IF;
SELECT requests_retention_days INTO retention FROM public.mcp_settings WHERE mcp_settings.id=1 FOR SHARE;
IF retention IS NULL OR retention NOT BETWEEN 1 AND 365 THEN RETURN false; END IF;
terminal_at:=a.created_at;
IF a.identity_kind='mcp' THEN
IF NOT pg_try_advisory_xact_lock(hashtextextended('mcp-operation:'||a.execution_id::text,0)) THEN RETURN false; END IF;
BEGIN
SELECT GREATEST(o.finished_at,p.expires_at) INTO terminal_at FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id JOIN public.mcp_connections c ON c.id=o.connection_id
WHERE o.id=a.execution_id AND o.connection_id=a.connection_id AND c.owner_id=a.owner_id AND p.operation_id='servers.poll'
AND o.state IN('applied','failed','unknown','rejected','partial','cancelled') AND o.finished_at IS NOT NULL FOR UPDATE OF o,p NOWAIT;
IF NOT FOUND THEN
IF a.storage_state<>'compacted' OR EXISTS(SELECT 1 FROM public.mcp_operations o WHERE o.id=a.execution_id) OR NOT EXISTS (
SELECT 1 FROM public.mcp_operation_replay_tombstones t JOIN public.mcp_connections c ON c.id=t.connection_id
WHERE t.operation_id=a.execution_id AND t.connection_id=a.connection_id AND c.owner_id=a.owner_id
AND t.action_id='servers.poll' AND t.target_global AND cardinality(t.target_server_ids)>0
AND NOT EXISTS(SELECT 1 FROM public.mcp_plans WHERE mcp_plans.id=t.plan_id)
) OR EXISTS(SELECT 1 FROM public.server_poll_archives r WHERE r.receipt_id=id AND
(r.version<>2 OR r.execution_id<>a.execution_id OR r.connection_id<>a.connection_id OR r.owner_id<>a.owner_id
OR r.original_activity_at IS DISTINCT FROM a.original_activity_at OR r.original_expires_at<>a.authority_expires_at
OR r.binding_proof<>a.binding_proof OR r.context_proof<>a.context_proof OR r.ciphertext_bytes<>a.archive_bytes))
THEN RETURN false; END IF;
terminal_at:=a.original_activity_at;
END IF;
IF terminal_at IS NULL THEN RETURN false; END IF;
EXCEPTION WHEN lock_not_available THEN RETURN false; END;
END IF;
IF a.storage_state='compacted' THEN activity:=GREATEST(a.original_activity_at,a.authority_expires_at,terminal_at);
ELSE
SELECT GREATEST(a.authority_expires_at,a.created_at,COALESCE(a.claim_attempted_at,a.created_at),terminal_at,
COALESCE((SELECT created_at FROM public.server_poll_executions WHERE receipt_id=id),a.created_at),
COALESCE((SELECT max(created_at) FROM public.server_poll_intents WHERE receipt_id=id),a.created_at),
COALESCE((SELECT max(created_at) FROM public.server_poll_acks WHERE receipt_id=id),a.created_at),
COALESCE((SELECT created_at FROM public.server_poll_receipts WHERE receipt_id=id),a.created_at),
COALESCE((SELECT created_at FROM public.server_poll_quarantines WHERE receipt_id=id),a.created_at)) INTO activity;
END IF;
RETURN isfinite(activity) AND activity+make_interval(days=>GREATEST(a.retention_days,retention))+a.safety_ms*interval '1 millisecond'<clock_timestamp();
END $$;
INSERT INTO public.schema_migrations(id) VALUES ('0046_server_poll_operation_retirement') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,40 @@
BEGIN;
CREATE TABLE public.server_poll_retirement_claims (
receipt_id uuid PRIMARY KEY REFERENCES public.server_poll_storage_admissions(receipt_id) ON DELETE CASCADE,
old_archive_proof text NOT NULL CHECK(old_archive_proof ~ '^[0-9a-f]{64}$'),
claim_token uuid NOT NULL,
claim_xid xid8 NOT NULL DEFAULT pg_current_xact_id()
);
CREATE FUNCTION public.server_poll_retirement_claim_immutable() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.server_poll_archives r WHERE r.receipt_id=NEW.receipt_id
AND r.version=1 AND r.archive_proof=NEW.old_archive_proof
) THEN RETURN NEW; END IF;
IF TG_OP='DELETE' AND pg_trigger_depth()>1 AND NOT EXISTS(
SELECT 1 FROM public.server_poll_storage_admissions a WHERE a.receipt_id=OLD.receipt_id
) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'poll retirement claim cannot be replaced or reopened';
END $$;
CREATE TRIGGER server_poll_retirement_claim_immutable
BEFORE INSERT OR UPDATE OR DELETE ON public.server_poll_retirement_claims
FOR EACH ROW EXECUTE FUNCTION public.server_poll_retirement_claim_immutable();
CREATE TRIGGER server_poll_retirement_claim_no_truncate
BEFORE TRUNCATE ON public.server_poll_retirement_claims
FOR EACH STATEMENT EXECUTE FUNCTION public.server_poll_retirement_claim_immutable();
CREATE FUNCTION public.server_poll_retirement_requires_claim() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
IF NOT EXISTS(SELECT 1 FROM public.server_poll_retirement_claims c
WHERE c.receipt_id=OLD.receipt_id AND c.old_archive_proof=OLD.archive_proof
AND c.claim_token::text=current_setting('mm.poll_retire_claim',true)
AND c.claim_xid<>pg_current_xact_id())
THEN RAISE EXCEPTION 'poll retirement requires prior exact claim'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER server_poll_retirement_requires_claim BEFORE UPDATE ON public.server_poll_archives
FOR EACH ROW WHEN(OLD.version=1 AND NEW.version=2)
EXECUTE FUNCTION public.server_poll_retirement_requires_claim();
INSERT INTO public.schema_migrations(id) VALUES ('0047_server_poll_retirement_claims') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,27 @@
BEGIN;
CREATE TABLE public.mcp_ordinary_retirement_claims (
operation_id uuid PRIMARY KEY,
plan_id uuid NOT NULL UNIQUE,
connection_id uuid NOT NULL REFERENCES public.mcp_connections(id),
source_proof text NOT NULL CHECK(source_proof ~ '^[0-9a-f]{64}$'),
claim_token uuid NOT NULL,
claim_xid xid8 NOT NULL DEFAULT pg_current_xact_id()
);
CREATE FUNCTION public.mcp_ordinary_retirement_claim_immutable() RETURNS trigger
LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.mcp_operations o JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
WHERE o.id=NEW.operation_id AND o.plan_id=NEW.plan_id AND o.connection_id=NEW.connection_id
AND o.state='applied' AND o.verification='passed' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND p.operation_id='logs.settings.update' AND p.operation_version=1 AND p.state='completed'
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'ordinary operation retirement claim cannot be replaced or reopened';
END $$;
CREATE TRIGGER mcp_ordinary_retirement_claim_immutable BEFORE INSERT OR UPDATE OR DELETE
ON public.mcp_ordinary_retirement_claims FOR EACH ROW EXECUTE FUNCTION public.mcp_ordinary_retirement_claim_immutable();
CREATE TRIGGER mcp_ordinary_retirement_claim_no_truncate BEFORE TRUNCATE
ON public.mcp_ordinary_retirement_claims FOR EACH STATEMENT EXECUTE FUNCTION public.mcp_ordinary_retirement_claim_immutable();
INSERT INTO public.schema_migrations(id) VALUES ('0048_mcp_ordinary_retirement_claims') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,18 @@
BEGIN;
CREATE OR REPLACE FUNCTION public.mcp_ordinary_retirement_claim_immutable() RETURNS trigger
LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.mcp_operations o JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
WHERE o.id=NEW.operation_id AND o.plan_id=NEW.plan_id AND o.connection_id=NEW.connection_id
AND o.state='applied' AND o.verification='passed' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND (p.operation_id,p.operation_version) IN (
('logs.settings.update',1),('traffic.settings.update',2),('uptime.settings.update',2),
('servers.api-ping.settings.update',2),('internet-path.settings.update',1)
) AND p.state='completed'
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'ordinary operation retirement claim cannot be replaced or reopened';
END $$;
INSERT INTO public.schema_migrations(id) VALUES ('0049_mcp_ordinary_telemetry_retirement') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,22 @@
BEGIN;
CREATE TABLE public.server_probe_retired_sources (
receipt_id uuid PRIMARY KEY REFERENCES public.server_probe_storage_admissions(receipt_id),
operation_id uuid NOT NULL UNIQUE REFERENCES public.mcp_operation_replay_tombstones(operation_id),
source jsonb NOT NULL CHECK(jsonb_typeof(source)='object' AND octet_length(source::text) BETWEEN 1 AND 8192),
proof text NOT NULL CHECK(proof ~ '^[0-9a-f]{64}$'),
CHECK((source->>'version') IS NOT DISTINCT FROM '1'
AND (source->>'receiptId') IS NOT DISTINCT FROM receipt_id::text
AND (source->>'executionId') IS NOT DISTINCT FROM operation_id::text)
);
CREATE FUNCTION public.server_probe_retired_source_immutable() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
RAISE EXCEPTION 'retired diagnostic source is immutable';
END $$;
CREATE TRIGGER server_probe_retired_source_immutable
BEFORE UPDATE OR DELETE ON public.server_probe_retired_sources
FOR EACH ROW EXECUTE FUNCTION public.server_probe_retired_source_immutable();
CREATE TRIGGER server_probe_retired_source_no_truncate BEFORE TRUNCATE ON public.server_probe_retired_sources
FOR EACH STATEMENT EXECUTE FUNCTION public.server_probe_retired_source_immutable();
INSERT INTO public.schema_migrations(id) VALUES ('0050_server_probe_retired_sources') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,52 @@
BEGIN;
CREATE TABLE public.server_probe_retirement_claims (
receipt_id uuid PRIMARY KEY,
operation_id uuid NOT NULL UNIQUE,
plan_id uuid NOT NULL UNIQUE,
connection_id uuid NOT NULL,
source_proof text NOT NULL CHECK(source_proof ~ '^[a-f0-9]{64}$'),
claim_token uuid NOT NULL,
claim_xid xid8 NOT NULL DEFAULT pg_current_xact_id()
);
CREATE FUNCTION public.server_probe_retirement_claim_guard() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public SET TimeZone='UTC' AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.server_probe_storage_admissions a
JOIN public.mcp_operations o ON o.id=a.execution_id
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_connections c ON c.id=o.connection_id AND c.owner_id=a.owner_id
WHERE a.receipt_id=NEW.receipt_id AND a.identity_kind='mcp' AND a.claim_token IS NULL
AND a.claim_attempted_at IS NOT NULL AND a.cancelled_at IS NULL AND a.purged_at IS NULL
AND o.id=NEW.operation_id AND p.id=NEW.plan_id AND c.id=NEW.connection_id
AND o.state='applied' AND o.verification='inconclusive' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND p.operation_id='servers.probes.run' AND p.operation_version=1 AND p.state='completed'
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'diagnostic retirement claim cannot be replaced or reopened';
END $$;
CREATE TRIGGER server_probe_retirement_claim_guard BEFORE INSERT OR UPDATE OR DELETE
ON public.server_probe_retirement_claims FOR EACH ROW EXECUTE FUNCTION public.server_probe_retirement_claim_guard();
CREATE TRIGGER server_probe_retirement_claim_no_truncate BEFORE TRUNCATE
ON public.server_probe_retirement_claims FOR EACH STATEMENT EXECUTE FUNCTION public.server_probe_retirement_claim_guard();
CREATE FUNCTION public.server_probe_retired_source_insert_guard() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
IF EXISTS(
SELECT 1 FROM public.server_probe_retirement_claims r
JOIN public.mcp_operation_replay_tombstones t ON t.operation_id=r.operation_id AND t.plan_id=r.plan_id
AND t.connection_id=r.connection_id AND t.action_id='servers.probes.run' AND NOT t.target_global
JOIN public.mcp_operations o ON o.id=r.operation_id AND o.plan_id=r.plan_id AND o.connection_id=r.connection_id
JOIN public.server_probe_storage_admissions a ON a.receipt_id=r.receipt_id AND a.execution_id=r.operation_id
WHERE r.receipt_id=NEW.receipt_id AND r.operation_id=NEW.operation_id AND r.source_proof=NEW.proof
AND r.claim_xid<>pg_current_xact_id() AND r.claim_token::text=current_setting('mm.probe_retirement_claim',true)
AND NEW.source->>'planId'=r.plan_id::text AND NEW.source->>'connectionId'=r.connection_id::text
AND NEW.source->>'ownerId'=a.owner_id AND NEW.source->>'bindingProof'=a.binding_proof
AND NEW.source->'serverIds'=to_jsonb(t.target_server_ids)
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'diagnostic archive requires an exact committed retirement claim';
END $$;
CREATE TRIGGER server_probe_retired_source_insert_guard BEFORE INSERT ON public.server_probe_retired_sources
FOR EACH ROW EXECUTE FUNCTION public.server_probe_retired_source_insert_guard();
INSERT INTO public.schema_migrations(id) VALUES ('0051_server_probe_retirement_claims') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,96 @@
BEGIN;
ALTER TABLE public.server_probe_retired_sources
DROP CONSTRAINT server_probe_retired_sources_receipt_id_fkey,
ADD CONSTRAINT server_probe_retired_sources_receipt_id_fkey
FOREIGN KEY(receipt_id) REFERENCES public.server_probe_storage_admissions(receipt_id) ON DELETE CASCADE;
CREATE FUNCTION public.server_probe_retired_gc_allowed(target_receipt uuid,current_retention integer) RETURNS boolean
LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
DECLARE activity timestamptz; retained_days integer; safety integer;
BEGIN
IF current_setting('mm.probe_storage_gc',true) IS DISTINCT FROM 'on'
OR current_setting('mm.probe_retired_gc_receipt',true) IS DISTINCT FROM target_receipt::text THEN RETURN false; END IF;
SELECT (r.source->>'activityAt')::timestamptz,a.retention_days,a.safety_ms
INTO activity,retained_days,safety
FROM public.server_probe_retired_sources r
JOIN public.server_probe_storage_admissions a ON a.receipt_id=r.receipt_id AND a.execution_id=r.operation_id
JOIN public.mcp_operation_replay_tombstones t ON t.operation_id=r.operation_id
JOIN public.mcp_connections c ON c.id=t.connection_id AND c.owner_id=a.owner_id
WHERE r.receipt_id=target_receipt AND r.proof=current_setting('mm.probe_retired_gc_proof',true)
AND a.identity_kind='mcp' AND a.cancelled_at IS NULL AND a.claim_token IS NULL AND isfinite(a.claim_attempted_at)
AND t.action_id='servers.probes.run' AND NOT t.target_global
AND r.source->>'planId'=t.plan_id::text AND r.source->>'connectionId'=c.id::text
AND r.source->'serverIds'=to_jsonb(t.target_server_ids) AND r.source->>'ownerId'=a.owner_id
AND r.source->>'bindingProof'=a.binding_proof
AND (r.source->>'authorityExpiresAt')::timestamptz=a.authority_expires_at
AND (r.source->>'gcNotBefore')::timestamptz=a.gc_not_before
AND (r.source->>'retentionDays')::int=a.retention_days AND (r.source->>'safetyMs')::int=a.safety_ms
AND isfinite(a.authority_expires_at) AND isfinite(a.gc_not_before)
AND a.authority_expires_at<clock_timestamp() AND a.gc_not_before<clock_timestamp()
AND NOT EXISTS(SELECT 1 FROM public.mcp_operations WHERE id=t.operation_id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_plans WHERE id=t.plan_id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_operation_steps WHERE operation_id=t.operation_id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_operation_checkpoints WHERE operation_id=t.operation_id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_diagnostic_cancellations WHERE operation_id=t.operation_id)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_execution_quarantines q
WHERE q.receipt_id=r.receipt_id OR(q.identity_kind='mcp' AND q.execution_id=r.operation_id));
IF NOT FOUND OR activity IS NULL OR NOT isfinite(activity)
OR current_retention IS NULL OR current_retention NOT BETWEEN 1 AND 365 THEN RETURN false; END IF;
IF EXISTS(SELECT 1 FROM public.server_probe_executions j WHERE j.receipt_id=target_receipt
AND (j.finished_at IS NULL OR j.finished_at>activity))
OR EXISTS(SELECT 1 FROM public.server_probe_execution_stages s WHERE s.receipt_id=target_receipt
AND GREATEST(s.created_at,COALESCE(s.received_at,s.created_at))>activity)
THEN RETURN false; END IF;
RETURN activity+make_interval(days=>GREATEST(retained_days,current_retention))
+GREATEST(safety,60000)*interval '1 millisecond'<clock_timestamp();
END $$;
CREATE OR REPLACE FUNCTION public.server_probe_retired_source_immutable() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='DELETE' THEN
IF current_setting('mm.probe_storage_gc',true)='on'
AND current_setting('mm.probe_retired_gc_receipt',true)=OLD.receipt_id::text
AND current_setting('mm.probe_retired_gc_proof',true)=OLD.proof
AND NOT EXISTS(SELECT 1 FROM public.server_probe_storage_admissions WHERE receipt_id=OLD.receipt_id)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_executions WHERE receipt_id=OLD.receipt_id)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_execution_payloads WHERE receipt_id=OLD.receipt_id)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_execution_stages WHERE receipt_id=OLD.receipt_id)
THEN RETURN OLD; END IF;
END IF;
RAISE EXCEPTION 'retired diagnostic source is immutable';
END $$;
CREATE OR REPLACE FUNCTION public.server_probe_storage_gc_allowed(id uuid) RETURNS boolean
LANGUAGE plpgsql SET TimeZone='UTC' AS $$
DECLARE a server_probe_storage_admissions%ROWTYPE; activity timestamptz; current_retention integer; terminal_at timestamptz;
BEGIN
IF current_setting('mm.probe_storage_gc',true) IS DISTINCT FROM 'on' THEN RETURN false; END IF;
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
SELECT * INTO a FROM server_probe_storage_admissions WHERE receipt_id=id FOR UPDATE;
IF NOT FOUND OR a.gc_not_before>=clock_timestamp() OR a.authority_expires_at>=clock_timestamp() THEN RETURN false; END IF;
SELECT settings.requests_retention_days INTO current_retention FROM mcp_settings settings WHERE settings.id=1;
IF current_retention IS NULL OR current_retention NOT BETWEEN 1 AND 365 THEN RETURN false; END IF;
IF EXISTS(SELECT 1 FROM public.server_probe_retired_sources WHERE receipt_id=id) THEN
RETURN public.server_probe_retired_gc_allowed(id,current_retention);
END IF;
terminal_at:=a.created_at;
IF a.identity_kind='mcp' THEN
SELECT GREATEST(o.finished_at,p.expires_at,COALESCE(cancellation.created_at,o.finished_at)) INTO terminal_at
FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN mcp_connections owner_connection ON owner_connection.id=o.connection_id
LEFT JOIN mcp_diagnostic_cancellations cancellation ON cancellation.operation_id=o.id
WHERE o.id=a.execution_id AND owner_connection.owner_id=a.owner_id AND p.operation_id='servers.probes.run'
AND o.state IN ('applied','failed','unknown','rejected','partial','cancelled') AND o.finished_at IS NOT NULL
AND (cancellation.operation_id IS NULL OR(cancellation.connection_id=o.connection_id AND cancellation.actor_id=a.owner_id))
FOR UPDATE OF o,p,owner_connection;
IF NOT FOUND OR terminal_at IS NULL THEN RETURN false; END IF;
END IF;
SELECT GREATEST(a.created_at,a.authority_expires_at,terminal_at,COALESCE(a.cancelled_at,a.created_at),
COALESCE((SELECT finished_at FROM server_probe_executions WHERE receipt_id=id),a.created_at),
COALESCE((SELECT created_at FROM server_probe_execution_quarantines WHERE receipt_id=id),a.created_at),
COALESCE((SELECT max(GREATEST(created_at,COALESCE(received_at,created_at))) FROM server_probe_execution_stages WHERE receipt_id=id),a.created_at)) INTO activity;
RETURN activity+make_interval(days=>GREATEST(a.retention_days,current_retention))+a.safety_ms*interval '1 millisecond'<clock_timestamp();
END $$;
INSERT INTO public.schema_migrations(id) VALUES ('0052_server_probe_retired_gc') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,74 @@
BEGIN;
CREATE TABLE public.server_probe_terminal_proofs (
operation_id uuid PRIMARY KEY REFERENCES public.mcp_operations(id),
receipt_id uuid NOT NULL UNIQUE,
source jsonb NOT NULL CHECK(jsonb_typeof(source)='object' AND octet_length(source::text) BETWEEN 1 AND 8192),
source_proof text NOT NULL CHECK(source_proof ~ '^[a-f0-9]{64}$'),
idempotency_digest text NOT NULL CHECK(idempotency_digest ~ '^[a-f0-9]{64}$'),
capture_proof text NOT NULL CHECK(capture_proof ~ '^[a-f0-9]{64}$'),
CHECK((source->>'version') IS NOT DISTINCT FROM '1'
AND (source->>'executionId') IS NOT DISTINCT FROM operation_id::text
AND (source->>'receiptId') IS NOT DISTINCT FROM receipt_id::text)
);
CREATE FUNCTION public.server_probe_terminal_proof_guard() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='INSERT' THEN
IF EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_connections c ON c.id=o.connection_id
JOIN public.server_probe_storage_admissions a ON a.execution_id=o.id AND a.identity_kind='mcp' AND a.owner_id=c.owner_id
JOIN public.server_probe_executions j ON j.receipt_id=a.receipt_id
JOIN public.server_probe_execution_payloads b ON b.receipt_id=a.receipt_id AND b.receipt_proof=j.receipt_proof
WHERE o.id=NEW.operation_id AND a.receipt_id=NEW.receipt_id AND a.purged_at IS NULL AND a.cancelled_at IS NULL
AND a.claim_token IS NULL AND a.claim_attempted_at IS NOT NULL
AND o.state='applied' AND o.verification='inconclusive' AND o.error_code IS NULL
AND p.state='completed' AND p.operation_id='servers.probes.run' AND p.operation_version=1
AND j.state='completed' AND j.execution_id=o.id AND j.mcp_connection_id=c.id AND j.actor_id=c.owner_id
AND NEW.source->>'connectionId'=c.id::text AND NEW.source->>'planId'=p.id::text
AND NEW.source->>'ownerId'=a.owner_id AND NEW.source->>'bindingProof'=a.binding_proof
AND NEW.source->>'receiptProof'=b.receipt_proof
AND NEW.source->>'ciphertextSha256'=encode(sha256(convert_to(b.receipt_ciphertext,'UTF8')),'hex')
) THEN RETURN NEW; END IF;
ELSIF TG_OP='DELETE' THEN
IF EXISTS(
SELECT 1 FROM public.server_probe_retirement_claims r
JOIN public.mcp_operation_replay_tombstones t ON t.operation_id=r.operation_id AND t.plan_id=r.plan_id AND t.connection_id=r.connection_id
WHERE r.operation_id=OLD.operation_id AND r.receipt_id=OLD.receipt_id AND r.source_proof=OLD.source_proof
AND r.claim_xid<>pg_current_xact_id() AND r.claim_token::text=current_setting('mm.probe_retirement_claim',true)
AND t.action_id='servers.probes.run' AND NOT t.target_global AND t.idempotency_digest=OLD.idempotency_digest
AND OLD.source->'serverIds'=to_jsonb(t.target_server_ids)
) THEN RETURN OLD; END IF;
END IF;
RAISE EXCEPTION 'diagnostic terminal proof must match its original source or exact retirement claim';
END $$;
CREATE TRIGGER server_probe_terminal_proof_guard BEFORE INSERT OR UPDATE OR DELETE
ON public.server_probe_terminal_proofs FOR EACH ROW EXECUTE FUNCTION public.server_probe_terminal_proof_guard();
CREATE TRIGGER server_probe_terminal_proof_no_truncate BEFORE TRUNCATE
ON public.server_probe_terminal_proofs FOR EACH STATEMENT EXECUTE FUNCTION public.server_probe_terminal_proof_guard();
CREATE OR REPLACE FUNCTION public.server_probe_retirement_claim_guard() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public SET TimeZone='UTC' AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_connections c ON c.id=o.connection_id
WHERE o.id=NEW.operation_id AND p.id=NEW.plan_id AND c.id=NEW.connection_id
AND o.state='applied' AND o.verification='inconclusive' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND p.operation_id='servers.probes.run' AND p.operation_version=1 AND p.state='completed'
AND (EXISTS(SELECT 1 FROM public.server_probe_storage_admissions a
WHERE a.receipt_id=NEW.receipt_id AND a.execution_id=o.id AND a.identity_kind='mcp' AND a.owner_id=c.owner_id
AND a.claim_token IS NULL AND a.claim_attempted_at IS NOT NULL AND a.cancelled_at IS NULL AND a.purged_at IS NULL)
OR(EXISTS(SELECT 1 FROM public.server_probe_terminal_proofs f
WHERE f.operation_id=o.id AND f.receipt_id=NEW.receipt_id AND f.source_proof=NEW.source_proof
AND f.source->>'ownerId'=c.owner_id AND f.source->>'connectionId'=c.id::text AND f.source->>'planId'=p.id::text)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_storage_admissions a
WHERE a.receipt_id=NEW.receipt_id OR(a.identity_kind='mcp' AND a.execution_id=o.id))))
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'diagnostic retirement claim cannot be replaced or reopened';
END $$;
INSERT INTO public.schema_migrations(id) VALUES ('0053_server_probe_terminal_proofs') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,18 @@
BEGIN;
CREATE OR REPLACE FUNCTION public.mcp_ordinary_retirement_claim_immutable() RETURNS trigger
LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.mcp_operations o JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
WHERE o.id=NEW.operation_id AND o.plan_id=NEW.plan_id AND o.connection_id=NEW.connection_id
AND o.state='applied' AND o.verification='passed' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND (p.operation_id,p.operation_version) IN (
('logs.settings.update',1),('traffic.settings.update',2),('uptime.settings.update',2),
('servers.api-ping.settings.update',2),('internet-path.settings.update',1),('geoip.settings.update',1)
) AND p.state='completed'
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'ordinary operation retirement claim cannot be replaced or reopened';
END $$;
INSERT INTO public.schema_migrations(id) VALUES ('0054_mcp_geoip_retirement') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,18 @@
BEGIN;
CREATE OR REPLACE FUNCTION public.mcp_ordinary_retirement_claim_immutable() RETURNS trigger
LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.mcp_operations o JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
WHERE o.id=NEW.operation_id AND o.plan_id=NEW.plan_id AND o.connection_id=NEW.connection_id
AND o.state='applied' AND o.verification='passed' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND (p.operation_id,p.operation_version) IN (
('logs.settings.update',1),('traffic.settings.update',2),('uptime.settings.update',2),
('servers.api-ping.settings.update',2),('internet-path.settings.update',1),('geoip.settings.update',1),('backups.schedule.update',1)
) AND p.state='completed'
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'ordinary operation retirement claim cannot be replaced or reopened';
END $$;
INSERT INTO public.schema_migrations(id) VALUES ('0055_mcp_backup_schedule_retirement') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,43 @@
-- Keep the live charge and intent fences in sync with recovery action v8.
-- Existing authenticated v6/v7 history remains unchanged.
BEGIN;
CREATE OR REPLACE FUNCTION public.mcp_routing_manual_inverse_charge_source() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_routing_profiles r ON r.id=NEW.profile_id AND r.connection_id=o.connection_id
JOIN public.mcp_operations source ON source.id=NEW.source_operation_id AND source.connection_id=o.connection_id
JOIN public.mcp_plans sp ON sp.id=source.plan_id AND sp.connection_id=source.connection_id
JOIN public.mcp_connections c ON c.id=o.connection_id
WHERE o.id=NEW.recovery_operation_id AND p.id=NEW.recovery_plan_id
AND o.connection_id=NEW.connection_id AND c.owner_id=NEW.owner_id
AND r.version=NEW.profile_version AND o.state='pending' AND p.state='executing'
AND p.operation_id='operations.recover' AND p.operation_version=8
AND sp.id=NEW.source_plan_id AND sp.operation_id='routing.community.switch' AND sp.operation_version=7
AND source.state IN('applied','partial')
) THEN RAISE EXCEPTION 'manual inverse charge differs from pending admission'; END IF;
RETURN NEW;
END $$;
CREATE OR REPLACE FUNCTION public.mcp_scoped_recovery_manual_version() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NEW.mode='manual' AND NOT pg_try_advisory_xact_lock(hashtextextended('mcp-runner-admission',0))
THEN RAISE EXCEPTION 'manual intent admission snapshot busy'; END IF;
IF NEW.mode='manual' AND NOT EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=0
JOIN public.mcp_operations source ON source.id=NEW.source_operation_id AND source.connection_id=o.connection_id
WHERE o.id=NEW.recovery_operation_id AND o.state='running'
AND p.state='executing' AND p.operation_id='operations.recover' AND p.operation_version=8
AND NEW.source_step_index=0 AND s.state='intent'
AND s.intended_state->>'kind'='service'
AND s.intended_state->>'service'='routing.community.scoped.recover'
AND s.intended_state->'input'->>'mode'='manual'
AND s.intended_state->'input'->>'sourceOperationId'=NEW.source_operation_id::text
AND s.intended_state->'input'->>'receiptId'=NEW.recovery_receipt_id::text
) THEN RAISE EXCEPTION 'manual inverse requires current exact running recovery'; END IF;
RETURN NEW;
END $$;
INSERT INTO public.schema_migrations(id) VALUES('0056_mcp_manual_inverse_recovery_v8') ON CONFLICT(id) DO NOTHING;
COMMIT;
+14
View File
@@ -597,6 +597,13 @@
"operations": [], "operations": [],
"reason": "Управление самим MCP, транспорт или машинная авторизация: отдельного AI-инструмента нет." "reason": "Управление самим MCP, транспорт или машинная авторизация: отдельного AI-инструмента нет."
}, },
{
"method": "GET",
"path": "/api/mcp/devices/:serverId/bridge-vlan/configuration",
"status": "mediated",
"operations": ["network.bridge-vlan.read"],
"reason": "Чтение связанной конфигурации bridge/VLAN через общую типизированную модель и проверку доступа MCP."
},
{ {
"method": "GET", "method": "GET",
"path": "/api/mcp/drift", "path": "/api/mcp/drift",
@@ -1611,6 +1618,13 @@
"operations": [], "operations": [],
"reason": "Управление самим MCP, транспорт или машинная авторизация: отдельного AI-инструмента нет." "reason": "Управление самим MCP, транспорт или машинная авторизация: отдельного AI-инструмента нет."
}, },
{
"method": "POST",
"path": "/api/mcp/devices/:serverId/bridge-vlan/configuration/plan",
"status": "mediated",
"operations": ["network.bridge-vlan.configure"],
"reason": "Подготовка связанного плана bridge/VLAN через тот же MCP-инструмент; выполнение требует человеческого подтверждения."
},
{ {
"method": "POST", "method": "POST",
"path": "/api/mcp/management-channels", "path": "/api/mcp/management-channels",
+4 -3
View File
@@ -35,7 +35,7 @@
"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: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: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": "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:wireguard": "tsx src/services/mikrotik-response-bounds.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: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-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: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/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts",
@@ -66,7 +66,7 @@
"test:mcp:ipsec-export-journal:pg": "node scripts/test-mcp-postgres.mjs --ipsec-export-journal --restore", "test:mcp:ipsec-export-journal:pg": "node scripts/test-mcp-postgres.mjs --ipsec-export-journal --restore",
"test:mcp:ipsec-export:http:pg": "node scripts/test-mcp-postgres.mjs --ipsec-export-http --restore", "test:mcp:ipsec-export:http:pg": "node scripts/test-mcp-postgres.mjs --ipsec-export-http --restore",
"test:mcp:diagnostic-cancel-host:pg": "node scripts/test-mcp-postgres.mjs --diagnostic-cancel-host --restore", "test:mcp:diagnostic-cancel-host:pg": "node scripts/test-mcp-postgres.mjs --diagnostic-cancel-host --restore",
"test:server-probe-management": "tsx src/services/server-probe-authority.test.ts && tsx src/services/server-probe-public-result.test.ts && tsx src/services/server-probe-execution.test.ts && tsx src/services/server-probe-rest-contract.test.ts && tsx src/services/server-probe-rest-execution.test.ts && tsx src/services/server-probe-journal.test.ts && tsx src/services/server-probe-journal-history.test.ts && tsx src/modules/mcp/server-probe-actions.test.ts && tsx src/modules/mcp/diagnostic-cancellation.test.ts && tsx src/modules/mcp/service-diagnostic-progress.test.ts && tsx src/services/mikrotik-get-admission.test.ts && tsx src/services/server-probe-storage-journal-adapter.test.ts && tsx --test src/services/server-probe-storage-history.test.ts && npm run test:server-probe-storage-models", "test:server-probe-management": "tsx src/services/server-probe-authority.test.ts && tsx src/services/server-probe-public-result.test.ts && tsx src/services/server-probe-execution.test.ts && tsx src/services/server-probe-rest-contract.test.ts && tsx src/services/server-probe-rest-execution.test.ts && tsx src/services/server-probe-journal.test.ts && tsx src/services/server-probe-journal-history.test.ts && tsx src/modules/mcp/server-probe-actions.test.ts && tsx src/modules/mcp/diagnostic-cancellation.test.ts && tsx src/modules/mcp/service-diagnostic-progress.test.ts && tsx src/services/mikrotik-get-admission.test.ts && tsx src/services/server-probe-storage-journal-adapter.test.ts && npm run test:server-probe-history && npm run test:server-probe-storage-models",
"test:server-poll-http:pg": "node scripts/test-server-poll-http-owned.mjs", "test:server-poll-http:pg": "node scripts/test-server-poll-http-owned.mjs",
"test:server-poll-approval:pg": "node scripts/test-server-poll-approval-owned.mjs", "test:server-poll-approval:pg": "node scripts/test-server-poll-approval-owned.mjs",
"test:server-poll-atomic:pg": "node scripts/test-server-poll-atomic-owned.mjs", "test:server-poll-atomic:pg": "node scripts/test-server-poll-atomic-owned.mjs",
@@ -155,8 +155,9 @@
"test:server-poll-durable:pg": "node scripts/test-mcp-postgres.mjs --server-poll-durable --restore", "test:server-poll-durable:pg": "node scripts/test-mcp-postgres.mjs --server-poll-durable --restore",
"test:server-probe-legacy:pg": "node scripts/test-mcp-postgres.mjs --probe-legacy-orphans", "test:server-probe-legacy:pg": "node scripts/test-mcp-postgres.mjs --probe-legacy-orphans",
"test:server-probe-storage:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-journal", "test:server-probe-storage:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-journal",
"test:server-probe-history": "tsx --test src/services/server-probe-storage-history.test.ts", "test:server-probe-history": "tsx --test src/services/server-probe-storage-history.test.ts src/services/server-probe-retired-source.test.ts",
"test:server-probe-history:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-history", "test:server-probe-history:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-history",
"test:server-probe-retirement:pg": "node scripts/test-mcp-postgres.mjs --probe-operation-retirement",
"test:server-probe-storage-models": "tsx src/services/server-probe-journal-maintenance.test.ts && tsx src/modules/mcp/diagnostic-cancellation-source.test.ts && tsx src/services/server-probe-storage-source.test.ts && tsx src/services/server-probe-storage-connection.test.ts && tsx src/modules/mcp/server-probe-storage-host.test.ts && tsx src/services/server-probe-storage-rest.test.ts && tsx src/services/server-probe-storage-metadata.test.ts && tsx src/services/server-probe-legacy-verifier.test.ts && tsx src/services/server-probe-legacy-snapshot.test.ts && tsx src/services/server-probe-legacy-inspection.test.ts && tsx src/db/database-source-observation.test.ts && tsx src/services/server-probe-retention.test.ts && tsx src/modules/mcp/server-probe-registration.test.ts", "test:server-probe-storage-models": "tsx src/services/server-probe-journal-maintenance.test.ts && tsx src/modules/mcp/diagnostic-cancellation-source.test.ts && tsx src/services/server-probe-storage-source.test.ts && tsx src/services/server-probe-storage-connection.test.ts && tsx src/modules/mcp/server-probe-storage-host.test.ts && tsx src/services/server-probe-storage-rest.test.ts && tsx src/services/server-probe-storage-metadata.test.ts && tsx src/services/server-probe-legacy-verifier.test.ts && tsx src/services/server-probe-legacy-snapshot.test.ts && tsx src/services/server-probe-legacy-inspection.test.ts && tsx src/db/database-source-observation.test.ts && tsx src/services/server-probe-retention.test.ts && tsx src/modules/mcp/server-probe-registration.test.ts",
"test:mcp:migration-data:pg": "node scripts/test-mcp-postgres.mjs --migration-data-inventory --restore", "test:mcp:migration-data:pg": "node scripts/test-mcp-postgres.mjs --migration-data-inventory --restore",
"test:mcp:poll-memory:pg": "npm run build -w @mmapp/contracts && node scripts/acceptance/build-poll-memory.mjs && node scripts/test-mcp-postgres.mjs --poll-memory", "test:mcp:poll-memory:pg": "npm run build -w @mmapp/contracts && node scripts/acceptance/build-poll-memory.mjs && node scripts/test-mcp-postgres.mjs --poll-memory",
@@ -0,0 +1,123 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { randomBytes } from 'node:crypto'
import { lstat, mkdtemp, rm } from 'node:fs/promises'
import { createServer } from 'node:net'
import { tmpdir } from 'node:os'
import { isAbsolute, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import EmbeddedPostgres from 'embedded-postgres'
import pg from 'pg'
import { applySqlMigrations } from '../../src/db/migrate.ts'
import { MIGRATION_MANIFEST } from '../../src/db/migration-manifest.ts'
assert.deepEqual(process.argv.slice(2), ['--owned-embedded'])
const bin = process.env.MMAPP_PG_CLIENT_BIN
assert(bin && isAbsolute(bin) && resolve(bin) === bin, 'Set MMAPP_PG_CLIENT_BIN to the exact local PostgreSQL client directory')
const psql = join(bin, process.platform === 'win32' ? 'psql.exe' : 'psql')
const executable = await lstat(psql)
assert(executable.isFile() && !executable.isSymbolicLink(), 'PostgreSQL client must be a regular file')
const clientEnv = Object.fromEntries(Object.entries(process.env).filter(([name]) => !/^PG/i.test(name)))
const run = (command, args, options = {}) => execFileSync(command, args, {
timeout: 10000, maxBuffer: 16384, encoding: 'utf8', windowsHide: true, ...options,
})
assert.match(run(psql, ['--version'], { env: clientEnv }), /^psql \(PostgreSQL\) (?:1[6-9]|[2-9][0-9])\./)
const witness = fileURLToPath(new URL('../../../tmp/mcp-netflow-linux-topology-20261104/collector-drain-witness.py', import.meta.url))
const { IDENTITY_SQL, SQL } = JSON.parse(run('python', ['-I', '-B', '-c',
'import json,runpy,sys; m=runpy.run_path(sys.argv[1]); print(json.dumps({k:m[k] for k in ("IDENTITY_SQL","SQL")}))', witness]))
assert(typeof SQL === 'string' && typeof IDENTITY_SQL === 'string')
const directory = await mkdtemp(join(tmpdir(), 'mm-netflow-drain-pg-'))
const password = randomBytes(24).toString('hex')
const host = '127.0.0.1', database = 'postgres', user = 'mcp_test'
const listener = createServer()
let embedded, pool, started = false
try {
await new Promise((done, reject) => { listener.once('error', reject); listener.listen(0, host, done) })
const { port } = listener.address()
await new Promise((done, reject) => listener.close(error => error ? reject(error) : done()))
embedded = new EmbeddedPostgres({
databaseDir: join(directory, 'pg'), port, user, password, persistent: true, createPostgresUser: false,
initdbFlags: ['--encoding=UTF8'], postgresFlags: ['-h', host, '-c', 'unix_socket_directories='],
onLog: () => {}, onError: () => {},
})
await embedded.initialise()
await embedded.start()
started = true
pool = new pg.Pool({ host, port, database, user, password, max: 1, connectionTimeoutMillis: 5000 })
await applySqlMigrations(pool)
const { rows: [version] } = await pool.query("SELECT current_setting('server_version_num')::int AS number, (SELECT count(*)::int FROM schema_migrations) AS migrations")
assert(version.number >= 180000 && version.number <= 189999)
assert.equal(version.migrations, MIGRATION_MANIFEST.length)
const runId = 'a012bcde', owner = 'netflow-operator-' + runId, collectorPort = 51000
const read = (sql, probePort = collectorPort) => JSON.parse(run(psql, ['-X', '-w', '-qAt',
'-h', host, '-p', String(port), '-U', user, '-d', database, '-v', 'ON_ERROR_STOP=1',
'-v', 'database=' + database, '-v', 'socket_dir=/tmp/mcp-netflow-' + runId + '/pg-socket',
'-v', 'run_id=' + runId, '-v', 'port=' + probePort],
{ env: { ...clientEnv, PGCONNECT_TIMEOUT: '2', PGPASSWORD: password }, input: sql }).trim())
const identity = read(IDENTITY_SQL)
assert.deepEqual(Object.keys(identity).sort(), ['oid', 'source'])
assert.match(identity.oid, /^[1-9][0-9]{0,9}$/)
// The production source predicate deliberately rejects this TCP fixture.
assert.equal(identity.source, false)
const expect = (planCount, enabled, verified, ordered = true, sqlPort = collectorPort) =>
assert.deepEqual(read(SQL, sqlPort), { ...identity, connectionCount: 1, planCount, enabled, verified, ordered })
const uuid = index => 'a012bcde-0000-4000-8000-' + String(index).padStart(12, '0')
const connection = uuid(1)
await pool.query(`INSERT INTO mcp_connections(id,name,owner_id,owner_permissions,policy,generation)
VALUES($1,$2,$3,'[]','{}','inert-witness')`, [connection, 'Owned NetFlow ' + runId, owner])
await pool.query("UPDATE traffic_flow_settings SET collector_ip='198.18.242.3', flow_listen_port=$1, enabled=false WHERE id=1", [collectorPort])
expect(0, false, [])
async function seed(index, enabled) {
const plan = uuid(index), operation = uuid(index + 1), receipt = uuid(index + 2)
const base = Date.UTC(2026, 0, 1) + index * 1000
const at = offset => new Date(base + offset)
const result = { receiptId: receipt, configurationConfirmed: true, receiptConfirmed: true,
listener: 'request-acknowledged', automaticReplay: false }
await pool.query(`INSERT INTO mcp_plans(id,connection_id,operation_id,operation_version,policy_version,generation,payload,evidence,state,expires_at,approved_by,approved_at,created_at)
VALUES($1,$2,'netflow.settings.update',3,1,'inert-witness','{}','{}','completed',$3,$4,$5,$6)`,
[plan, connection, at(9000), owner, at(100), at(0)])
await pool.query(`INSERT INTO mcp_operations(id,plan_id,connection_id,idempotency_key,state,verification,created_at,finished_at)
VALUES($1,$2,$3,$4,'applied','passed',$5,$6)`, [operation, plan, connection, 'inert-' + index, at(200), at(400)])
await pool.query(`INSERT INTO mcp_operation_steps(operation_id,step_index,state,intended_state,result,created_at,finished_at)
VALUES($1,0,'verified',$2,$3,$4,$5)`,
[operation, { service: 'netflow.settings.update', input: { receiptId: receipt, enabled } }, result, at(300), at(400)])
await pool.query(`INSERT INTO mcp_audit(actor_id,action,connection_id,payload,created_at)
VALUES($1,'plan.approve',$2,$3,$4)`, [owner, connection, { planId: plan }, at(100)])
await pool.query(`INSERT INTO flow_settings_intents(receipt_id,phase,request_proof,state,result,created_at)
VALUES($1,'configuration','inert-proof','completed',$2,$3),($1,'listener','inert-proof','completed',$2,$3)`, [receipt, result, at(300)])
await pool.query('UPDATE traffic_flow_settings SET enabled=$1 WHERE id=1', [enabled])
return [plan, operation, receipt]
}
const enabled = await seed(10, true)
expect(1, true, [enabled])
const disabled = await seed(20, false)
expect(2, false, [enabled, disabled])
expect(2, null, [enabled, disabled], true, collectorPort + 1)
await pool.query("UPDATE flow_settings_intents SET request_proof='foreign' WHERE receipt_id=$1 AND phase='listener'", [disabled[2]])
expect(2, false, [enabled])
await pool.query("UPDATE flow_settings_intents SET request_proof='inert-proof' WHERE receipt_id=$1 AND phase='listener'", [disabled[2]])
const { rows: [duplicate] } = await pool.query(`INSERT INTO mcp_audit(actor_id,action,connection_id,payload)
VALUES($1,'plan.approve',$2,$3) RETURNING id`, [owner, connection, { planId: disabled[0] }])
expect(2, false, [enabled])
await pool.query('DELETE FROM mcp_audit WHERE id=$1', [duplicate.id])
const { rows: [original] } = await pool.query(`UPDATE mcp_operations SET finished_at=(SELECT created_at + interval '1 second' FROM mcp_plans WHERE id=$2)
WHERE id=$1 RETURNING created_at`, [enabled[1], disabled[0]])
expect(2, false, [enabled, disabled], false)
await pool.query("UPDATE mcp_operations SET finished_at=$2::timestamptz + interval '200 milliseconds' WHERE id=$1", [enabled[1], original.created_at])
expect(2, false, [enabled, disabled])
console.log(JSON.stringify({ migrations: version.migrations, exactPsqlQueries: true, inertEnableDisableAndRejections: true,
linuxSocketSourceAccepted: false, realMcpExecutionClaimed: false }))
} finally {
try {
await pool?.end()
} finally {
if (started) await embedded.stop()
if (listener.listening) await new Promise(done => listener.close(done))
await rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
}
}
@@ -0,0 +1,73 @@
/** External SDK consumer. No application/store/runtime/database imports. */
import assert from 'node:assert/strict'
import { randomUUID } from 'node:crypto'
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'
const endpoint = new URL(process.env.OWN_MCP_ENDPOINT)
assert.equal(process.env.OWN_EXTERNAL_CLIENT, 'true')
assert.equal(endpoint.hostname, '127.0.0.1')
assert.notEqual(process.pid, Number(process.env.OWN_SERVER_PID))
const client = new Client({ name: 'owned-linked-bridge-vlan-sdk', version: '1.0.0' })
const boundedFetch = async (input, init) => {
const url = new URL(typeof input === 'string' || input instanceof URL ? input : input.url)
assert.equal(url.origin, endpoint.origin)
return fetch(input, { ...init, redirect: 'manual', signal: AbortSignal.timeout(10000) })
}
const payload = result => {
const text = result.content.find(item => item.type === 'text')?.text
assert(text && Buffer.byteLength(text) < 131072)
return JSON.parse(text)
}
const call = async (name, args) => {
const result = await client.callTool({ name, arguments: args }, undefined, { timeout: 10000 })
assert(!result.isError, JSON.stringify(payload(result)))
return payload(result)
}
try {
await client.connect(new StreamableHTTPClientTransport(endpoint, {
requestInit: { headers: { authorization: 'Bearer ' + process.env.OWN_MCP_TOKEN } }, fetch: boundedFetch,
}), { timeout: 10000 })
const tools = (await client.listTools({}, { timeout: 10000 })).tools
const read = tools.find(tool => tool.name === 'mm_bridge_vlan_configuration_get')
const configure = tools.find(tool => tool.name === 'mm_bridge_vlan_configuration_plan')
assert(read && configure)
assert.match(read.description, /Полная конфигурация bridge VLAN/)
assert.match(read.description, /Возвращает данные только из разрешённой области/)
assert.match(configure.description, /Связанный план VLAN membership, PVID и bridge filtering/)
assert.match(configure.description, /Подготавливает неизменяемый план/)
assert.equal(read.annotations.readOnlyHint, true)
assert.equal(configure.annotations.readOnlyHint, false)
assert.deepEqual(read.inputSchema.required, ['serverId'])
assert.deepEqual(configure.inputSchema.required, ['serverId', 'bridgeId', 'desired'])
const desiredSchema = configure.inputSchema.properties.desired
for (const name of ['ports', 'vlans', 'removePortIds', 'removeVlanIds']) assert(desiredSchema.properties[name])
const serverId = Number(process.env.OWN_ALLOWED_SERVER)
const state = (await call(read.name, { serverId })).state
assert.equal(state.bridges[0]['.id'], '*1')
assert(state.interfaces.some(item => item.resource === 'ethernet' && item.id === '*3' && item.name === 'ether3'))
assert.equal(state.vlans[0]['vlan-ids'], '10')
const cpu = { resource: 'bridge', id: '*1' }, access = { resource: 'ethernet', id: '*2' }, trunk = { resource: 'ethernet', id: '*3' }
const plan = await call(configure.name, { serverId, bridgeId: '*1', desired: {
filtering: true, pvid: 1,
ports: [{ id: '*10', interface: access, pvid: 10, ingressFiltering: true, frameTypes: 'admit-all', disabled: false },
{ interface: trunk, pvid: 1, ingressFiltering: true, frameTypes: 'admit-only-vlan-tagged', disabled: false }],
vlans: [{ id: '*11', vlanIds: '10', tagged: [cpu], untagged: [access], disabled: false },
{ vlanIds: '20', tagged: [cpu, trunk], untagged: [], disabled: false }], removePortIds: [], removeVlanIds: [],
} })
assert.equal(plan.state, 'awaiting-approval')
assert.equal(typeof plan.planId, 'string')
assert.equal(plan.changes.requiresHuman, true)
assert.deepEqual(plan.changes.steps.map(item => [item.step.resource, item.step.action]),
[['bridge_vlan', 'create'], ['bridge_port', 'create'], ['bridge', 'update']])
const denied = await client.callTool({ name: 'mm_change_apply', arguments: { planId: plan.planId, idempotencyKey: randomUUID() } }, undefined, { timeout: 10000 })
assert.equal(denied.isError, true)
const deniedPayload = payload(denied)
assert(JSON.stringify(deniedPayload).includes('APPROVAL_REQUIRED'))
const readback = (await call(read.name, { serverId })).state
assert.deepEqual(readback, state)
process.send({ kind: 'result', result: { externalSdkProcess: true, transport: 'streamable-http', clientPid: process.pid,
listedTools: [read, configure], state, plan, applyDenial: deniedPayload, unchangedReadback: true } })
} finally {
await client.close()
process.disconnect?.()
}
@@ -0,0 +1,84 @@
import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import { createServer } from 'node:http'
import { writeFile } from 'node:fs/promises'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { isAbsolute, resolve } from 'node:path'
assert.equal(process.env.OWN_EXTERNAL_PROCESS_PG, 'true')
assert.equal(process.env.NODE_ENV, 'test')
assert(isAbsolute(process.env.OWN_EXTERNAL_SOURCE_ROOT!))
assert(isAbsolute(process.env.OWN_EXTERNAL_RESULT_FILE!))
const database = new URL(process.env.DATABASE_URL!)
assert.equal(database.hostname, '127.0.0.1'); assert.equal(database.username, 'mcp_test')
assert.equal(database.pathname, '/postgres'); assert.notEqual(database.port, '5432')
const source = (path: string) => pathToFileURL(resolve(process.env.OWN_EXTERNAL_SOURCE_ROOT!, 'backend/src', path)).href
const rows: Record<string, Record<string, string>[]> = {
'/interface/ethernet': [{ '.id': '*2', name: 'ether2', disabled: 'false' }, { '.id': '*3', name: 'ether3', disabled: 'false' }, { '.id': '*4', name: 'ether-mgmt', disabled: 'false' }],
'/interface/bridge': [{ '.id': '*1', name: 'br-users', pvid: '1', 'vlan-filtering': 'false', 'frame-types': 'admit-all', 'ingress-filtering': 'true', mvrp: 'false', disabled: 'false' }],
'/interface/bridge/port': [{ '.id': '*10', bridge: 'br-users', interface: 'ether2', pvid: '10', 'ingress-filtering': 'true', 'frame-types': 'admit-all', disabled: 'false' }],
'/interface/bridge/vlan': [{ '.id': '*11', bridge: 'br-users', 'vlan-ids': '10', tagged: 'br-users', untagged: 'ether2', disabled: 'false', dynamic: 'false' }],
'/interface/vlan': [], '/interface/gre': [], '/interface/wireguard': [], '/interface/vxlan': [],
'/ip/address': [{ '.id': '*20', interface: 'ether-mgmt', address: '127.0.0.1/8', disabled: 'false', dynamic: 'false' }], '/ipv6/address': [],
}
const requests: string[] = []
let writeAttempts = 0
const router = createServer((req, res) => {
res.setHeader('content-type', 'application/json')
const path = decodeURIComponent(new URL(req.url!, 'http://127.0.0.1').pathname.replace(/^\/rest/, ''))
requests.push(req.method + ' ' + path)
if (req.method !== 'GET') { writeAttempts++; res.writeHead(405); res.end('{}'); return }
if (req.headers.authorization !== 'Basic ' + Buffer.from('fixture:fixture-only').toString('base64')) { res.writeHead(401); res.end('{}'); return }
if (!Object.hasOwn(rows, path)) { res.writeHead(404); res.end('{}'); return }
res.end(JSON.stringify(rows[path]))
})
await new Promise<void>(done => router.listen(0, '127.0.0.1', done))
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, updateMcpSettings } = await import(source('modules/mcp/store.js'))
const { buildApp } = await import(source('index.js'))
const actor = { id: 'owned-bridge-vlan-sdk-owner', email: 'bridge-sdk@example.invalid', name: 'Owned bridge VLAN SDK owner', apps: ['mm'], permissions: ['mm:settings:admin', 'mm:mcp:admin', 'mm:network:admin', 'mm:servers:admin'], expiresAt: Date.now() + 3600000, issuedAt: Date.now() - 1000 }
let app: Awaited<ReturnType<typeof buildApp>> | undefined
let child: ReturnType<typeof spawn> | undefined
let childExit: Promise<number> | undefined
try {
await applySqlMigrations(pool)
const serverId = (await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password) VALUES('owned bridge VLAN SDK','127.0.0.1',$1,false,true,'fixture','fixture-only') RETURNING id", [(router.address() as { port: number }).port])).rows[0].id
await updateMcpSettings({ enabled: true }, actor)
app = await buildApp({ startScheduler: false, logger: false })
await app.listen({ host: '127.0.0.1', port: 0 })
const endpoint = `http://127.0.0.1:${(app.server.address() as { port: number }).port}/mcp`
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: 'Owned bridge VLAN external SDK', mode: 'autonomous',
expiresAt: new Date(Date.now() + 1200000).toISOString(), permissions: ['mm:network:write'],
scope: { serverIds: [serverId], operationIds: ['network.bridge-vlan.read', 'network.bridge-vlan.configure', 'changes.apply'], allowGlobal: false },
limits: { callsPerMinute: 120, mutationsPerHour: 100 } }), actor)
let result: unknown, stdout = '', stderr = ''
child = spawn(process.execPath, [fileURLToPath(new URL('./bridge-vlan-client.mjs', import.meta.url))], { cwd: process.cwd(), windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'], env: { ...Object.fromEntries(['SystemRoot', 'WINDIR', 'TEMP', 'TMP', 'PATH', 'Path'].filter(k => process.env[k]).map(k => [k, process.env[k]!])),
OWN_EXTERNAL_CLIENT: 'true', OWN_MCP_ENDPOINT: endpoint, OWN_MCP_TOKEN: issued.token, OWN_SERVER_PID: String(process.pid), OWN_ALLOWED_SERVER: String(serverId) } })
child.on('message', (message: { kind: string; result?: unknown }) => { if (message.kind === 'result') result = message.result })
child.stdout?.on('data', chunk => { stdout += String(chunk); if (stdout.length > 1048576) child?.kill() })
child.stderr?.on('data', chunk => { stderr += String(chunk); if (stderr.length > 1048576) child?.kill() })
childExit = new Promise<number>((done, reject) => { child!.once('exit', code => done(code ?? 1)); child!.once('error', reject) })
const timeout = setTimeout(() => child?.kill(), 60000)
const code = await childExit; clearTimeout(timeout)
assert.equal(code, 0, stderr.slice(0, 3000)); assert(result)
assert.equal(writeAttempts, 0)
assert.equal((await pool.query('SELECT count(*)::int AS count FROM mcp_operations')).rows[0].count, 0)
const plans = (await pool.query('SELECT operation_id,state,approved_by FROM mcp_plans')).rows
assert.equal(plans.length, 1); assert.equal(plans[0].operation_id, 'network.bridge-vlan.configure')
assert.equal(plans[0].state, 'awaiting-approval'); assert.equal(plans[0].approved_by, null)
const audit = (await pool.query('SELECT tool,status,error_code FROM mcp_requests ORDER BY created_at')).rows
assert(audit.some(row => row.error_code === 'APPROVAL_REQUIRED'))
const serialized = JSON.stringify({ result, stdout, stderr, audit })
assert(!serialized.includes(issued.token)); assert(!serialized.includes('fixture-only'))
const final = { ...(result as object), actualOwnPg: true, actualProductionHttpServer: true, serverFixturePid: process.pid,
connectionMode: 'autonomous', plans, audit, nativeReadRequests: requests, nativeWriteAttempts: writeAttempts,
appliedOperations: 0, syntheticRouterOnly: true, productionDatabaseTouched: false, portalContacted: false }
await writeFile(process.env.OWN_EXTERNAL_RESULT_FILE!, JSON.stringify(final, null, 2))
console.log('Owned bridge VLAN SDK passed:', JSON.stringify({ listedTools: 2, planState: plans[0].state, nativeReadRequests: requests.length, nativeWriteAttempts: writeAttempts }))
} finally {
if (child && child.exitCode === null && child.signalCode === null) { child.kill(); await childExit }
try { await app?.close() } finally { try { await closePool() } finally { router.closeIdleConnections(); await new Promise<void>((done, reject) => router.close(error => error ? reject(error) : done())) } }
}
@@ -6,10 +6,14 @@ import { randomBytes, createHash } from 'node:crypto'
import { createServer } from 'node:net' import { createServer } from 'node:net'
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
const sourceRoot = resolve(process.argv[2] ?? fileURLToPath(new URL('../../../', import.meta.url))) const sourceRoot = resolve(process.argv[2] ?? fileURLToPath(new URL('../../../', import.meta.url)))
const output = resolve(sourceRoot, 'tmp/mcp-external-process-acceptance'); await mkdir(output, { recursive: true }) const scenario = process.argv[3] ?? 'acceptance'
if (!['acceptance', 'bridge-vlan'].includes(scenario)) throw Error('Unsupported owned external scenario')
const output = resolve(sourceRoot, scenario === 'bridge-vlan' ? 'tmp/mcp-bridge-vlan-sdk-20261105' : 'tmp/mcp-external-process-acceptance'); await mkdir(output, { recursive: true })
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', 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/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'] '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',
'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 } 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) const before = await pins(), directory = await mkdtemp(join(resolve(sourceRoot, 'tmp'), 'mm-mcp-external-')), state = join(directory, 'state'); await mkdir(state)
const listener = createServer(); await new Promise(resolve => listener.listen(0, '127.0.0.1', resolve)); const port = listener.address().port; await new Promise(resolve => listener.close(resolve)) const listener = createServer(); await new Promise(resolve => listener.listen(0, '127.0.0.1', resolve)); const port = listener.address().port; await new Promise(resolve => listener.close(resolve))
@@ -19,7 +23,7 @@ const postgres = new EmbeddedPostgres({ databaseDir: join(directory, 'pg'), port
try { try {
await postgres.initialise(); await postgres.start() await postgres.initialise(); await postgres.start()
process.exitCode = await new Promise((done, reject) => { process.exitCode = await new Promise((done, reject) => {
const child = spawn(process.execPath, ['--import', import.meta.resolve('tsx'), fileURLToPath(new URL('./server-fixture.ts', import.meta.url))], { cwd: directory, const child = spawn(process.execPath, ['--import', import.meta.resolve('tsx'), fileURLToPath(new URL(scenario === 'bridge-vlan' ? './bridge-vlan-fixture.ts' : './server-fixture.ts', import.meta.url))], { cwd: directory,
windowsHide: true, stdio: 'inherit', env: { ...Object.fromEntries(['SystemRoot', 'WINDIR', 'TEMP', 'TMP', 'PATH', 'Path'].filter(k => process.env[k]).map(k => [k, process.env[k]])), windowsHide: true, stdio: 'inherit', env: { ...Object.fromEntries(['SystemRoot', 'WINDIR', 'TEMP', 'TMP', 'PATH', 'Path'].filter(k => process.env[k]).map(k => [k, process.env[k]])),
OWN_EXTERNAL_CLEANUP_FAULT: process.env.OWN_EXTERNAL_CLEANUP_FAULT, OWN_EXTERNAL_SOURCE_ROOT: sourceRoot, OWN_EXTERNAL_RESULT_FILE: join(output, 'result.json'), NODE_ENV: 'test', OWN_EXTERNAL_PROCESS_PG: 'true', DATABASE_URL: `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres`, DATABASE_PATH: join(directory, 'no-legacy.sqlite'), OWN_EXTERNAL_CLEANUP_FAULT: process.env.OWN_EXTERNAL_CLEANUP_FAULT, OWN_EXTERNAL_SOURCE_ROOT: sourceRoot, OWN_EXTERNAL_RESULT_FILE: join(output, 'result.json'), NODE_ENV: 'test', OWN_EXTERNAL_PROCESS_PG: 'true', DATABASE_URL: `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres`, DATABASE_PATH: join(directory, 'no-legacy.sqlite'),
MCP_STATE_DIR: state, AUTH_REQUIRED: 'true', AUTH_JWT_SECRET: 'owned-external-synthetic-issuer-test-only', AUTH_ISSUER: 'https://synthetic-portal.invalid', AUTH_PORTAL_URL: 'https://synthetic-portal.invalid' } }) MCP_STATE_DIR: state, AUTH_REQUIRED: 'true', AUTH_JWT_SECRET: 'owned-external-synthetic-issuer-test-only', AUTH_ISSUER: 'https://synthetic-portal.invalid', AUTH_PORTAL_URL: 'https://synthetic-portal.invalid' } })
@@ -1,4 +1,5 @@
import assert from "node:assert/strict" import assert from "node:assert/strict"
import { createHash } from "node:crypto"
import { spawn } from "node:child_process" import { spawn } from "node:child_process"
import { createServer } from "node:net" import { createServer } from "node:net"
import { fileURLToPath, pathToFileURL } from "node:url" import { fileURLToPath, pathToFileURL } from "node:url"
@@ -49,6 +50,61 @@ try {
await updateMcpSettings({ enabled: true }, actor) await updateMcpSettings({ enabled: true }, actor)
app = await buildApp({ startScheduler: false, logger: false }); await app.listen({ port, host: "127.0.0.1" }) 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 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 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"
const registration = await fetch(origin + "/api/mcp/oauth/clients", { method: "POST", headers: portalHeaders,
body: JSON.stringify({ name: "Owned denial fixture", redirectUris: [callback] }) })
assert.equal(registration.status, 200)
const { clientId } = await registration.json() as { clientId: string }
const consent = (payload: Record<string, unknown>, headers = portalHeaders) => fetch(origin + "/api/mcp/oauth/consent", { method: "POST", headers, body: JSON.stringify(payload) })
const authorize = async (state: string) => {
const parameters = new URLSearchParams({ client_id: clientId, redirect_uri: callback, response_type: "code", resource: endpoint,
code_challenge_method: "S256", code_challenge: createHash("sha256").update("v".repeat(64)).digest("base64url"), state })
const response = await fetch(`${origin}/oauth/authorize?${parameters}`, { redirect: "manual" })
assert.equal(response.status, 302)
const requestId = new URL(response.headers.get("location")!).searchParams.get("oauthRequest")
assert.ok(requestId)
return requestId
}
const requestId = await authorize("deny-without-connection")
assert.equal((await consent({ requestId, approve: false }, { ...portalHeaders, authorization: "" })).status, 401)
assert.equal((await consent({ requestId, approve: true })).status, 400)
const refusals = await Promise.all([consent({ requestId, approve: false }), consent({ requestId, approve: false })])
assert.deepEqual(refusals.map(response => response.status).sort(), [200, 404])
const { redirectUrl } = await refusals.find(response => response.status === 200)!.json() as { redirectUrl: string }
const redirect = new URL(redirectUrl)
const expected = new URL(callback)
expected.searchParams.set("state", "deny-without-connection")
expected.searchParams.set("iss", origin)
expected.searchParams.set("error", "access_denied")
assert.equal(redirect.href, expected.href)
const otherOwner = { ...actor, id: "owned-external-process-other-owner" }
const unrelated = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Unrelated paused connection", mode: "observer",
expiresAt: new Date(Date.now() + 600000).toISOString(), permissions: ["mm:servers:read"],
scope: { serverIds: [Number(allowed)], operationIds: ["servers.list"], allowGlobal: false } }), otherOwner)
await setConnectionStatus(unrelated.connection.id, "paused", otherOwner)
const legacyRequestId = await authorize("legacy-denial")
const legacy = await consent({ requestId: legacyRequestId, connectionId: unrelated.connection.id, approve: false })
assert.equal(legacy.status, 200)
assert.equal((await consent({ requestId: legacyRequestId, connectionId: unrelated.connection.id, approve: false })).status, 404)
const expired = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Expired connection", mode: "observer",
expiresAt: new Date(Date.now() + 600000).toISOString(), permissions: ["mm:servers:read"],
scope: { serverIds: [Number(allowed)], operationIds: ["servers.list"], allowGlobal: false } }), actor)
await pool.query("UPDATE mcp_connections SET policy=jsonb_set(policy,'{expiresAt}',to_jsonb($2::text)) WHERE id=$1", [expired.connection.id, "2000-01-01T00:00:00Z"])
const expiredRequestId = await authorize("expired-approval")
assert.equal((await consent({ requestId: expiredRequestId, connectionId: expired.connection.id, approve: true })).status, 403)
const expiredRequest = (await pool.query("SELECT consumed_at,code_digest,connection_id FROM mcp_oauth_requests WHERE id=$1", [expiredRequestId])).rows[0]
assert.deepEqual(expiredRequest, { consumed_at: null, code_digest: null, connection_id: null })
assert.equal((await consent({ requestId: expiredRequestId, approve: false })).status, 200)
const deniedRows = (await pool.query("SELECT consumed_at,code_digest,connection_id FROM mcp_oauth_requests WHERE client_id=$1", [clientId])).rows
assert.equal(deniedRows.length, 3)
assert(deniedRows.every(row => row.consumed_at instanceof Date && row.code_digest === null && row.connection_id === null))
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_credentials WHERE client_id=$1", [clientId])).rows[0].count, 0)
const denialAudit = (await pool.query("SELECT actor_id,connection_id,payload FROM mcp_audit WHERE action='oauth.consent' AND payload->>'clientId'=$1", [clientId])).rows
assert.equal(denialAudit.length, 3)
assert(denialAudit.every(row => row.actor_id === actor.id && row.connection_id === null && row.payload.approved === false))
console.log("OAuth consent: expired approval refused before code issuance, denial uses exact redirect and one-use request without credentials")
const transcript: unknown[] = []; let failure: unknown, result: unknown const transcript: unknown[] = []; let failure: unknown, result: unknown
child = spawn(process.execPath, [fileURLToPath(new URL("./client.mjs", import.meta.url))], { cwd: process.cwd(), windowsHide: true, stdio: ["ignore", "pipe", "pipe", "ipc"], child = spawn(process.execPath, [fileURLToPath(new URL("./client.mjs", import.meta.url))], { cwd: process.cwd(), windowsHide: true, stdio: ["ignore", "pipe", "pipe", "ipc"],
env: { ...Object.fromEntries(["SystemRoot", "WINDIR", "TEMP", "TMP", "PATH", "Path"].filter(k => process.env[k]).map(k => [k, process.env[k]!])), env: { ...Object.fromEntries(["SystemRoot", "WINDIR", "TEMP", "TMP", "PATH", "Path"].filter(k => process.env[k]).map(k => [k, process.env[k]!])),
@@ -90,7 +146,8 @@ try {
const logs = JSON.stringify((await pool.query("SELECT tool,input,result,error_code FROM mcp_requests ORDER BY created_at")).rows) const logs = JSON.stringify((await pool.query("SELECT tool,input,result,error_code FROM mcp_requests ORDER BY created_at")).rows)
for (const token of [...credentials, jwt, secret]) { assert(!logs.includes(token)); assert(!stdout.includes(token)); assert(!stderr.includes(token)) } 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, const final = { ...(result as object), actualOwnPg: true, actualProductionHttpServer: true, serverFixturePid: process.pid,
controlTransitions: transcript, auditCredentialLeakAbsent: true, nativeRouterCalls: 0, productionDatabaseTouched: false } controlTransitions: transcript, auditCredentialLeakAbsent: true, denialWithoutConnection: true,
legacyDenialIgnoresPausedForeignConnection: true, nativeRouterCalls: 0, productionDatabaseTouched: false }
await writeFile(process.env.OWN_EXTERNAL_RESULT_FILE!, JSON.stringify(final, null, 2)) await writeFile(process.env.OWN_EXTERNAL_RESULT_FILE!, JSON.stringify(final, null, 2))
console.log("Owned external process SDK result:", JSON.stringify(final)) console.log("Owned external process SDK result:", JSON.stringify(final))
} finally { } finally {
+10 -4
View File
@@ -94,8 +94,11 @@ const operatorPeerMetadata = process.argv.includes('--operator-peer-metadata')
if (operatorPeerMetadata && process.argv.slice(2).some(arg => arg !== '--operator-peer-metadata')) throw new Error('Operator peer metadata requires a separate disposable cluster without restore') if (operatorPeerMetadata && process.argv.slice(2).some(arg => arg !== '--operator-peer-metadata')) throw new Error('Operator peer metadata requires a separate disposable cluster without restore')
const externalPeerMetadata = process.argv.includes('--external-peer-metadata') const externalPeerMetadata = process.argv.includes('--external-peer-metadata')
if (externalPeerMetadata && process.argv.slice(2).some(arg => arg !== '--external-peer-metadata')) throw new Error('Guarded external peer metadata requires a separate disposable cluster without restore') if (externalPeerMetadata && process.argv.slice(2).some(arg => arg !== '--external-peer-metadata')) throw new Error('Guarded external peer metadata requires a separate disposable cluster without restore')
const mcpOperationOrigin = process.argv.includes('--mcp-operation-origin') const pollOperationRetirement = process.argv.includes('--poll-operation-retirement')
if (mcpOperationOrigin && process.argv.slice(2).some(arg => arg !== '--mcp-operation-origin')) throw new Error('MCP operation root requires a separate disposable cluster without restore') const ordinaryOperationRetirement = process.argv.includes('--ordinary-operation-retirement')
const probeOperationRetirement = process.argv.includes('--probe-operation-retirement')
const mcpOperationOrigin = process.argv.includes('--mcp-operation-origin') || pollOperationRetirement || ordinaryOperationRetirement || probeOperationRetirement
if (mcpOperationOrigin && process.argv.slice(2).some(arg => arg !== (probeOperationRetirement ? '--probe-operation-retirement' : ordinaryOperationRetirement ? '--ordinary-operation-retirement' : pollOperationRetirement ? '--poll-operation-retirement' : '--mcp-operation-origin'))) throw new Error('MCP operation root requires a separate disposable cluster without restore')
const mcpSpeedOriginal = process.argv.includes('--mcp-speed-original') const mcpSpeedOriginal = process.argv.includes('--mcp-speed-original')
if (mcpSpeedOriginal && process.argv.slice(2).some(arg => arg !== '--mcp-speed-original')) throw new Error('MCP speed component requires a separate disposable cluster without restore') if (mcpSpeedOriginal && process.argv.slice(2).some(arg => arg !== '--mcp-speed-original')) throw new Error('MCP speed component requires a separate disposable cluster without restore')
const routerConfigurationGuard = process.argv.includes('--router-configuration-guard') const routerConfigurationGuard = process.argv.includes('--router-configuration-guard')
@@ -243,13 +246,13 @@ try {
if (regressionOnly) console.log('Selected regression only: primary integration and terminal restore are not run') 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') 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 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' : 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' : flowRebuild ? 'src/test/flow-facts-rebuild-sql.ts' : flowWrites ? 'src/services/traffic-flow-write-transaction.pg.test.ts' : nativeConservation ? 'src/modules/mcp/native-conservation-sql.test.ts' : nativeCreateId ? 'src/modules/mcp/native-create-id-http.test.ts' : ipsecAdapters ? "src/mLine 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', 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
cwd: pollMemory ? directory : new URL('../', import.meta.url), stdio: 'inherit', env: { ...(pollMemory ? pollMemoryEnvironment : process.env), cwd: pollMemory ? directory : new URL('../', import.meta.url), stdio: 'inherit', env: { ...(pollMemory ? pollMemoryEnvironment : process.env),
...(routingOriginalEffects ? { MMAPP_OWN_ROUTING_ORIGINAL_EFFECTS: 'true' } : {}), ...(routingOriginalEffects ? { MMAPP_OWN_ROUTING_ORIGINAL_EFFECTS: 'true' } : {}),
DATABASE_URL: `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres`, DATABASE_URL: `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres`,
DATABASE_PATH: join(directory, 'no-legacy-database.sqlite'), SQLITE_IMPORT_FULL_HISTORY: 'false', DATABASE_PATH: join(directory, 'no-legacy-database.sqlite'), SQLITE_IMPORT_FULL_HISTORY: 'false',
MCP_STATE_DIR: join(directory, 'state'), AUTH_REQUIRED: 'true', AUTH_JWT_SECRET: 'mcp-integration-test-only', MMAPP_TEST_WG_EXPORT_ONLY: String(wireguardExport), MCP_STATE_DIR: join(directory, 'state'), AUTH_REQUIRED: 'true', AUTH_JWT_SECRET: 'mcp-integration-test-only', MMAPP_TEST_WG_EXPORT_ONLY: String(wireguardExport),
MCP_PUBLIC_URL: 'http://localhost:8000/mcp', NODE_ENV: 'test', PATH: testPath, NODE_OPTIONS: pollMemory ? '' : process.env.NODE_OPTIONS, MMAPP_OWN_PRINCIPAL_SECURITY_SOURCE: String(principalSecuritySource), MMAPP_OWN_POLL_JOURNAL_SNAPSHOT: String(pollJournalSnapshot), MMAPP_OWN_POLL_MEMORY: String(pollMemory), MMAPP_OWN_POLL_BOOTSTRAP: String(pollMemoryBootstrap), MMAPP_OWN_POLL_COST_DIAGNOSTICS: String(pollMemoryCost), MMAPP_OWN_POLL_ARCHIVE_CLAIM_LOSS: String(pollArchiveClaimLoss), MMAPP_OWN_POLL_ARCHIVE_HISTORY: String(pollArchiveHistory), MMAPP_OWN_POLL_ARCHIVE_COLD: String(pollArchiveCold), MMAPP_OWN_POLL_ARCHIVE_LOST_ACK: String(pollArchiveLostAck), MMAPP_OWN_POLL_TERMINAL_ARCHIVE: String(pollTerminalArchive), MMAPP_OWN_POLL_ARCHIVE_SCHEMA: String(pollArchiveSchema), MMAPP_OWN_TELEMETRY_SETTINGS_CONFIGURATION: String(telemetrySettingsConfiguration), MMAPP_OWN_ROUTING_RESERVATION_SCHEMA: String(routingReservationSchema), MMAPP_OWN_ROUTING_FORWARD_ROUNDS: String(routingForwardRounds), MMAPP_OWN_FLOW_ACTIVE_SERVICES: String(flowActiveServices), MMAPP_OWN_FLOW_ACTIVE_REBUILD: String(flowActiveRebuild), MMAPP_OWN_FLOW_QUALITY: String(flowQuality), MMAPP_OWN_FLOW_WORKER_IPC: String(flowWorkerIpc), MMAPP_OWN_MANUAL_UPTIME: String(manualUptime), MMAPP_OWN_MANUAL_PING_GROUP: String(manualPingGroup), MMAPP_OWN_MANUAL_TRAFFIC: String(manualTraffic), MMAPP_OWN_MANUAL_API_PING: String(manualApiPing), MMAPP_OWN_SIMPLE_SPEED: String(simpleSpeed), MMAPP_OWN_SIMPLE_SPEED_CHR: String(simpleSpeedChr), MMAPP_OWN_DIAGNOSTIC_CANCEL_HOST: String(diagnosticCancelHost), MMAPP_OWN_OPERATOR_DOCKER_SQL: String(operatorDockerSql), MMAPP_OWN_PUBLIC_REGISTRY_METADATA: String(publicRegistryMetadata), MMAPP_OWN_OPERATOR_PEER_METADATA: String(operatorPeerMetadata), MMAPP_OWN_EXTERNAL_PEER_METADATA: String(externalPeerMetadata), MMAPP_OWN_BANDWIDTH_EXTERNAL_REGISTRY: String(externalPeerMetadata || operatorPeerMetadata || publicRegistryMetadata || operatorDockerSql || mcpSpeedOriginal || bandwidthSpeedWorkflow || bandwidthExternalRegistry), MMAPP_OWN_BANDWIDTH_SPEED_WORKFLOW: String(bandwidthSpeedWorkflow), MMAPP_OWN_REST_SPEED_ORIGINAL: String(restSpeedOriginal), MMAPP_OWN_MCP_OPERATION_ORIGIN: String(mcpOperationOrigin), MMAPP_OWN_MCP_SPEED_ORIGINAL: String(mcpSpeedOriginal), MMAPP_OWN_SCHEDULED_SPEED_ORIGINAL: String(scheduledSpeedOriginal), MMAPP_TEST_ROUTER_GUARD_OWNED: String(routerConfigurationGuard), MMAPP_TEST_ROUTER_GUARD_CLUSTER_DISPOSABLE: String(routerConfigurationGuard), MMAPP_TEST_ROUTER_GUARD_ADMIN_URL: routerConfigurationGuard ? `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres` : '', MMAPP_OWN_BANDWIDTH_ADMISSION: String(externalPeerMetadata || operatorPeerMetadata || publicRegistryMetadata || operatorDockerSql || mcpOperationOrigin || mcpSpeedOriginal || scheduledSpeedOriginal || bandwidthSpeedWorkflow || restSpeedOriginal || bandwidthExternalRegistry || operationLocks || serviceExecutionOrigin || bandwidthAdmission || bandwidthControlSource || bandwidthNodeRegistry || bandwidthNodeSource), MMAPP_OWN_BANDWIDTH_RESTORE: String((bandwidthAdmission || bandwidthControlSource || bandwidthNodeRegistry || bandwidthNodeSource) && process.argv.includes('--restore')), MMAPP_OWN_IPSEC_EXPORT_HTTP: String(ipsecExportHttp), MMAPP_OWN_IPSEC_EXPORT_JOURNAL: String(ipsecExportJournal), MMAPP_OWN_SQL_DB: 'postgres', MMAPP_OWN_PROBE_LEGACY_ORPHANS: String(probeLegacyOrphans), MMAPP_OWN_PROBE_STORAGE_HISTORY: String(probeStorageHistory), MMAPP_OWN_PROBE_STORAGE_FRESH: String(probeStorageFresh), MMAPP_OWN_PROBE_STORAGE_JOURNAL: String(probeStorageJournal || probeStorageFresh), MMAPP_OWN_CONTROLLED_SCHEMA_REFERENCE: String(controlledSchemaReference), MMAPP_OWN_CONTROLLED_MIGRATIONS: String(controlledMigrations), MMAPP_OWN_DATABASE_SCHEMA_CONTROL: String(databaseSchemaControl), MMAPP_OWN_MIGRATION_DATA_INVENTORY: String(migrationDataInventory), MMAPP_OWN_MIGRATION_SCHEMA_INVENTORY: String(migrationSchemaInventory), MMAPP_OWN_ROUTING_NETWORK_NATIVE: String(routingNetworkNative), MMAPP_OWN_ROUTING_RESERVATIONS: String(routingReservations || routingReservationRuntime), MMAPP_OWN_COLLECTOR_PARTITION_WRITERS: String(collectorPartitionWriters), MMAPP_OWN_SERVER_POLL_STORAGE_FRESH: String(serverPollStorageFresh), MMAPP_OWN_SERVER_POLL_STORAGE: String(serverPollStorage), MMAPP_OWN_SERVER_POLL_DURABLE: String(serverPollDurable), MMAPP_OWN_ROUTING_RECOVERY_AUTHORITY: String(routingRecoveryAuthority), MMAPP_OWN_LEGACY_FLOW_REBUILD: String(legacyFlowRebuild), MMAPP_OWN_PROBE_JOURNAL: String(probeJournal), MMAPP_MODULE_COMPLETION_ONLY: String(moduleCompletion), MMAPP_MODULE_COMPLETION_TARGET: completionTarget, MMAPP_OWN_FLOW_HOST_KEYS: String(flowHostKeys), MMAPP_OWN_MCP_FLOW_REBUILD: String(flowRebuildMcp), MMAPP_OWN_FLOW_WRITES: String(flowWrites), MMAPP_OWN_FLOW_REBUILD: String(flowRebuild) }, MCP_PUBLIC_URL: 'http://localhost:8000/mcp', NODE_ENV: 'test', PATH: testPath, NODE_OPTIONS: pollMemory ? '' : process.env.NODE_OPTIONS, MMAPP_OWN_PRINCIPAL_SECURITY_SOURCE: String(principalSecuritySource), MMAPP_OWN_POLL_JOURNAL_SNAPSHOT: String(pollJournalSnapshot), MMAPP_OWN_POLL_MEMORY: String(pollMemory), MMAPP_OWN_POLL_BOOTSTRAP: String(pollMemoryBootstrap), MMAPP_OWN_POLL_COST_DIAGNOSTICS: String(pollMemoryCost), MMAPP_OWN_POLL_ARCHIVE_CLAIM_LOSS: String(pollArchiveClaimLoss), MMAPP_OWN_POLL_ARCHIVE_HISTORY: String(pollArchiveHistory), MMAPP_OWN_POLL_ARCHIVE_COLD: String(pollArchiveCold), MMAPP_OWN_POLL_ARCHIVE_LOST_ACK: String(pollArchiveLostAck), MMAPP_OWN_POLL_TERMINAL_ARCHIVE: String(pollTerminalArchive), MMAPP_OWN_POLL_ARCHIVE_SCHEMA: String(pollArchiveSchema), MMAPP_OWN_TELEMETRY_SETTINGS_CONFIGURATION: String(telemetrySettingsConfiguration), MMAPP_OWN_ROUTING_RESERVATION_SCHEMA: String(routingReservationSchema), MMAPP_OWN_ROUTING_FORWARD_ROUNDS: String(routingForwardRounds), MMAPP_OWN_FLOW_ACTIVE_SERVICES: String(flowActiveServices), MMAPP_OWN_FLOW_ACTIVE_REBUILD: String(flowActiveRebuild), MMAPP_OWN_FLOW_QUALITY: String(flowQuality), MMAPP_OWN_FLOW_WORKER_IPC: String(flowWorkerIpc), MMAPP_OWN_MANUAL_UPTIME: String(manualUptime), MMAPP_OWN_MANUAL_PING_GROUP: String(manualPingGroup), MMAPP_OWN_MANUAL_TRAFFIC: String(manualTraffic), MMAPP_OWN_MANUAL_API_PING: String(manualApiPing), MMAPP_OWN_SIMPLE_SPEED: String(simpleSpeed), MMAPP_OWN_SIMPLE_SPEED_CHR: String(simpleSpeedChr), MMAPP_OWN_DIAGNOSTIC_CANCEL_HOST: String(diagnosticCancelHost), MMAPP_OWN_OPERATOR_DOCKER_SQL: String(operatorDockerSql), MMAPP_OWN_PUBLIC_REGISTRY_METADATA: String(publicRegistryMetadata), MMAPP_OWN_OPERATOR_PEER_METADATA: String(operatorPeerMetadata), MMAPP_OWN_EXTERNAL_PEER_METADATA: String(externalPeerMetadata), MMAPP_OWN_BANDWIDTH_EXTERNAL_REGISTRY: String(externalPeerMetadata || operatorPeerMetadata || publicRegistryMetadata || operatorDockerSql || mcpSpeedOriginal || bandwidthSpeedWorkflow || bandwidthExternalRegistry), MMAPP_OWN_BANDWIDTH_SPEED_WORKFLOW: String(bandwidthSpeedWorkflow), MMAPP_OWN_REST_SPEED_ORIGINAL: String(restSpeedOriginal), MMAPP_OWN_MCP_OPERATION_ORIGIN: String(mcpOperationOrigin), MMAPP_OWN_PROBE_OPERATION_RETIREMENT: String(probeOperationRetirement), MMAPP_OWN_MCP_SPEED_ORIGINAL: String(mcpSpeedOriginal), MMAPP_OWN_SCHEDULED_SPEED_ORIGINAL: String(scheduledSpeedOriginal), MMAPP_TEST_ROUTER_GUARD_OWNED: String(routerConfigurationGuard), MMAPP_TEST_ROUTER_GUARD_CLUSTER_DISPOSABLE: String(routerConfigurationGuard), MMAPP_TEST_ROUTER_GUARD_ADMIN_URL: routerConfigurationGuard ? `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres` : '', MMAPP_OWN_BANDWIDTH_ADMISSION: String(externalPeerMetadata || operatorPeerMetadata || publicRegistryMetadata || operatorDockerSql || mcpOperationOrigin || mcpSpeedOriginal || scheduledSpeedOriginal || bandwidthSpeedWorkflow || restSpeedOriginal || bandwidthExternalRegistry || operationLocks || serviceExecutionOrigin || bandwidthAdmission || bandwidthControlSource || bandwidthNodeRegistry || bandwidthNodeSource), MMAPP_OWN_BANDWIDTH_RESTORE: String((bandwidthAdmission || bandwidthControlSource || bandwidthNodeRegistry || bandwidthNodeSource) && process.argv.includes('--restore')), MMAPP_OWN_IPSEC_EXPORT_HTTP: String(ipsecExportHttp), MMAPP_OWN_IPSEC_EXPORT_JOURNAL: String(ipsecExportJournal), MMAPP_OWN_SQL_DB: 'postgres', MMAPP_OWN_PROBE_LEGACY_ORPHANS: String(probeLegacyOrphans), MMAPP_OWN_PROBE_STORAGE_HISTORY: String(probeStorageHistory), MMAPP_OWN_PROBE_STORAGE_FRESH: String(probeStorageFresh), MMAPP_OWN_PROBE_STORAGE_JOURNAL: String(probeStorageJournal || probeStorageFresh), MMAPP_OWN_CONTROLLED_SCHEMA_REFERENCE: String(controlledSchemaReference), MMAPP_OWN_CONTROLLED_MIGRATIONS: String(controlledMigrations), MMAPP_OWN_DATABASE_SCHEMA_CONTROL: String(databaseSchemaControl), MMAPP_OWN_MIGRATION_DATA_INVENTORY: String(migrationDataInventory), MMAPP_OWN_MIGRATION_SCHEMA_INVENTORY: String(migrationSchemaInventory), MMAPP_OWN_ROUTING_NETWORK_NATIVE: String(routingNetworkNative), MMAPP_OWN_ROUTING_RESERVATIONS: String(routingReservations || routingReservationRuntime), MMAPP_OWN_COLLECTOR_PARTITION_WRITERS: String(collectorPartitionWriters), MMAPP_OWN_SERVER_POLL_STORAGE_FRESH: String(serverPollStorageFresh), MMAPP_OWN_SERVER_POLL_STORAGE: String(serverPollStorage), MMAPP_OWN_SERVER_POLL_DURABLE: String(serverPollDurable), MMAPP_OWN_ROUTING_RECOVERY_AUTHORITY: String(routingRecoveryAuthority), MMAPP_OWN_LEGACY_FLOW_REBUILD: String(legacyFlowRebuild), MMAPP_OWN_PROBE_JOURNAL: String(probeJournal), MMAPP_MODULE_COMPLETION_ONLY: String(moduleCompletion), MMAPP_MODULE_COMPLETION_TARGET: completionTarget, MMAPP_OWN_FLOW_HOST_KEYS: String(flowHostKeys), MMAPP_OWN_MCP_FLOW_REBUILD: String(flowRebuildMcp), MMAPP_OWN_FLOW_WRITES: String(flowWrites), MMAPP_OWN_FLOW_REBUILD: String(flowRebuild) },
}) })
child.once('error', reject); child.once('exit', code => resolve(code ?? 1)) child.once('error', reject); child.once('exit', code => resolve(code ?? 1))
}) })
@@ -286,6 +289,9 @@ try {
else if (result === 0 && operatorPeerMetadata) console.log('Operator peer metadata: actual managed endpoint SQL and genuine localhost license origin; synthetic Q, external metadata and full operator window remain open') else if (result === 0 && operatorPeerMetadata) console.log('Operator peer metadata: actual managed endpoint SQL and genuine localhost license origin; synthetic Q, external metadata and full operator window remain open')
else if (result === 0 && publicRegistryMetadata) console.log('Public registry metadata: actual owned PostgreSQL and same physical lease; no secret/Q/native/write authority or backend-window acceptance') else if (result === 0 && publicRegistryMetadata) console.log('Public registry metadata: actual owned PostgreSQL and same physical lease; no secret/Q/native/write authority or backend-window acceptance')
else if (result === 0 && operatorDockerSql) console.log('Operator Docker SQL: actual owned PostgreSQL with mocked Docker HTTP only; not a real Docker/backend-window acceptance') else if (result === 0 && operatorDockerSql) console.log('Operator Docker SQL: actual owned PostgreSQL with mocked Docker HTTP only; not a real Docker/backend-window acceptance')
else if (result === 0 && ordinaryOperationRetirement) console.log('Ordinary operation retirement: owned actual logs-settings apply and worker')
else if (result === 0 && pollOperationRetirement) console.log('Poll operation retirement: owned authenticated historical fixtures and current reader/GC')
else if (result === 0 && probeOperationRetirement) console.log('Diagnostic retirement: actual owned worker tick, PostgreSQL and product read/replay; no live scheduling')
else if (result === 0 && mcpOperationOrigin) console.log('MCP operation root: owned PostgreSQL ordinary runner provenance; no restore/recovery, global writer activation, CHR or deployment') else if (result === 0 && mcpOperationOrigin) console.log('MCP operation root: owned PostgreSQL ordinary runner provenance; no restore/recovery, global writer activation, CHR or deployment')
else if (result === 0 && mcpSpeedOriginal) console.log('MCP speed: genuine trusted service context with real connection/grant/SQL, host receipt and localhost native; synthetic Q/mapping; not a public MCP runner/action acceptance') else if (result === 0 && mcpSpeedOriginal) console.log('MCP speed: genuine trusted service context with real connection/grant/SQL, host receipt and localhost native; synthetic Q/mapping; not a public MCP runner/action acceptance')
else if (result === 0 && routerConfigurationGuard) console.log('Router configuration guard: dedicated owned databases/roles and localhost native fixture; no application migration, production activation or real CHR') else if (result === 0 && routerConfigurationGuard) console.log('Router configuration guard: dedicated owned databases/roles and localhost native fixture; no application migration, production activation or real CHR')
@@ -1,7 +1,7 @@
import EmbeddedPostgres from 'embedded-postgres' import EmbeddedPostgres from 'embedded-postgres'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { mkdir, mkdtemp, readdir, readFile, writeFile, cp, rm } from 'node:fs/promises' import { mkdir, mkdtemp, readdir, readFile, writeFile, cp, rm } from 'node:fs/promises'
import { resolve, join, relative, dirname } from 'node:path' import { resolve, join, relative, dirname, basename } from 'node:path'
import { randomBytes, createHash } from 'node:crypto' import { randomBytes, createHash } from 'node:crypto'
import { createServer } from 'node:net' import { createServer } from 'node:net'
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
@@ -16,7 +16,8 @@ await readFile(join(root, 'backend/src/modules/mcp/routing-main-transition.ts'))
await readFile(join(root, 'backend/src/modules/mcp/routing-controller-baseline-acceptance.ts')) await readFile(join(root, 'backend/src/modules/mcp/routing-controller-baseline-acceptance.ts'))
if (process.argv.includes('--check-only')) { console.log(JSON.stringify({ mode, sourcePresent: true, acceptanceExecuted: false, productionApplied: false })); process.exit(0) } if (process.argv.includes('--check-only')) { console.log(JSON.stringify({ mode, sourcePresent: true, acceptanceExecuted: false, productionApplied: false })); process.exit(0) }
await mkdir(join(root, 'tmp'), { recursive: true }) await mkdir(join(root, 'tmp'), { recursive: true })
const directory = await mkdtemp(join(root, 'tmp/routing-controller-baseline-owned-')), output = join(directory, 'dist'), sourcePins = [] const ownedPrefix = 'mm-mcp-test-routing-controller-baseline-'
const directory = await mkdtemp(join(root, 'tmp', ownedPrefix)), output = join(directory, 'dist'), sourcePins = []
const hash = buffer => createHash('sha256').update(buffer).digest('hex') const hash = buffer => createHash('sha256').update(buffer).digest('hex')
async function compile(sourceDirectory) { async function compile(sourceDirectory) {
for (const entry of await readdir(sourceDirectory, { withFileTypes: true })) { for (const entry of await readdir(sourceDirectory, { withFileTypes: true })) {
@@ -59,7 +60,7 @@ try {
// embedded-postgres waits for a new exit event; avoid waiting again if its // embedded-postgres waits for a new exit event; avoid waiting again if its
// owned child has already exited. This never targets another database. // owned child has already exited. This never targets another database.
if (postgres.process?.exitCode === null) await postgres.stop() if (postgres.process?.exitCode === null) await postgres.stop()
if (dirname(resolve(directory)) !== resolve(root, 'tmp') || !directory.includes('routing-controller-baseline-owned-')) throw new Error('Owned routing cleanup boundary changed') if (dirname(resolve(directory)) !== resolve(root, 'tmp') || !basename(directory).startsWith(ownedPrefix)) throw new Error('Owned routing cleanup boundary changed')
await rm(join(directory, 'pg'), { recursive: true, force: true, maxRetries: 20, retryDelay: 100 }) await rm(join(directory, 'pg'), { recursive: true, force: true, maxRetries: 20, retryDelay: 100 })
console.log('Owned routing PostgreSQL stopped and its data removed; snapshot/log evidence retained') console.log('Owned routing PostgreSQL stopped and its data removed; snapshot/log evidence retained')
} }
+13 -7
View File
@@ -38,6 +38,7 @@ export function projectVrfDiagnostics(log, privateValues = [], expectedSlice = n
if (Array.isArray(value.accepted)) { row.finalCaseCount = value.accepted.length; if (expectedSlice !== null && value.slice === expectedSlice) finalComponentPresent = true } if (Array.isArray(value.accepted)) { row.finalCaseCount = value.accepted.length; if (expectedSlice !== null && value.slice === expectedSlice) finalComponentPresent = true }
if (['planned','queued','running','applied','failed','cancelled','unknown','expired','rejected','unconfirmed'].includes(value.operationState)) row.operationState = value.operationState if (['planned','queued','running','applied','failed','cancelled','unknown','expired','rejected','unconfirmed'].includes(value.operationState)) row.operationState = value.operationState
for (const key of ['phase', 'elapsedMs', 'nativeReads', 'nativeWrites', 'roundTrips', 'lastSqlMs', 'totalSqlMs', 'maxSqlMs']) if (Number.isSafeInteger(value[key]) && value[key] >= 0) row[key] = value[key] for (const key of ['phase', 'elapsedMs', 'nativeReads', 'nativeWrites', 'roundTrips', 'lastSqlMs', 'totalSqlMs', 'maxSqlMs']) if (Number.isSafeInteger(value[key]) && value[key] >= 0) row[key] = value[key]
if (Number.isSafeInteger(value.wireguardPeerTelemetryTicks) && value.wireguardPeerTelemetryTicks > 0 && value.wireguardPeerTelemetryTicks <= 10000) row.wireguardPeerTelemetryTicks = value.wireguardPeerTelemetryTicks
if (value.diagnosticWaitSql === true) row.diagnosticWaitSql = true if (value.diagnosticWaitSql === true) row.diagnosticWaitSql = true
if (value.diagnosticError === true) { row.diagnosticError = true; row.safeName = diagnosticNames.has(value.safeName) ? value.safeName : 'other'; row.safeCode = diagnosticCodes.has(value.safeCode) ? value.safeCode : 'other' } if (value.diagnosticError === true) { row.diagnosticError = true; row.safeName = diagnosticNames.has(value.safeName) ? value.safeName : 'other'; row.safeCode = diagnosticCodes.has(value.safeCode) ? value.safeCode : 'other' }
if (!Object.keys(row).length) { omittedLines++; continue } if (!Object.keys(row).length) { omittedLines++; continue }
@@ -47,10 +48,10 @@ export function projectVrfDiagnostics(log, privateValues = [], expectedSlice = n
} }
export function vrfSliceDiagnostic(result, log, childBudgetMs, privateValues = []) { export function vrfSliceDiagnostic(result, log, childBudgetMs, privateValues = []) {
assert(result && typeof result === 'object') assert(result && typeof result === 'object')
assert(['baseline','physical','vlan','disabled','mixed','reconcile','sql-journal','guards','coexistence','alias'].includes(result.slice)) assert(['baseline','physical','vlan','disabled','mixed','reconcile','sql-journal','guards','coexistence','alias','gre'].includes(result.slice))
assert(Number.isSafeInteger(result.code) && typeof result.childExitConfirmed === 'boolean') assert(Number.isSafeInteger(result.code) && typeof result.childExitConfirmed === 'boolean')
assert(Number.isSafeInteger(result.caseCount) && result.caseCount >= 0) assert(Number.isSafeInteger(result.caseCount) && result.caseCount >= 0)
assert.equal(childBudgetMs, result.slice === 'baseline' ? 240000 : 120000) assert.equal(childBudgetMs, result.slice === 'baseline' ? 240000 : result.slice === 'disabled' ? 180000 : 120000)
assert(result.failure === null || ['child-deadline','child-log-limit','child-signal','child-error','child-error-after-pid','child-exit-unconfirmed','missing-component-receipt'].includes(result.failure)) assert(result.failure === null || ['child-deadline','child-log-limit','child-signal','child-error','child-error-after-pid','child-exit-unconfirmed','missing-component-receipt'].includes(result.failure))
assert(result.accepted === (result.code === 0 && result.failure === null && result.childExitConfirmed)) assert(result.accepted === (result.code === 0 && result.failure === null && result.childExitConfirmed))
return { ...result, capMs: childBudgetMs, diagnostics: projectVrfDiagnostics(log, privateValues, result.slice), scope: 'same-owned-PG-test-only', actualRouter: false, forwardingVerified: false } return { ...result, capMs: childBudgetMs, diagnostics: projectVrfDiagnostics(log, privateValues, result.slice), scope: 'same-owned-PG-test-only', actualRouter: false, forwardingVerified: false }
@@ -88,10 +89,14 @@ export async function runOwnedVrfProductionProfile({ directory, database, databa
const report = fileURLToPath(new URL('../../docs/mcp-chr-vrf-companion-native-v5.json', import.meta.url)) const report = fileURLToPath(new URL('../../docs/mcp-chr-vrf-companion-native-v5.json', import.meta.url))
const loader = pathToFileURL(createRequire(import.meta.url).resolve('tsx')).href const loader = pathToFileURL(createRequire(import.meta.url).resolve('tsx')).href
const safe = Object.fromEntries(['SystemRoot', 'WINDIR', 'TEMP', 'TMP', 'LANG', 'LC_ALL'].filter(key => typeof process.env[key] === 'string').map(key => [key, process.env[key]])) const safe = Object.fromEntries(['SystemRoot', 'WINDIR', 'TEMP', 'TMP', 'LANG', 'LC_ALL'].filter(key => typeof process.env[key] === 'string').map(key => [key, process.env[key]]))
for (const slice of ['baseline', 'physical', 'vlan', 'disabled', 'mixed', 'reconcile', 'sql-journal', 'guards', 'coexistence', 'alias']) { // Diagnose the disabled lifecycle first, then the synthetic GRE extension.
// Every original case remains required by the same full74 gate and caps.
for (const slice of ['disabled', 'gre', 'baseline', 'physical', 'vlan', 'mixed', 'reconcile', 'sql-journal', 'guards', 'coexistence', 'alias']) {
currentSlice = slice; currentChildExitConfirmed = false; lastSliceResult = null currentSlice = slice; currentChildExitConfirmed = false; lastSliceResult = null
await persist(slice + '-started.json', { version: 1, slice, status: 'running', accepted: false, childExitConfirmed: false }) await persist(slice + '-started.json', { version: 1, slice, status: 'running', accepted: false, childExitConfirmed: false })
const childBudgetMs = slice === 'baseline' ? 240000 : 120000 // Nine disabled lifecycle operations need their own aggregate child cap;
// measured seven operations alone consumed 105.5s under the original120s.
const childBudgetMs = slice === 'baseline' ? 240000 : slice === 'disabled' ? 180000 : 120000
const cwd = join(owned, 'vrf-production-' + slice), rel = relative(owned, cwd) const cwd = join(owned, 'vrf-production-' + slice), rel = relative(owned, cwd)
assert(rel && !rel.startsWith('..') && !isAbsolute(rel)); await mkdir(cwd) assert(rel && !rel.startsWith('..') && !isAbsolute(rel)); await mkdir(cwd)
assert.equal(await realpath(cwd), cwd); assert(!(await lstat(cwd)).isSymbolicLink()) assert.equal(await realpath(cwd), cwd); assert(!(await lstat(cwd)).isSymbolicLink())
@@ -121,9 +126,10 @@ export async function runOwnedVrfProductionProfile({ directory, database, databa
unknownTimer = setTimeout(() => { if (!exited) { stop('child-exit-unconfirmed'); resolveExit(1) } }, childBudgetMs + 15000) unknownTimer = setTimeout(() => { if (!exited) { stop('child-exit-unconfirmed'); resolveExit(1) } }, childBudgetMs + 15000)
}) })
clearTimeout(deadline); clearTimeout(stopTimer); clearTimeout(unknownTimer) clearTimeout(deadline); clearTimeout(stopTimer); clearTimeout(unknownTimer)
const expectedCases = { baseline: 11, physical: 7, vlan: 9, disabled: 9, mixed: 7, reconcile: 6, 'sql-journal': 1, guards: 11, coexistence: 3, alias: 4 } const expectedCases = { baseline: 11, physical: 7, vlan: 9, disabled: 9, mixed: 7, reconcile: 6, 'sql-journal': 1, guards: 11, coexistence: 3, alias: 4, gre: 6 }
const component = log.trim().split(/\r?\n/).flatMap(line => { try { return [JSON.parse(line)] } catch { return [] } }).findLast(value => value.slice === slice && Array.isArray(value.accepted)) const component = log.trim().split(/\r?\n/).flatMap(line => { try { return [JSON.parse(line)] } catch { return [] } }).findLast(value => value.slice === slice && Array.isArray(value.accepted))
if (!component || component.accepted.length !== expectedCases[slice] || component.actualSql !== true || component.actualCrypto !== true || component.actualAuthPolicyApprovalRuntime !== true || component.fakeVerifier !== false || component.actualRouter !== false || component.forwardingVerified !== false) failure ??= 'missing-component-receipt' if (!component || component.accepted.length !== expectedCases[slice] || component.actualSql !== true || component.actualCrypto !== true || component.actualAuthPolicyApprovalRuntime !== true || component.fakeVerifier !== false || component.actualRouter !== false || component.forwardingVerified !== false) failure ??= 'missing-component-receipt'
if (slice === 'gre' && (!Number.isSafeInteger(component?.wireguardPeerTelemetryTicks) || component.wireguardPeerTelemetryTicks <= 6 || component.wireguardPeerTelemetryTicks > 10000)) failure ??= 'missing-component-receipt'
const result = { slice, accepted: code === 0 && !failure && exited, caseCount: component?.accepted?.length ?? 0, code, failure, childPid: child.pid ?? null, childExitConfirmed: exited } const result = { slice, accepted: code === 0 && !failure && exited, caseCount: component?.accepted?.length ?? 0, code, failure, childPid: child.pid ?? null, childExitConfirmed: exited }
lastSliceResult = result; currentChildExitConfirmed = exited lastSliceResult = result; currentChildExitConfirmed = exited
const diagnostic = vrfSliceDiagnostic(result, log, childBudgetMs, [databaseUrl, target.password, decodeURIComponent(target.password), authSecret]) const diagnostic = vrfSliceDiagnostic(result, log, childBudgetMs, [databaseUrl, target.password, decodeURIComponent(target.password), authSecret])
@@ -135,8 +141,8 @@ export async function runOwnedVrfProductionProfile({ directory, database, databa
console.log(JSON.stringify({ profile: 'test:vrf:production', ...result })) console.log(JSON.stringify({ profile: 'test:vrf:production', ...result }))
if (!result.accepted) break if (!result.accepted) break
} }
const receipt = { accepted: results.length === 10 && results.every(result => result.accepted), childExitConfirmed: results.every(result => result.childExitConfirmed), sameOwnedPg: true, actualRouter: false, forwardingVerified: false, results } const receipt = { accepted: results.length === 11 && results.every(result => result.accepted), childExitConfirmed: results.every(result => result.childExitConfirmed), sameOwnedPg: true, actualRouter: false, forwardingVerified: false, results }
await persist('terminal.json', { version: 1, status: 'terminal', ...receipt, requiredCases: 68, completedCases: results.reduce((sum, value) => sum + value.caseCount, 0), diagnosticDirectory: diagnosticPath }) await persist('terminal.json', { version: 1, status: 'terminal', ...receipt, requiredCases: 74, completedCases: results.reduce((sum, value) => sum + value.caseCount, 0), diagnosticDirectory: diagnosticPath })
await writeFile(join(owned, 'vrf-production-acceptance.json'), JSON.stringify(receipt, null, 2)) await writeFile(join(owned, 'vrf-production-acceptance.json'), JSON.stringify(receipt, null, 2))
return receipt return receipt
} catch { } catch {
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { vrfSliceDiagnostic } from './vrf-production-profile.mjs'
test('only the nine-operation disabled slice receives its measured aggregate budget', () => {
for (const [slice, capMs] of [['disabled', 180000], ['gre', 120000], ['baseline', 240000]]) {
const result = { slice, accepted: false, caseCount: 0, code: 1, failure: 'child-deadline', childExitConfirmed: true }
assert.equal(vrfSliceDiagnostic(result, '', capMs).capMs, capMs)
for (const otherCap of [120000, 180000, 240000].filter(value => value !== capMs)) assert.throws(() => vrfSliceDiagnostic(result, '', otherCap))
}
})
test('GRE lifecycle diagnostic retains the six-case receipt with existing privacy bounds', () => {
const accepted = ['vrf-create', 'vrf-rename', 'vrf-disable', 'vrf-reenable', 'vrf-membership-transfer-a-to-b', 'vrf-delete-addresses-remain']
const result = { slice: 'gre', accepted: true, caseCount: 6, code: 0, failure: null, childExitConfirmed: true }
const log = accepted.map((acceptedCase, index) => JSON.stringify({ slice: 'gre', acceptedCase, nativeWrites: 1, wireguardPeerTelemetryTicks: (index + 1) * 4, password: 'fixture-private-value' }))
log.push(JSON.stringify({ slice: 'gre', accepted, wireguardPeerTelemetryTicks: 24, rawShape: 'synthetic GRE lifecycle', password: 'fixture-private-value' }))
const diagnostic = vrfSliceDiagnostic(result, log.join('\n'), 120000, ['fixture-private-value'])
assert.deepEqual(diagnostic.diagnostics.acceptedCaseSequence, accepted)
assert.equal(diagnostic.diagnostics.finalComponentPresent, true)
assert.equal(diagnostic.diagnostics.rawLogRetained, false)
assert.deepEqual(diagnostic.diagnostics.records.map(row => row.wireguardPeerTelemetryTicks), [4, 8, 12, 16, 20, 24, 24])
assert.equal(diagnostic.actualRouter, false)
assert.equal(diagnostic.forwardingVerified, false)
assert.equal(JSON.stringify(diagnostic).includes('fixture-private-value'), false)
assert.throws(() => vrfSliceDiagnostic(result, '', 240000))
})
test('peer telemetry diagnostic rejects nonnumeric and out-of-budget values', () => {
const result = { slice: 'gre', accepted: false, caseCount: 0, code: 1, failure: 'missing-component-receipt', childExitConfirmed: true }
for (const wireguardPeerTelemetryTicks of [0, -1, 10001, 1.5, 'private-raw-value', { secret: 'private-raw-value' }]) {
const diagnostic = vrfSliceDiagnostic(result, JSON.stringify({ slice: 'gre', wireguardPeerTelemetryTicks }), 120000)
assert.equal(diagnostic.diagnostics.records.some(row => Object.hasOwn(row, 'wireguardPeerTelemetryTicks')), false)
assert.equal(JSON.stringify(diagnostic).includes('private-raw-value'), false)
}
})
test('a partial GRE lifecycle is diagnostic evidence without a final component receipt', () => {
const result = { slice: 'gre', accepted: false, caseCount: 0, code: 1, failure: 'child-deadline', childExitConfirmed: true }
const diagnostic = vrfSliceDiagnostic(result, JSON.stringify({ slice: 'gre', acceptedCase: 'vrf-create' }), 120000)
assert.equal(diagnostic.accepted, false)
assert.equal(diagnostic.diagnostics.finalComponentPresent, false)
assert.equal(diagnostic.diagnostics.lastAcceptedCase, 'vrf-create')
assert.equal(diagnostic.capMs, 120000)
})
+23 -6
View File
@@ -12,10 +12,10 @@ const reviewed = (source: string, extra: Partial<MigrationReview> = {}) => {
const bytes = Buffer.from(source), file = "9000_fixture.sql" const bytes = Buffer.from(source), file = "9000_fixture.sql"
return () => analyzeMigration(file, bytes, { file, sha256: createHash("sha256").update(bytes).digest("hex"), transaction: "owner-transaction", selfMarker: false, opaqueBodies: 0, bodyReview: [], effects: ["Explicit inert fixture review"], ...extra }) return () => analyzeMigration(file, bytes, { file, sha256: createHash("sha256").update(bytes).digest("hex"), transaction: "owner-transaction", selfMarker: false, opaqueBodies: 0, bodyReview: [], effects: ["Explicit inert fixture review"], ...extra })
} }
await test("all44 exact file pins and every top-level schema operation are inventoried", () => { await test("all 56 exact file pins and every top-level schema operation are inventoried", () => {
assert.equal(inventory.length, 44); assert.equal(MIGRATION_MANIFEST.length, 44) assert.equal(inventory.length, 56); assert.equal(MIGRATION_MANIFEST.length, 56)
assert.equal(inventory.filter(file => file.transaction === "owner-transaction").length, 17) assert.equal(inventory.filter(file => file.transaction === "owner-transaction").length, 17)
assert.equal(inventory.filter(file => file.transaction === "self-managed-single-transaction").length, 27) assert.equal(inventory.filter(file => file.transaction === "self-managed-single-transaction").length, 39)
assert(inventory.every(file => file.operations.length === file.statementCount && file.effects.length > 0)) assert(inventory.every(file => file.operations.length === file.statementCount && file.effects.length > 0))
let tables = new Set<string>() let tables = new Set<string>()
for (const file of inventory) { for (const file of inventory) {
@@ -32,8 +32,8 @@ await test("DO BEGIN and stored function BEGIN are never transaction boundaries"
assert.equal(endpoint.topLevelTransactionControls.length, 2); assert.equal(endpoint.opaqueBodies.length, 2) assert.equal(endpoint.topLevelTransactionControls.length, 2); assert.equal(endpoint.opaqueBodies.length, 2)
assert.deepEqual(endpoint.opaqueBodies.map(body => body.kind), ["stored-function-body", "executed-do-body"]) assert.deepEqual(endpoint.opaqueBodies.map(body => body.kind), ["stored-function-body", "executed-do-body"])
}) })
await test("0035 through0044 have exact atomic file-owned terminal markers", () => { await test("0035 through 0056 have exact atomic file-owned terminal markers", () => {
assert.deepEqual(inventory.filter(file => file.selfMarker).map(file => file.id), ["0035_server_probe_journal", "0036_diagnostic_cancellations", "0037_server_probe_storage_v2", "0038_server_poll_journal_and_writer_guards", "0039_server_poll_storage", "0040_mcp_routing_reservations", "0041_server_poll_terminal_archives", "0042_server_poll_settings_epoch", "0043_mcp_routing_forward_charges", "0044_mcp_routing_manual_inverse_charges"]) assert.deepEqual(inventory.filter(file => file.selfMarker).map(file => file.id), ["0035_server_probe_journal", "0036_diagnostic_cancellations", "0037_server_probe_storage_v2", "0038_server_poll_journal_and_writer_guards", "0039_server_poll_storage", "0040_mcp_routing_reservations", "0041_server_poll_terminal_archives", "0042_server_poll_settings_epoch", "0043_mcp_routing_forward_charges", "0044_mcp_routing_manual_inverse_charges", "0045_mcp_operation_replay_tombstones", "0046_server_poll_operation_retirement", "0047_server_poll_retirement_claims", "0048_mcp_ordinary_retirement_claims", "0049_mcp_ordinary_telemetry_retirement", "0050_server_probe_retired_sources", "0051_server_probe_retirement_claims", "0052_server_probe_retired_gc", "0053_server_probe_terminal_proofs", "0054_mcp_geoip_retirement", "0055_mcp_backup_schedule_retirement", "0056_mcp_manual_inverse_recovery_v8"])
for (const file of inventory.filter(file => file.selfMarker)) { assert.equal(file.selfMarker!.atomicWithAllFileEffects, true); assert.equal(file.selfMarker!.statement, file.statementCount - 2); assert.equal(file.currentOuterMarkerGap, false) } for (const file of inventory.filter(file => file.selfMarker)) { assert.equal(file.selfMarker!.atomicWithAllFileEffects, true); assert.equal(file.selfMarker!.statement, file.statementCount - 2); assert.equal(file.currentOuterMarkerGap, false) }
assert.equal(inventory.filter(file => file.currentOuterMarkerGap).length, 34) assert.equal(inventory.filter(file => file.currentOuterMarkerGap).length, 34)
}) })
@@ -134,7 +134,8 @@ await test("poll storage enrollment refuses history before any change and preser
for(const name of ["server_poll_executions","server_poll_intents","server_poll_acks","server_poll_receipts","server_poll_quarantines"])assert(guard.includes(`EXISTS(SELECT 1 FROM ${name})`)) for(const name of ["server_poll_executions","server_poll_intents","server_poll_acks","server_poll_receipts","server_poll_quarantines"])assert(guard.includes(`EXISTS(SELECT 1 FROM ${name})`))
for(const name of ["server_poll_storage_admissions","server_poll_storage_usage"])assert(guard.includes("'"+name+"'")&&CURRENT_REQUIRED_TABLES.includes(name)) for(const name of ["server_poll_storage_admissions","server_poll_storage_usage"])assert(guard.includes("'"+name+"'")&&CURRENT_REQUIRED_TABLES.includes(name))
const bodies=(list:ReturnType<typeof parseMigrationStatements>)=>list.filter(statement=>statement.tokens[0].value==="CREATE").flatMap(statement=>statement.tokens.filter(token=>token.kind==="dollar").map(token=>token.value)) const bodies=(list:ReturnType<typeof parseMigrationStatements>)=>list.filter(statement=>statement.tokens[0].value==="CREATE").flatMap(statement=>statement.tokens.filter(token=>token.kind==="dollar").map(token=>token.value))
assert.deepEqual(bodies(statements),bodies(historical));assert.equal(bodies(statements).length,5) const comparableBodies=(list:ReturnType<typeof parseMigrationStatements>)=>bodies(list).map(body=>body.replace(/\r\n/g,"\n"))
assert.deepEqual(comparableBodies(statements),comparableBodies(historical));assert.equal(bodies(statements).length,5)
assert(bodies(statements)[0].includes("mcp_settings.id=1 FOR SHARE"),"Predicate itself fences current retention, including direct guarded deletes") assert(bodies(statements)[0].includes("mcp_settings.id=1 FOR SHARE"),"Predicate itself fences current retention, including direct guarded deletes")
assert.deepEqual(migration.operations.filter(operation=>operation.command==="INSERT").map(operation=>operation.target),["server_poll_storage_usage","schema_migrations"]) assert.deepEqual(migration.operations.filter(operation=>operation.command==="INSERT").map(operation=>operation.target),["server_poll_storage_usage","schema_migrations"])
assert.equal(migration.operations.some(operation=>["UPDATE","DELETE","DROP"].includes(operation.command)),false) assert.equal(migration.operations.some(operation=>["UPDATE","DELETE","DROP"].includes(operation.command)),false)
@@ -227,4 +228,20 @@ await test("0044 adds only a fixed manual inverse ledger and current-version int
assert(!pin.operations.some(operation => operation.command === "UPDATE" || operation.command === "DELETE")) assert(!pin.operations.some(operation => operation.command === "UPDATE" || operation.command === "DELETE"))
}) })
await test("0056 changes only live manual inverse recovery version guards", async () => {
const migration = inventory.find(file => file.id === "0056_mcp_manual_inverse_recovery_v8")!
assert.equal(migration.statementCount, 5)
assert.equal(migration.opaqueBodies.length, 2)
assert.equal(migration.selfMarker?.atomicWithAllFileEffects, true)
const oldSql = await readFile(new URL("../../drizzle/0044_mcp_routing_manual_inverse_charges.sql", import.meta.url), "utf8")
const newSql = await readFile(new URL("../../drizzle/0056_mcp_manual_inverse_recovery_v8.sql", import.meta.url), "utf8")
for (const name of ["mcp_routing_manual_inverse_charge_source", "mcp_scoped_recovery_manual_version"]) {
const definition = new RegExp(`CREATE(?: OR REPLACE)? FUNCTION public\\.${name}\\(\\)[\\s\\S]*?END \\$\\$;`)
const original = oldSql.match(definition)?.[0]?.replaceAll("\r\n", "\n")
assert.ok(original)
assert.equal(newSql.match(definition)?.[0]?.replaceAll("\r\n", "\n"), original.replace("CREATE FUNCTION", "CREATE OR REPLACE FUNCTION")
.replace("p.operation_id='operations.recover' AND p.operation_version=7", "p.operation_id='operations.recover' AND p.operation_version=8"))
}
})
console.log(`migration-manifest: ${cases} inert parser/inventory/baseline scenarios passed; ${inventory.length} exact files`) console.log(`migration-manifest: ${cases} inert parser/inventory/baseline scenarios passed; ${inventory.length} exact files`)
+167 -1
View File
@@ -702,6 +702,172 @@ const classifiedMigrationPins: readonly MigrationReview[] = [
"Adds one one-unit immutable manual inverse charge table, two charge indexes, one manual-history page index, and four triggers. Existing43 bytes, reserved2 and generic debit ownership remain unchanged.", "Adds one one-unit immutable manual inverse charge table, two charge indexes, one manual-history page index, and four triggers. Existing43 bytes, reserved2 and generic debit ownership remain unchanged.",
"No inferred/backfilled identity, legacy timestamp debit, permission change, source enrollment, public controller activation or READY. Full RT3/all-writer/family-deletion proof remains open." "No inferred/backfilled identity, legacy timestamp debit, permission change, source enrollment, public controller activation or READY. Full RT3/all-writer/family-deletion proof remains open."
] ]
},
{
"file": "0045_mcp_operation_replay_tombstones.sql",
"sha256": "8edf5b074c7eef49dad7835c44ac2539df4347f19eb9dd9b2aa7005bafaadbc0",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 0,
"bodyReview": [],
"effects": [
"Adds one empty replay identity table with plan/operation uniqueness and connection-scoped idempotency digest uniqueness.",
"Table creation refuses an unmarked collision; the exact marker commits with the schema. No backfill, operation deletion, or worker activation."
]
},
{
"file": "0046_server_poll_operation_retirement.sql",
"sha256": "75d7956ea8134b01bcc88da083b77eb52f6e1ad70ad854918ed3380cb0e8cf88",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 4,
"bodyReview": [
"Archive guard permits only fixed receipt-scoped V1 to V2 updates with matching approved terminal operation, tombstone and SQL 180-day cutoff.",
"Deferred guard independently requires the tombstone, physically absent operation and plan, and exact ledger accounting.",
"Admission guard retains existing paths and applies exact V2 archive-byte delta to both owners without changing row charges or lifetime fields.",
"Existing GC retains its authority/activity cutoff and admits retired identity only with exact poll tombstone and compatible V2 archive when present."
],
"effects": [
"Extends the same archive table to a bounded version 2, changes three guards and adds one deferred retirement guard.",
"No automatic conversion, worker activation, operation deletion or retention-window adjustment in migration."
]
},
{
"file": "0047_server_poll_retirement_claims.sql",
"sha256": "8c01ab14d241f715af021e8a7d7f5bb273b450b32f566140359260282524e01d",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 2,
"bodyReview": [
"Protects exact current-transaction V1 claim insertion and permits deletion only as the owning admission is cascaded by GC.",
"Requires exact receipt, V1 archive proof and claim token already committed in another transaction before V2 update."
],
"effects": [
"Adds one durable one-shot poll retirement claim per admission, cascading only with that admission's existing guarded GC.",
"No migration-time conversion, backfill, dispatch, or quota change."
]
},
{
"file": "0048_mcp_ordinary_retirement_claims.sql",
"sha256": "5bc3598c9f5d09611e98d05345accda6d39b199a583bba789b97a24140fafbfd",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 1,
"bodyReview": [
"Makes ordinary claims immutable and allows insert only for the exact linked settled and aged logs.settings.update v1 operation."
],
"effects": [
"Adds one durable ordinary-operation claim with an authenticated source digest. No migration-time data purge or backfill."
]
},
{
"file": "0049_mcp_ordinary_telemetry_retirement.sql",
"sha256": "68993e924d20db6b08973846a0da367fe89fe2948c414c810e9b171dbbae6761",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 1,
"bodyReview": [
"Extends the same immutable ordinary claim guard to four explicitly versioned settled telemetry settings families."
],
"effects": [
"Replaces only the ordinary claim guard function. No backfill, data purge, or changes to poll or diagnostic storage."
]
},
{
"file": "0050_server_probe_retired_sources.sql",
"sha256": "0468ac7e53638b532557273f55d065ff4cde092d8b08073f5860bab35263aead",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 1,
"bodyReview": [
"Refuses update, delete, and truncate of a signed retired diagnostic source pending a separately reviewed lifecycle."
],
"effects": [
"Creates empty bounded storage linking exact diagnostic admission and replay tombstone IDs. No backfill, retirement, or GC activation."
]
},
{
"file": "0051_server_probe_retirement_claims.sql",
"sha256": "b38876b5da44cf14e20a9df0baaa233ea4de81e471b38dddd370e149c8116243",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 2,
"bodyReview": [
"Restricts diagnostic claims to their original settled 180-day source and makes each claim permanent.",
"Restricts archival insertion to the exact committed claim and current original source and tombstone identity."
],
"effects": [
"Creates empty diagnostic retirement claims and guards the archive insert. No conversion, backfill, or GC activation."
]
},
{
"file": "0052_server_probe_retired_gc.sql",
"sha256": "ea56a1f1bdabfce606bdfec56b6a7b31d45b0187fbea4c900bc656ae320f2fdd",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 3,
"bodyReview": [
"Validates the exact retired diagnostic identity and the later of current and persisted retention plus the original safety window.",
"Only permits archive removal during an exact guarded admission cascade after journal removal.",
"Adds the exact retired GC branch while retaining the live admission eligibility predicate."
],
"effects": [
"Changes only the retired source admission FK to guarded cascade and adds an explicit exact GC path. No collection runs during migration."
]
},
{
"file": "0053_server_probe_terminal_proofs.sql",
"sha256": "759ec8c349fcb24c43af410ad6ade714432678b3a021c94bd982080d6a8986be",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 2,
"bodyReview": [
"Pins terminal proof to the current retained original on insert and an exact committed retirement claim on delete.",
"Extends claims only to the identical signed terminal proof when the original diagnostic admission is gone."
],
"effects": [
"Creates empty bounded private terminal proof storage. Existing payload retention, scheduler, and archive GC remain unchanged."
]
},
{
"file": "0054_mcp_geoip_retirement.sql",
"sha256": "680390f98076e3047dbb5a34eab417b253853703932d16c17e4fa3a11060accc",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 1,
"bodyReview": [
"Adds only exact settled GeoIP settings version 1 to the existing immutable ordinary retirement claim guard."
],
"effects": [
"Replaces the ordinary claim guard function without changing settings or retiring existing rows during migration."
]
},
{
"file": "0055_mcp_backup_schedule_retirement.sql",
"sha256": "07e63c1245ea547176d613933a81ad354e734fa64218dadced67ee8f965c01b6",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 1,
"bodyReview": [
"Adds only exact settled backup schedule settings version 1 to the existing immutable ordinary retirement claim guard."
],
"effects": [
"Replaces the ordinary claim guard function without changing backup settings, metadata or entries during migration."
]
},
{
"file": "0056_mcp_manual_inverse_recovery_v8.sql",
"sha256": "1dece1d9c5a9cf69e51336f35f868561dad9da6d21556ea31ef5b102634ed9ee",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 2,
"bodyReview": [
"The manual inverse charge-source guard requires recovery version 8 and preserves its pending operation, source version, owner and profile checks.",
"The manual inverse intent guard requires recovery version 8 and preserves its advisory lock and exact running service intent checks."
],
"effects": [
"Replaces the two live version guard functions and preserves existing charges, intents, and plans."
]
} }
] ]
@@ -724,7 +890,7 @@ export const migrationManifestDigest = () => hash(JSON.stringify(MIGRATION_MANIF
// resetting the accumulated public table set at the reviewed destructive0002. // resetting the accumulated public table set at the reviewed destructive0002.
// This is a necessary presence check, not a substitute for exact live column, // This is a necessary presence check, not a substitute for exact live column,
// constraint/index/function/trigger comparison by the control protocol. // constraint/index/function/trigger comparison by the control protocol.
export const CURRENT_REQUIRED_TABLES: readonly string[] = Object.freeze(["acme_settings","alert_engine_confirm_pending","alert_engine_cursor","alert_engine_prev_live","alert_engine_state","alert_groups","alert_history","alert_outbox","alert_rule_conditions","alert_rule_targets","alert_rules","alert_telegram_settings","app_users","backup_entries","backup_schedule_settings","backup_storage_settings","certificate_issue_jobs","certificate_renew_settings","config_revisions","data_migration","events","evobgp_settings","filter_rules","flow_asn_meta","flow_buckets","flow_daily_dims","flow_daily_facts","flow_hour_facts","flow_ip_meta","flow_minute_dims","flow_minute_stats","flow_settings_intents","flow_write_receipts","geoip_settings","internet_path_settings","internet_path_snapshots","mcp_artifacts","mcp_audit","mcp_budget_buckets","mcp_connections","mcp_credentials","mcp_diagnostic_cancellations","mcp_drift","mcp_grants","mcp_management_channels","mcp_oauth_clients","mcp_oauth_requests","mcp_operation_checkpoints","mcp_operation_steps","mcp_operations","mcp_owner_revocations","mcp_path_checks","mcp_plans","mcp_portal_events","mcp_protections","mcp_read_leases","mcp_recovery_intents","mcp_requests","mcp_routing_forward_charges","mcp_routing_manual_inverse_charges","mcp_routing_observations","mcp_routing_profiles","mcp_routing_reservation_slots","mcp_routing_reservations","mcp_scoped_recovery_intents","mcp_secrets","mcp_settings","mcp_unauthorized_buckets","recursive_routes","router_log_collection_state","router_log_entries","router_log_settings","scheduler_runs","schema_migrations","server_poll_acks","server_poll_archives","server_poll_executions","server_poll_intents","server_poll_quarantines","server_poll_receipts","server_poll_storage_admissions","server_poll_storage_usage","server_probe_execution_payloads","server_probe_execution_quarantines","server_probe_execution_stages","server_probe_executions","server_probe_storage_admissions","server_probe_storage_usage","server_snapshots","servers","servers_api_ping_settings","servers_rest_ping_samples","traffic_collection_state","traffic_flow_settings","traffic_samples","traffic_settings","uptime_probe_samples","uptime_probes","uptime_resource_samples","uptime_settings","uptime_speed_probes","uptime_speed_test_runs","user_interface_bindings","vrf_production_tickets"]) export const CURRENT_REQUIRED_TABLES: readonly string[] = Object.freeze(["acme_settings","alert_engine_confirm_pending","alert_engine_cursor","alert_engine_prev_live","alert_engine_state","alert_groups","alert_history","alert_outbox","alert_rule_conditions","alert_rule_targets","alert_rules","alert_telegram_settings","app_users","backup_entries","backup_schedule_settings","backup_storage_settings","certificate_issue_jobs","certificate_renew_settings","config_revisions","data_migration","events","evobgp_settings","filter_rules","flow_asn_meta","flow_buckets","flow_daily_dims","flow_daily_facts","flow_hour_facts","flow_ip_meta","flow_minute_dims","flow_minute_stats","flow_settings_intents","flow_write_receipts","geoip_settings","internet_path_settings","internet_path_snapshots","mcp_artifacts","mcp_audit","mcp_budget_buckets","mcp_connections","mcp_credentials","mcp_diagnostic_cancellations","mcp_drift","mcp_grants","mcp_management_channels","mcp_oauth_clients","mcp_oauth_requests","mcp_operation_checkpoints","mcp_operation_replay_tombstones","mcp_operation_steps","mcp_operations","mcp_ordinary_retirement_claims","mcp_owner_revocations","mcp_path_checks","mcp_plans","mcp_portal_events","mcp_protections","mcp_read_leases","mcp_recovery_intents","mcp_requests","mcp_routing_forward_charges","mcp_routing_manual_inverse_charges","mcp_routing_observations","mcp_routing_profiles","mcp_routing_reservation_slots","mcp_routing_reservations","mcp_scoped_recovery_intents","mcp_secrets","mcp_settings","mcp_unauthorized_buckets","recursive_routes","router_log_collection_state","router_log_entries","router_log_settings","scheduler_runs","schema_migrations","server_poll_acks","server_poll_archives","server_poll_executions","server_poll_intents","server_poll_quarantines","server_poll_receipts","server_poll_retirement_claims","server_poll_storage_admissions","server_poll_storage_usage","server_probe_execution_payloads","server_probe_execution_quarantines","server_probe_execution_stages","server_probe_executions","server_probe_retired_sources","server_probe_retirement_claims","server_probe_storage_admissions","server_probe_storage_usage","server_probe_terminal_proofs","server_snapshots","servers","servers_api_ping_settings","servers_rest_ping_samples","traffic_collection_state","traffic_flow_settings","traffic_samples","traffic_settings","uptime_probe_samples","uptime_probes","uptime_resource_samples","uptime_settings","uptime_speed_probes","uptime_speed_test_runs","user_interface_bindings","vrf_production_tickets"])
export interface LegacyBaselineEvidence { export interface LegacyBaselineEvidence {
inventoryComplete: boolean; databaseGeneration: string; unresolvedAttemptIds: readonly string[] inventoryComplete: boolean; databaseGeneration: string; unresolvedAttemptIds: readonly string[]
+1 -1
View File
@@ -5,7 +5,7 @@ import type { ConnectionRow, McpPrincipal } from "./store.js"
import type { RosSnapshot, RosStep } from "./resources.js" import type { RosSnapshot, RosStep } from "./resources.js"
export interface ActionContext { export interface ActionContext {
connection: ConnectionRow; principal: McpPrincipal; signal?: AbortSignal connection: ConnectionRow; principal: McpPrincipal; signal?: AbortSignal; logsFollowMaintenance?: AbortSignal
/** Process-internal persistent controller identity; never parsed from tool JSON. */ /** Process-internal persistent controller identity; never parsed from tool JSON. */
persistentRoutingProfile?: { id: string; version: number } persistentRoutingProfile?: { id: string; version: number }
/** Available only during target resolution, never retained by the read/prepare handler. */ /** Available only during target resolution, never retained by the read/prepare handler. */
+62 -14
View File
@@ -9,14 +9,17 @@ import { registerIpsecExistingCertificateExportActions } from "./ipsec-existing-
import { registerIpsecUserUpdateActions } from "./ipsec-user-update-actions.js" import { registerIpsecUserUpdateActions } from "./ipsec-user-update-actions.js"
import { prepareIpsecNativeWrite } from "./ipsec-native-write-actions.js" import { prepareIpsecNativeWrite } from "./ipsec-native-write-actions.js"
import { isIP } from "node:net" import { isIP } from "node:net"
import { setTimeout as sleep } from "node:timers/promises"
import { logsFollowCancellation } from "./logs-follow-cancellation.js"
import { z } from "zod" import { z } from "zod"
import { logFiltersSchema, logSettingsPatchSchema } from "@mmapp/contracts" import { logFiltersSchema, logSettingsPatchSchema } from "@mmapp/contracts"
import { appUserCreateSchema, appUserUpdateSchema, appUserRoleSchema, sectionPermSchema, serverPermSchema, userBindingCreateSchema } from "@mmapp/contracts/users" import { appUserCreateSchema, appUserUpdateSchema, appUserRoleSchema, sectionPermSchema, serverPermSchema, userBindingCreateSchema } from "@mmapp/contracts/users"
import type { McpOperationDescriptor } from "@mmapp/contracts/mcp" import type { McpOperationDescriptor } from "@mmapp/contracts/mcp"
import { hasPermission } from "../../lib/permissions.js" import { hasPermission } from "../../lib/permissions.js"
import { assertOperationReplayScope, findOperationReplayTombstone, operationHistoryExpired } from "./operation-replay-tombstones.js"
import { pool } from "../../db/index.js" import { pool } from "../../db/index.js"
import { getServerReadById } from "../servers/service/servers-service.js" import { getServerReadById } from "../servers/service/servers-service.js"
import { listUsers, getUserById, listInterfaceCatalog, getInterfaceCatalogState } from "../users/service/users-service.js" import { listUsers, getUserById, listInterfaceCatalog, getInterfaceCatalogState, validateUserBindingCreate, UsersServiceError } from "../users/service/users-service.js"
import { getLogSettings } from "../../services/router-logs-collector.js" import { getLogSettings } from "../../services/router-logs-collector.js"
import { readLogPage, readLogSummary, readLogContext, readLogEntry } from "../../services/router-logs-query.js" import { readLogPage, readLogSummary, readLogContext, readLogEntry } from "../../services/router-logs-query.js"
import { logWhere } from "../../services/router-logs-model.js" import { logWhere } from "../../services/router-logs-model.js"
@@ -103,6 +106,7 @@ import { registerRecursiveRecovery } from "./recursive-recovery.js"
import { registerNetworkHostActions } from "./network-host-actions.js" import { registerNetworkHostActions } from "./network-host-actions.js"
import { registerBgpFilterBindingActions } from "./bgp-filter-binding-actions.js" import { registerBgpFilterBindingActions } from "./bgp-filter-binding-actions.js"
import { registerBgpCommunityPolicyActions } from "./bgp-community-policy-actions.js" import { registerBgpCommunityPolicyActions } from "./bgp-community-policy-actions.js"
import { registerBridgeVlanActions, prepareGenericBridgeVlanChange } from "./bridge-vlan-actions.js"
const serverId = z.number().int().positive().max(Number.MAX_SAFE_INTEGER) const serverId = z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
const rowId = z.string().trim().min(1).max(200) const rowId = z.string().trim().min(1).max(200)
@@ -126,6 +130,7 @@ registerRecursiveRecovery(add)
registerNetworkHostActions(add) registerNetworkHostActions(add)
registerBgpFilterBindingActions(add) registerBgpFilterBindingActions(add)
registerBgpCommunityPolicyActions(add) registerBgpCommunityPolicyActions(add)
registerBridgeVlanActions(add)
registerTrafficHistoryActions(add) registerTrafficHistoryActions(add)
registerSidebarCountsActions(add) registerSidebarCountsActions(add)
registerLogArtifactActions(add) registerLogArtifactActions(add)
@@ -153,7 +158,7 @@ for (const resource of rosResources) {
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 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" ? 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, { 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" ? 1 : 0), 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) => { prepare: async (input, context) => {
if (resource.id === "vrf" && input.productionAdmission !== true) throw new McpError("UNSUPPORTED_CAPABILITY", "VRF требует явного productionAdmission:true для проверенного native executor; raw план недоступен", 409) if (resource.id === "vrf" && input.productionAdmission !== true) throw new McpError("UNSUPPORTED_CAPABILITY", "VRF требует явного productionAdmission:true для проверенного native executor; raw план недоступен", 409)
const sid = Number(input.serverId), id = input.id as string | undefined const sid = Number(input.serverId), id = input.id as string | undefined
@@ -249,13 +254,24 @@ for (const resource of rosResources) {
if (dependent) throw new McpError("STATE_CONFLICT", "Объект используется: сначала подготовьте явное изменение зависимостей", 409) if (dependent) throw new McpError("STATE_CONFLICT", "Объект используется: сначала подготовьте явное изменение зависимостей", 409)
} }
} }
const linkedVlan = await prepareGenericBridgeVlanChange({ serverId: sid, resource: resource.id, action, id, fields }, context)
if (linkedVlan) {
const captured = new Map(snapshots.map(snapshot => [snapshot.resource, snapshot]))
for (const snapshot of linkedVlan.snapshots) {
const previous = captured.get(snapshot.resource)
if (previous && (previous.fingerprint !== snapshot.fingerprint || previous.sourceProof !== snapshot.sourceProof)) throw new McpError("STATE_CONFLICT", "Зависимости VLAN изменились во время подготовки", 409)
captured.set(snapshot.resource, snapshot)
}
const objects = new Map([...references, ...linkedVlan.objects].map(object => [`${object.serverId}/${object.kind}/${object.id}`, object]))
return { ...linkedVlan, snapshots: [...captured.values()], objects: [...objects.values()] }
}
const placeBeforeId = resource.id === "vrf" && action === "create" ? before.rows[0]?.[".id"] : undefined const placeBeforeId = resource.id === "vrf" && action === "create" ? before.rows[0]?.[".id"] : undefined
if (resource.id === "vrf" && action === "create" && typeof placeBeforeId !== "string") throw new McpError("STATE_CONFLICT", "Не подтверждён порядок существующих VRF", 409) if (resource.id === "vrf" && action === "create" && typeof placeBeforeId !== "string") throw new McpError("STATE_CONFLICT", "Не подтверждён порядок существующих VRF", 409)
return { target: { scope: "servers", serverIds: [sid] }, objects: references, snapshots, return { target: { scope: "servers", serverIds: [sid] }, objects: references, snapshots,
...(resource.id === "vrf" && input.productionAdmission === true ? { vrfProductionAdmission: true as const } : {}), ...(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 })), steps: splitOspfPassiveUpdate({ serverId: sid, resource: resource.id, action, id, fields, ...(typeof placeBeforeId === "string" ? { placeBeforeId } : {}) }).map(step => ({ kind: "ros" as const, step })),
requiresHuman: true, requiresHuman: true,
warnings: ["Нативное изменение может затронуть управление устройством и требует подтверждения человеком.", "Автоматическое каскадное изменение зависимостей не выполняется.", ...(placeBeforeId ? [`VRF создаётся перед записью ${placeBeforeId}; порядок определяет принадлежность интерфейсов.`] : [])], warnings: ["Нативное изменение может затронуть управление устройством и требует подтверждения человеком.", "Автоматическое каскадное изменение зависимостей не выполняется.", ...(resource.id === "vrf" && action === "delete" ? ["При удалении VRF RouterOS может удалить автоматический IPv6 link-local и его connected-маршрут пустого bridge; статические адреса сохраняются."] : []), ...(placeBeforeId ? [`VRF создаётся перед записью ${placeBeforeId}; порядок определяет принадлежность интерфейсов.`] : [])],
} }
}, },
}) })
@@ -267,7 +283,7 @@ for (const definition of [
{ id: "firewall.update", tool: "mm_firewall_rule_update_plan", title: "План изменения правила firewall", resources: ["ip_filter", "ip_nat", "ip_mangle", "ip_raw", "ipv6_filter", "ipv6_nat", "ipv6_mangle", "ipv6_raw"] }, { id: "firewall.update", tool: "mm_firewall_rule_update_plan", title: "План изменения правила firewall", resources: ["ip_filter", "ip_nat", "ip_mangle", "ip_raw", "ipv6_filter", "ipv6_nat", "ipv6_mangle", "ipv6_raw"] },
] as const) { ] as const) {
add(definition.id, definition.tool, definition.title, "network", "mm:network:write", "write", "servers", z.object({ serverId, resource: z.enum(definition.resources), id: rowId, fields: z.record(z.string().max(100), z.union([z.string().max(1000), z.number().finite(), z.boolean()])) }).strict(), { add(definition.id, definition.tool, definition.title, "network", "mm:network:write", "write", "servers", z.object({ serverId, resource: z.enum(definition.resources), id: rowId, fields: z.record(z.string().max(100), z.union([z.string().max(1000), z.number().finite(), z.boolean()])) }).strict(), {
version: definition.id === "interfaces.update" ? 4 : 2, version: definition.id === "interfaces.update" ? 5 : 2,
prepare: async (input, context) => { prepare: async (input, context) => {
const resource = resourceById(String(input.resource)), fields = resource.fields.safeParse(input.fields) const resource = resourceById(String(input.resource)), fields = resource.fields.safeParse(input.fields)
if (!fields.success) throw new McpError("INVALID_INPUT", "Поля не соответствуют типизированной схеме выбранного объекта", 400) if (!fields.success) throw new McpError("INVALID_INPUT", "Поля не соответствуют типизированной схеме выбранного объекта", 400)
@@ -321,6 +337,21 @@ for (const action of ["create", "update", "delete", "binding_add", "binding_remo
const ids = [...new Set([...(user?.servers ?? []).map(server => Number(server.serverId)), ...(binding?.serverId ? [binding.serverId] : []), const ids = [...new Set([...(user?.servers ?? []).map(server => Number(server.serverId)), ...(binding?.serverId ? [binding.serverId] : []),
...(before?.bindings ?? []).map(binding => binding.serverId), ...(before?.servers ?? []).map(server => Number(server.serverId))])] ...(before?.bindings ?? []).map(binding => binding.serverId), ...(before?.servers ?? []).map(server => Number(server.serverId))])]
if (ids.some(id => !context.connection.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Сервер не разрешён") if (ids.some(id => !context.connection.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Сервер не разрешён")
if (action === "binding_remove" && !before?.bindings.some(binding => binding.id === input.bindingId)) {
throw new McpError("NOT_FOUND", "Привязка не найдена", 404)
}
if (action === "binding_add") {
try {
await validateUserBindingCreate(String(input.id), userBindingCreateSchema.parse(input.binding), { signal: context.signal })
} catch (error) {
if (error instanceof UsersServiceError) {
if (error.status === 409) throw new McpError("STATE_CONFLICT", error.message, 409)
if (error.status === 400) throw new McpError("INVALID_INPUT", error.message, 400)
if (error.status === 404) throw new McpError("NOT_FOUND", error.message, 404)
}
throw error
}
}
return { target: { scope: "global" }, objects: [{ kind: "user", id: String(input.id ?? "new") }, ...ids.map(id => ({ kind: "server", id: String(id), serverId: id }))], snapshots: [], before, return { target: { scope: "global" }, objects: [{ kind: "user", id: String(input.id ?? "new") }, ...ids.map(id => ({ kind: "server", id: String(id), serverId: id }))], snapshots: [], before,
steps: [{ kind: "service", service: `users.${action}`, input }], warnings: ["Изменение /users не меняет JWT-права портала."] } steps: [{ kind: "service", service: `users.${action}`, input }], warnings: ["Изменение /users не меняет JWT-права портала."] }
}, },
@@ -349,16 +380,17 @@ function logFilters(input: Record<string, unknown>, context: ActionContext) {
} }
add("logs.query", "mm_logs_query", "История логов", "logs", "mm:servers:read", "read", "servers", logInput, { add("logs.query", "mm_logs_query", "История логов", "logs", "mm:servers:read", "read", "servers", logInput, {
read: async (input, context) => { read: async (input, context) => {
const filters = logFilters(input, context), generation = await accessGeneration() const generation = await accessGeneration()
let filters: ReturnType<typeof logFilters>
let cursor: string | undefined, watermark: string | undefined let cursor: string | undefined, watermark: string | undefined
if (input.cursor) { if (input.cursor) {
try { try {
const state = JSON.parse(await decryptSecret(String(input.cursor))) as { generation: string; scope: string; cursor: string; watermark: string; from: string } const state = JSON.parse(await decryptSecret(String(input.cursor))) as { generation: string; scope: string; cursor: string; watermark: string; from: string }
if (input.from === undefined) filters.from = state.from filters = logFilters({ ...input, from: input.from ?? state.from }, context)
if (state.generation !== generation || state.scope !== fingerprint({ filters, connection: context.connection.id })) throw new Error("cursor") if (state.generation !== generation || state.scope !== fingerprint({ filters, connection: context.connection.id })) throw new Error("cursor")
cursor = state.cursor; watermark = state.watermark cursor = state.cursor; watermark = state.watermark
} catch { throw new McpError("RESET_REQUIRED", "Курсор устарел или относится к другим фильтрам", 409) } } catch { throw new McpError("RESET_REQUIRED", "Курсор устарел или относится к другим фильтрам", 409) }
} } else filters = logFilters(input, context)
const result = await readLogPage(filters, { cursor, watermark, limit: Number(input.limit) }) const result = await readLogPage(filters, { cursor, watermark, limit: Number(input.limit) })
return { ...result, nextCursor: result.nextCursor ? await encryptSecret(JSON.stringify({ generation, scope: fingerprint({ filters, connection: context.connection.id }), cursor: result.nextCursor, watermark: result.watermark, from: filters.from })) : null } return { ...result, nextCursor: result.nextCursor ? await encryptSecret(JSON.stringify({ generation, scope: fingerprint({ filters, connection: context.connection.id }), cursor: result.nextCursor, watermark: result.watermark, from: filters.from })) : null }
}, },
@@ -373,30 +405,41 @@ for (const context of [false, true]) add(context ? "logs.context" : "logs.get",
return context ? readLogContext(String(input.id)) : entry return context ? readLogContext(String(input.id)) : entry
}, },
}) })
add("logs.follow", "mm_logs_follow", "Новые события", "logs", "mm:servers:read", "read", "servers", logInput.safeExtend({ serverId, cursor: z.string().max(8192).optional(), waitSeconds: z.number().int().min(0).max(30).default(30) }), { add("logs.follow", "mm_logs_follow", "Новые события", "logs", "mm:servers:read", "read", "servers", logInput.safeExtend({ serverId, limit: logInput.shape.limit.default(200), cursor: z.string().max(8192).optional(), waitSeconds: z.number().int().min(0).max(30).default(30) }), {
read: async (input, context) => { read: async (input, context) => {
const maintenance = context.logsFollowMaintenance
const signal = context.signal
const waitSignal = maintenance && signal ? AbortSignal.any([maintenance, signal]) : maintenance ?? signal
const assertActive = () => { const failure = logsFollowCancellation(maintenance, signal); if (failure) throw failure }
assertActive()
const { server } = await routerClient(Number(input.serverId)); void server const { server } = await routerClient(Number(input.serverId)); void server
assertActive()
const release = subscribeLogDevice(Number(input.serverId)), until = Date.now() + Number(input.waitSeconds) * 1000 const release = subscribeLogDevice(Number(input.serverId)), until = Date.now() + Number(input.waitSeconds) * 1000
try { try {
const filters = logFilters(input, context), generation = await accessGeneration() const generation = await accessGeneration()
let filters: ReturnType<typeof logFilters>
let cursor = "0" let cursor = "0"
if (input.cursor) { if (input.cursor) {
try { try {
const saved = JSON.parse(await decryptSecret(String(input.cursor))) const saved = JSON.parse(await decryptSecret(String(input.cursor)))
filters.from = saved.from filters = logFilters({ ...input, from: input.from ?? saved.from }, context)
if (saved.kind !== "follow" || saved.generation !== generation || saved.scope !== fingerprint({ filters, connection: context.connection.id }) || !/^\d{1,20}$/.test(saved.cursor)) throw new Error("cursor") if (saved.kind !== "follow" || saved.generation !== generation || saved.scope !== fingerprint({ filters, connection: context.connection.id }) || !/^\d{1,20}$/.test(saved.cursor)) throw new Error("cursor")
cursor = saved.cursor cursor = saved.cursor
} catch { throw new McpError("RESET_REQUIRED", "Курсор follow устарел или относится к другим фильтрам", 409) } } catch { throw new McpError("RESET_REQUIRED", "Курсор follow устарел или относится к другим фильтрам", 409) }
} else { } else {
filters = logFilters(input, context)
cursor = (await pool.query("SELECT COALESCE(max(id),0)::text AS cursor FROM router_log_entries WHERE server_id=$1", [input.serverId])).rows[0].cursor cursor = (await pool.query("SELECT COALESCE(max(id),0)::text AS cursor FROM router_log_entries WHERE server_id=$1", [input.serverId])).rows[0].cursor
} }
while (true) { while (true) {
assertActive()
const current = await validatePrincipal(context.principal) const current = await validatePrincipal(context.principal)
assertActive()
if (current.policy.version !== context.connection.policy.version) throw new McpError("RESET_REQUIRED", "Права или область доступа изменились; начните новое чтение", 409) if (current.policy.version !== context.connection.policy.version) throw new McpError("RESET_REQUIRED", "Права или область доступа изменились; начните новое чтение", 409)
const batch = await readLogStream(filters, cursor) const batch = await readLogStream(filters, cursor, Number(input.limit))
assertActive()
cursor = batch.cursor cursor = batch.cursor
if (batch.entries.length || !batch.enabled || Date.now() >= until || context.signal?.aborted) return { ...batch, cursor: await encryptSecret(JSON.stringify({ kind: "follow", generation, scope: fingerprint({ filters, connection: context.connection.id }), cursor, from: filters.from })) } if (batch.entries.length || !batch.enabled || Date.now() >= until) return { ...batch, cursor: await encryptSecret(JSON.stringify({ kind: "follow", generation, scope: fingerprint({ filters, connection: context.connection.id }), cursor, from: filters.from })) }
await new Promise(resolve => setTimeout(resolve, 1000)) await sleep(1000, undefined, { signal: waitSignal })
} }
} finally { release() } } finally { release() }
}, },
@@ -473,7 +516,12 @@ add("changes.cancel", "mm_change_cancel", "Отменить свой незап
add("operations.read", "mm_operation_get", "Результат своей операции", "mcp", null, "read", "connection", z.object({ operationId: z.string().uuid() }).strict(), { add("operations.read", "mm_operation_get", "Результат своей операции", "mcp", null, "read", "connection", z.object({ operationId: z.string().uuid() }).strict(), {
read: async (input, context) => { read: async (input, context) => {
const { rows: [operation] } = await pool.query("SELECT * FROM mcp_operations WHERE id=$1 AND connection_id=$2", [input.operationId, context.connection.id]) const { rows: [operation] } = await pool.query("SELECT * FROM mcp_operations WHERE id=$1 AND connection_id=$2", [input.operationId, context.connection.id])
if (!operation) throw new McpError("NOT_FOUND", "Операция не найдена", 404) if (!operation) {
const tombstone = await findOperationReplayTombstone(pool, context.connection.id, String(input.operationId))
if (!tombstone) throw new McpError("NOT_FOUND", "Операция не найдена", 404)
assertOperationReplayScope(tombstone, context.connection)
operationHistoryExpired()
}
await assertPlanReadScope(await (await import("./runtime.js")).loadPlan(operation.plan_id), context) await assertPlanReadScope(await (await import("./runtime.js")).loadPlan(operation.plan_id), context)
const { rows: steps } = await pool.query("SELECT * FROM mcp_operation_steps WHERE operation_id=$1 ORDER BY step_index", [input.operationId]) const { rows: steps } = await pool.query("SELECT * FROM mcp_operation_steps WHERE operation_id=$1 ORDER BY step_index", [input.operationId])
return { ...operation, steps } return { ...operation, steps }
@@ -17,6 +17,7 @@ try{
await pool.query("INSERT INTO backup_storage_settings(id) VALUES(1) ON CONFLICT(id) DO NOTHING") await pool.query("INSERT INTO backup_storage_settings(id) VALUES(1) ON CONFLICT(id) DO NOTHING")
await updateMcpSettings({enabled:true},actor) await updateMcpSettings({enabled:true},actor)
app=await buildApp({startScheduler:false});await app.ready() app=await buildApp({startScheduler:false});await app.ready()
await (await import("../../test/mcp-users-bindings.js")).verifyMcpUserBindings(actor)
await (await import("./routing-community-context-acceptance.js")).acceptRouteCommunityContextMcp(actor) await (await import("./routing-community-context-acceptance.js")).acceptRouteCommunityContextMcp(actor)
await (await import("../../test/mcp-acme-test.js")).verifyMcpAcmeTest(actor) await (await import("../../test/mcp-acme-test.js")).verifyMcpAcmeTest(actor)
await (await import("../../test/mcp-acme-settings.js")).verifyMcpAcmeSettings(actor) await (await import("../../test/mcp-acme-settings.js")).verifyMcpAcmeSettings(actor)
@@ -0,0 +1,153 @@
import { z } from "zod"
import { isIP } from "node:net"
import { bridgeVlanConfigurationRequestSchema } from "@mmapp/contracts/bridge-vlan"
import { buildBridgeVlanConfiguration, bridgeVlanCurrentDesired, BridgeVlanConfigurationError, type BridgeVlanState } from "../../services/bridge-vlan-configuration.js"
import { readRosSnapshot, routerClient, rosSnapshotSourceProof, type RosSnapshot, type RosStep } from "./resources.js"
import { verifyBridgeManagementChannel } from "./management.js"
import { McpError } from "./security.js"
import { canonicalBridgeVlanValue } from "../../services/bridge-vlan-values.js"
import type { ActionContext, ChangeEvidence } from "./action-types.js"
import type { add } from "./actions.js"
const interfaceResources = ["ethernet", "bridge", "vlan", "gre", "wireguard", "vxlan"]
const resources = [...interfaceResources, "bridge_port", "bridge_vlan", "ipv4_address", "ipv6_address"]
export async function readBridgeVlanState(serverId: number, context: Pick<ActionContext, "signal">): Promise<{ state: BridgeVlanState; snapshots: RosSnapshot[] }> {
const snapshots: RosSnapshot[] = []
const active = async () => { if (context.signal?.aborted) throw new McpError("READ_CANCELLED", "Чтение конфигурации VLAN отменено", 409) }
// Bind every catalogue to the same configured device source. The executor
// rechecks these complete configuration snapshots before each native write.
for (const resource of resources) snapshots.push(await readRosSnapshot(serverId, resource, active, snapshots[0]?.sourceProof))
const rows = (resource: string) => snapshots.find(snapshot => snapshot.resource === resource)!.rows
const state: BridgeVlanState = {
bridges: rows("bridge"), ports: rows("bridge_port"), vlans: rows("bridge_vlan"),
interfaces: snapshots.filter(snapshot => interfaceResources.includes(snapshot.resource)).flatMap(snapshot => snapshot.rows.map(row => ({
resource: snapshot.resource, id: String(row[".id"]), name: String(row.name), disabled: row.disabled,
}))),
vlanInterfaces: rows("vlan"), ipv4Addresses: rows("ipv4_address"), ipv6Addresses: rows("ipv6_address"),
}
return { state, snapshots }
}
export async function prepareBridgeVlanConfiguration(raw: unknown, context: ActionContext): Promise<ChangeEvidence> {
const input = bridgeVlanConfigurationRequestSchema.parse(raw)
let management: Awaited<ReturnType<typeof verifyBridgeManagementChannel>> | undefined
try { management = await verifyBridgeManagementChannel(input.serverId) }
catch (error) {
// Only lack of a current alternate channel permits primary-path admission.
// Identity, transport and other verification failures stay fail-closed.
if (!(error instanceof McpError) || error.code !== "APPROVAL_REQUIRED") throw error
}
const { state, snapshots } = await readBridgeVlanState(input.serverId, context)
const primaryManagement = management === undefined
if (!management) {
const { server } = await routerClient(input.serverId)
if (!isIP(server.host)) throw new McpError("MANAGEMENT_UNSAFE", "Основной адрес управления должен быть буквальным IP, подтверждённым в конфигурации устройства", 409)
if (!snapshots[0]?.sourceProof || await rosSnapshotSourceProof(server) !== snapshots[0].sourceProof) throw new McpError("STATE_CONFLICT", "Источник основного управления изменился во время подготовки VLAN", 409)
const expiresAt = Math.min(Date.parse(context.connection.policy.expiresAt), context.connection.owner_valid_until.getTime(), Date.parse(context.principal.credentialExpiresAt))
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) throw new McpError("APPROVAL_REQUIRED", "Нет действующего допуска для основного канала управления", 409)
management = { channelId: "primary", serverId: input.serverId, host: server.host, expiresAt: new Date(expiresAt).toISOString() }
}
try {
const plan = buildBridgeVlanConfiguration({ ...input, state, management, now: Date.now() })
if (!plan.steps.length) throw new McpError("STATE_CONFLICT", "Конфигурация VLAN уже соответствует плану", 409)
return {
target: { scope: "servers", serverIds: [input.serverId] }, objects: plan.objects,
snapshots, steps: plan.steps.map(step => ({ kind: "ros" as const, step })),
requiresHuman: true, validUntil: management.expiresAt,
...(primaryManagement ? {} : { managementChannels: [{ channelId: management.channelId, serverId: management.serverId, expiresAt: management.expiresAt }] }),
warnings: ["Связанный план изменяет VLAN memberships и PVID в указанном порядке; включение filtering выполняется последним.",
"Каждая подтверждённая запись проверяется до следующей. При неизвестном результате дальнейшие записи останавливаются.",
primaryManagement ? "Используется основной адрес управления; независимость канала не подтверждена. Обязательно подтверждение человеком; каждый префикс плана сохраняет подтверждённый management-доступ."
: "Проверяется конфигурация и доступность зарегистрированного management-канала. Передача пользовательского трафика требует отдельной сетевой проверки."],
}
} catch (error) {
if (error instanceof BridgeVlanConfigurationError) throw new McpError(error.code, error.message, 409)
throw error
}
}
/** A legacy single-row tool must pass the same effective-state checks, but may
* not acquire authority to edit other resource kinds through this delegation. */
export async function prepareGenericBridgeVlanChange(step: RosStep, context: ActionContext): Promise<ChangeEvidence | undefined> {
const sensitive = step.resource === "bridge" ? ["pvid", "vlan-filtering", "frame-types", "ingress-filtering"] : step.resource === "bridge_port"
? ["bridge", "interface", "pvid", "ingress-filtering", "frame-types", "disabled"] : ["bridge", "vlan-ids", "tagged", "untagged", "disabled"]
if (!["bridge", "bridge_port", "bridge_vlan"].includes(step.resource) || step.resource === "bridge" && step.action !== "update"
|| step.action === "update" && !Object.keys(step.fields).some(key => sensitive.includes(key))) return undefined
const { state } = await readBridgeVlanState(step.serverId, context)
const rows = step.resource === "bridge" ? state.bridges : step.resource === "bridge_port" ? state.ports : state.vlans
const row = rows.find(item => item[".id"] === step.id)
const bridgeName = step.resource === "bridge" ? row?.name : step.fields.bridge ?? row?.bridge
const bridges = state.bridges.filter(item => item.name === bridgeName)
if (bridges.length !== 1 || step.resource !== "bridge" && row && row.bridge !== bridgeName) throw new McpError("STATE_CONFLICT", "Смена bridge требует отдельных явных планов удаления и добавления порта", 409)
const bridgeId = String(bridges[0][".id"])
try {
const desired = bridgeVlanCurrentDesired(state, bridgeId)
const bool = (value: unknown): boolean => {
if (value === "true" || value === true) return true
if (value === "false" || value === false) return false
throw new McpError("INVALID_INPUT", "Нужны явные параметры порта VLAN", 400)
}
const ref = (name: unknown) => {
const matches = state.interfaces.filter(iface => iface.name === name)
if (matches.length !== 1) throw new McpError("INVALID_INPUT", "Не подтверждён стабильный ID участника VLAN", 400)
return { resource: matches[0].resource, id: matches[0].id }
}
if (step.resource === "bridge") {
if (Object.keys(step.fields).some(key => !sensitive.includes(key))) throw new McpError("INVALID_INPUT", "Подготовьте свойства bridge и VLAN filtering отдельными планами", 400)
if (step.fields.pvid !== undefined) desired.pvid = Number(step.fields.pvid)
if (step.fields["vlan-filtering"] !== undefined) desired.filtering = bool(step.fields["vlan-filtering"])
if (step.fields["frame-types"] !== undefined) desired.frameTypes = bridgeVlanConfigurationRequestSchema.shape.desired.shape.frameTypes.parse(step.fields["frame-types"])
if (step.fields["ingress-filtering"] !== undefined) desired.ingressFiltering = bool(step.fields["ingress-filtering"])
} else if (step.resource === "bridge_port") {
const index = desired.ports.findIndex(port => port.id === step.id)
if (step.action === "delete") {
if (index < 0) throw new McpError("NOT_FOUND", "Статический порт не найден", 404)
desired.ports.splice(index, 1); desired.removePortIds.push(step.id!)
} else {
const effective = { ...row, ...step.fields }
const port = { ...(step.action === "update" ? { id: step.id } : {}), interface: ref(effective.interface), pvid: Number(effective.pvid),
ingressFiltering: bool(effective["ingress-filtering"]), frameTypes: effective["frame-types"], disabled: bool(effective.disabled),
...(effective.comment === undefined ? {} : { comment: effective.comment }) }
const parsed = bridgeVlanConfigurationRequestSchema.shape.desired.shape.ports.element.parse(port)
if (step.action === "create") desired.ports.push(parsed)
else { if (index < 0) throw new McpError("NOT_FOUND", "Статический порт не найден", 404); desired.ports[index] = parsed }
}
} else {
const index = desired.vlans.findIndex(vlan => vlan.id === step.id)
if (step.action === "delete") {
if (index < 0) throw new McpError("NOT_FOUND", "Статическая VLAN membership не найдена", 404)
desired.vlans.splice(index, 1); desired.removeVlanIds.push(step.id!)
} else {
const effective = { ...row, ...step.fields }
const members = (value: unknown) => value === undefined || value === "" ? [] : String(value).split(",").map(ref)
const vlan = { ...(step.action === "update" ? { id: step.id } : {}), vlanIds: effective["vlan-ids"], tagged: members(effective.tagged), untagged: members(effective.untagged),
disabled: effective.disabled === undefined && step.action === "create" ? false : bool(effective.disabled), ...(effective.comment === undefined ? {} : { comment: effective.comment }) }
const parsed = bridgeVlanConfigurationRequestSchema.shape.desired.shape.vlans.element.parse(vlan)
if (step.action === "create") desired.vlans.push(parsed)
else { if (index < 0) throw new McpError("NOT_FOUND", "Статическая VLAN membership не найдена", 404); desired.vlans[index] = parsed }
}
}
const evidence = await prepareBridgeVlanConfiguration({ serverId: step.serverId, bridgeId, desired }, context)
if (evidence.steps.length !== 1 || evidence.steps[0].kind !== "ros" || evidence.steps[0].step.resource !== step.resource || evidence.steps[0].step.action !== step.action || evidence.steps[0].step.id !== step.id) {
throw new McpError("APPROVAL_REQUIRED", "Для связанных изменений используйте явный план mm_bridge_vlan_configuration_plan с соответствующим разрешением", 409)
}
const native = evidence.steps[0].step
if (step.action === "update" && Object.entries(native.fields).some(([key, value]) => !Object.hasOwn(step.fields, key)
|| (step.resource === "bridge_vlan" ? canonicalBridgeVlanValue(key, value) !== canonicalBridgeVlanValue(key, step.fields[key]) : value !== step.fields[key]))) {
throw new McpError("STATE_CONFLICT", "Состояние VLAN изменилось во время подготовки; незапрошенные поля не будут перезаписаны", 409)
}
return evidence
} catch (error) {
if (error instanceof BridgeVlanConfigurationError) throw new McpError(error.code, error.message, 409)
throw error
}
}
export function registerBridgeVlanActions(register: typeof add) {
register("network.bridge-vlan.read", "mm_bridge_vlan_configuration_get", "Полная конфигурация bridge VLAN и стабильные ID участников", "network", "mm:network:read", "read", "servers", z.object({ serverId: z.number().int().positive() }).strict(), {
read: async (input, context) => ({ state: (await readBridgeVlanState(Number(input.serverId), context)).state }),
})
register("network.bridge-vlan.configure", "mm_bridge_vlan_configuration_plan", "Связанный план VLAN membership, PVID и bridge filtering", "network", "mm:network:write", "write", "servers", bridgeVlanConfigurationRequestSchema, {
prepare: prepareBridgeVlanConfiguration,
})
}
@@ -0,0 +1,21 @@
import assert from "node:assert/strict"
process.env.AUTH_REQUIRED = "true"
process.env.AUTH_JWT_SECRET = "bridge-vlan-owned-acceptance-secret"
process.env.AUTH_ISSUER = "https://auth.test.local"
const { pool, closePool } = await import("../../db/index.js")
const { applySqlMigrations } = await import("../../db/migrate.js")
const { updateMcpSettings } = await import("./store.js")
const { buildApp } = await import("../../index.js")
const actor = { id: "bridge-vlan-owner", email: "bridge@example.invalid", name: "Bridge VLAN owner", apps: ["mm"], permissions: ["mm:settings:admin", "mm:network:admin"], expiresAt: Date.now() + 3600000 }
let app: Awaited<ReturnType<typeof buildApp>> | undefined
try {
await applySqlMigrations(pool)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM servers")).rows[0].n, 0, "Disposable empty database required")
await updateMcpSettings({ enabled: true }, actor)
app = await buildApp({ startScheduler: false }); await app.ready()
await (await import("../../test/mcp-bridge-vlan-configuration.js")).verifyMcpBridgeVlanConfiguration(actor, app)
} finally {
await (await import("../../services/scheduler.js")).stopSchedulerForDatabaseOperation()
await (await import("../../services/traffic-flow-ingest.js")).stopTrafficFlowListenerForDatabaseOperation()
await app?.close(); await closePool()
}
@@ -0,0 +1,76 @@
import assert from "node:assert/strict"
import test from "node:test"
import { configurationRows, resourceById, rosSnapshotVersion, type ConfigurationRow, type RosSnapshot, type RosStep } from "./resources.js"
import { createRosPrefix } from "./ros-prefix.js"
import { verifyRosStepSnapshots } from "./ros-step-verification.js"
import { fingerprint, McpError } from "./security.js"
const snapshot = (resource: string, raw: ConfigurationRow[]): RosSnapshot => {
const rows = configurationRows(resourceById(resource), raw)
return { serverId: 1, resource, rows, fingerprint: fingerprint(rows), sourceProof: "a".repeat(64), projectionVersion: rosSnapshotVersion(resource) }
}
const membership = { ".id": "*1", bridge: "br", "vlan-ids": "20-22", tagged: "ether2,ether3", dynamic: "false", disabled: "false" }
test("VLAN readback accepts equivalent VID/member sets and omitted empty fields, retaining exact native identity", () => {
const before = snapshot("bridge_vlan", [membership])
const after = snapshot("bridge_vlan", [{ ...membership, "vlan-ids": "22,20,21", tagged: "ether3,ether2" }])
const step: RosStep = { serverId: 1, resource: "bridge_vlan", action: "update", id: "*1", fields: { "vlan-ids": "20-22", tagged: "ether3,ether2", untagged: "", comment: "" } }
assert.equal(verifyRosStepSnapshots(step, {}, before, after), true)
assert.equal(verifyRosStepSnapshots(step, {}, before, snapshot("bridge_vlan", [{ ...membership, "vlan-ids": "20-23" }])), false)
assert.equal(verifyRosStepSnapshots(step, {}, before, snapshot("bridge_vlan", [{ ...membership, ".id": "*2" }])), false)
assert.throws(() => snapshot("bridge_vlan", [{ ...membership, "vlan-ids": "20,20-22" }]), /Недостоверная/)
})
test("PVID-generated rows may change while the sealed static VLAN configuration remains exact", async () => {
const port = { ".id": "*2", bridge: "br", interface: "ether2", pvid: "10", disabled: "false" }
const dynamic = { ".id": "*D", bridge: "br", "vlan-ids": "10", untagged: "ether2", dynamic: "true" }
const raw = new Map<string, ConfigurationRow[]>([["bridge_port", [port]], ["bridge_vlan", [membership, dynamic]]])
const initial = [...raw].map(([resource, rows]) => snapshot(resource, rows))
const read = async (_server: number, resource: string, active: () => Promise<unknown> = async () => {}) => { await active(); return snapshot(resource, raw.get(resource)!) }
const prefix = createRosPrefix(initial, read)
await prefix.check()
raw.set("bridge_port", [{ ...port, pvid: "20" }])
raw.set("bridge_vlan", [membership, { ...dynamic, ".id": "*E", "vlan-ids": "20" }])
const step: RosStep = { serverId: 1, resource: "bridge_port", action: "update", id: "*2", fields: { pvid: "20" } }
await prefix.advance(step, {}, await read(1, "bridge_port"))
await prefix.check()
raw.set("bridge_vlan", [{ ...membership, tagged: "ether2" }, dynamic])
await assert.rejects(prefix.check(), error => error instanceof McpError && error.code === "STATE_CONFLICT")
})
test("full VLAN ranges and long sparse VID sets stay representable in plans and inverse snapshots", () => {
const resource = resourceById("bridge_vlan")
const expanded = Array.from({ length: 4094 }, (_, index) => String(index + 1)).join(",")
const sparse = Array.from({ length: 2047 }, (_, index) => String(index * 2 + 1)).join(",")
assert.ok(sparse.length > 100)
for (const value of ["1-4094", sparse]) assert.equal(resource.fields.safeParse({ "vlan-ids": value }).success, true)
for (const value of ["0", "4095", "10-9", "10,10-12"]) assert.equal(resource.fields.safeParse({ "vlan-ids": value }).success, false)
const before = snapshot("bridge_vlan", [membership])
const after = snapshot("bridge_vlan", [{ ...membership, "vlan-ids": expanded }])
assert.equal(after.rows[0]["vlan-ids"], "1-4094", "Native expansion must not overflow the public inverse DTO")
assert.equal(verifyRosStepSnapshots({ serverId: 1, resource: "bridge_vlan", action: "update", id: "*1", fields: { "vlan-ids": "1-4094" } }, {}, before, after), true)
})
test("unknown dynamic flags are retained; only explicit RouterOS-generated VLAN rows are excluded", () => {
assert.equal(snapshot("bridge_vlan", [{ ...membership, dynamic: "unknown" }]).rows.length, 1)
for (const dynamic of [true, "true", "yes"]) assert.equal(snapshot("bridge_vlan", [{ ...membership, dynamic }]).rows.length, 0)
assert.equal(snapshot("bridge_vlan", [{ ...membership, dynamic: "false" }]).rows.length, 1)
})
test("old bridge CPU, port and VLAN projection contracts cannot execute after semantic projection changes", () => {
for (const resource of ["bridge", "bridge_port", "bridge_vlan"]) {
const current = snapshot(resource, resource === "bridge_vlan" ? [membership] : [{ ".id": "*2", bridge: "br", interface: "ether2", pvid: "10" }])
assert.equal(current.projectionVersion, 4)
assert.throws(() => createRosPrefix([{ ...current, projectionVersion: 3 }]), error => error instanceof McpError && error.code === "PLAN_EXPIRED")
}
})
test("CPU frame admission and MVRP configuration are sealed, with no MVRP write permission", () => {
const bridge = { ".id": "*1", name: "br", pvid: "10", "vlan-filtering": "true", "frame-types": "admit-all", "ingress-filtering": "true", mvrp: "false", running: "true" }
const original = snapshot("bridge", [bridge])
for (const field of ["frame-types", "ingress-filtering", "mvrp"]) {
const changed = snapshot("bridge", [{ ...bridge, [field]: field === "frame-types" ? "admit-only-vlan-tagged" : "false" === bridge[field as keyof typeof bridge] ? "true" : "false" }])
assert.notEqual(changed.fingerprint, original.fingerprint)
}
assert.equal(snapshot("bridge", [{ ...bridge, running: "false" }]).fingerprint, original.fingerprint)
assert.equal(resourceById("bridge").fields.safeParse({ mvrp: true }).success, false)
})
@@ -0,0 +1,135 @@
import { bridgeVlanStateSchema, type BridgeVlanDesired, type BridgeVlanState } from "@mmapp/contracts/bridge-vlan"
import { bridgeVlanCurrentDesired } from "../../services/bridge-vlan-configuration.js"
import type { ActionContext, ChangeEvidence } from "./action-types.js"
import { prepareBridgeVlanConfiguration, readBridgeVlanState } from "./bridge-vlan-actions.js"
import { acknowledgedCreateId, assertNoCallerCreateId, recoveryRosStepId } from "./native-create-identity.js"
import { readRosSnapshot, rosSnapshotVersion, type RosSnapshot, type RosStep } from "./resources.js"
import { verifyRosStepSnapshots } from "./ros-step-verification.js"
import { decryptSecret, fingerprint, McpError } from "./security.js"
type Checkpoint = { step_index: number; before_sealed: string; after_sealed: string | null }
type Intended = { step_index: number; state: string; result: unknown }
const key = (snapshot: Pick<RosSnapshot, "serverId" | "resource">) => `${snapshot.serverId}:${snapshot.resource}`
function conflict(message: string): never { throw new McpError("STATE_CONFLICT", message, 409) }
export function needsBridgeVlanRecovery(sourceAction: string, evidence: ChangeEvidence): boolean {
if (sourceAction === "network.bridge-vlan.configure") return true
return evidence.steps.some(change => {
if (change.kind !== "ros") return false
const step = change.step
if (step.resource === "bridge") return step.action === "update" && Object.keys(step.fields).some(field => ["pvid", "vlan-filtering", "frame-types", "ingress-filtering"].includes(field))
const sensitive = step.resource === "bridge_port" ? ["bridge", "interface", "pvid", "ingress-filtering", "frame-types", "disabled"]
: step.resource === "bridge_vlan" ? ["bridge", "vlan-ids", "tagged", "untagged", "disabled"] : []
return sensitive.length > 0 && (step.action !== "update" || Object.keys(step.fields).some(field => sensitive.includes(field)))
})
}
function validSnapshot(snapshot: RosSnapshot): boolean {
return snapshot.projectionVersion === rosSnapshotVersion(snapshot.resource) && !!snapshot.sourceProof && Array.isArray(snapshot.rows) && snapshot.fingerprint === fingerprint(snapshot.rows)
}
function sameSnapshot(left: RosSnapshot, right: RosSnapshot): boolean {
return validSnapshot(left) && validSnapshot(right) && key(left) === key(right) && left.fingerprint === right.fingerprint && left.sourceProof === right.sourceProof
}
function stateFromSnapshots(serverId: number, snapshots: Map<string, RosSnapshot>): BridgeVlanState {
const rows = (resource: string) => {
const snapshot = snapshots.get(key({ serverId, resource }))
if (!snapshot || !validSnapshot(snapshot)) conflict("Нет полного исходного снимка bridge VLAN с актуальной проекцией")
return snapshot.rows
}
return bridgeVlanStateSchema.parse({ bridges: rows("bridge"), ports: rows("bridge_port"), vlans: rows("bridge_vlan"),
vlanInterfaces: rows("vlan"), ipv4Addresses: rows("ipv4_address"), ipv6Addresses: rows("ipv6_address"),
interfaces: ["ethernet", "bridge", "vlan", "gre", "wireguard", "vxlan"].flatMap(resource => rows(resource).map(row => ({ resource, id: String(row[".id"]), name: String(row.name), disabled: row.disabled }))),
})
}
/** Fresh typed inverse of a completely verified transition. Deleted port/VLAN
* rows are recreated without their old IDs; new rows are removed only by ACK. */
export async function prepareBridgeVlanRecovery(input: {
operationId: string; evidence: ChangeEvidence; checkpoints: Checkpoint[]; intended: Intended[];
}, context: ActionContext): Promise<ChangeEvidence> {
const { operationId, evidence, checkpoints, intended } = input
if (!evidence.steps.length || evidence.steps.some(change => change.kind !== "ros" || !["bridge", "bridge_port", "bridge_vlan"].includes(change.step.resource))) {
throw new McpError("UNSUPPORTED_CAPABILITY", "Обратный VLAN план не поддерживает смешанные ресурсы", 409)
}
const native = evidence.steps.map(change => (change as { kind: "ros"; step: RosStep }).step)
if (native.some(step => step.resource === "bridge" && step.action !== "update")) {
throw new McpError("UNSUPPORTED_CAPABILITY", "Создание или удаление самого bridge не поддерживает типизированный обратный VLAN план", 409)
}
const serverIds = [...new Set(native.map(step => step.serverId))]
if (serverIds.length !== 1 || evidence.target.scope !== "servers" || evidence.target.serverIds.length !== 1 || evidence.target.serverIds[0] !== serverIds[0]) conflict("Нет однозначного сервера исходного VLAN плана")
const serverId = serverIds[0]
if (!context.connection.policy.scope.serverIds.includes(serverId)) throw new McpError("SERVER_DENIED", "VLAN сервер вне текущего допуска")
if (intended.length !== native.length || checkpoints.length !== native.length || new Set(intended.map(item => item.step_index)).size !== native.length || new Set(checkpoints.map(item => item.step_index)).size !== native.length) conflict("Нет полного набора подтверждённых шагов VLAN")
const baseline = new Map<string, RosSnapshot>()
for (const snapshot of evidence.snapshots) {
if (snapshot.serverId !== serverId || !validSnapshot(snapshot) || baseline.has(key(snapshot))) conflict("Исходные VLAN снимки или их версии не подтверждены")
baseline.set(key(snapshot), snapshot)
}
const baselineState = stateFromSnapshots(serverId, baseline), expected = new Map(baseline)
const created: Array<{ resource: "bridge_port" | "bridge_vlan"; id: string }> = []
const deleted: Array<{ resource: "bridge_port" | "bridge_vlan"; id: string }> = []
for (let index = 0; index < native.length; index++) {
const step = native[index], checkpoint = checkpoints.find(item => item.step_index === index), receipt = intended.find(item => item.step_index === index)
assertNoCallerCreateId(step)
if (!checkpoint?.before_sealed || !checkpoint.after_sealed || receipt?.state !== "verified") conflict("Нет полного проверенного checkpoint VLAN; требуется ручная сверка")
const before = JSON.parse(await decryptSecret(checkpoint.before_sealed)) as RosSnapshot
const after = JSON.parse(await decryptSecret(checkpoint.after_sealed)) as RosSnapshot
const previous = expected.get(key({ serverId, resource: step.resource }))
if (!previous || !sameSnapshot(previous, before) || !validSnapshot(after) || before.sourceProof !== after.sourceProof || !verifyRosStepSnapshots(step, receipt.result, before, after)) conflict("Исходный переход VLAN или источник не подтверждён")
if (step.action === "create") {
const ack = acknowledgedCreateId(receipt.result), actual = recoveryRosStepId(step, before.rows, after.rows)
if (!ack || actual !== ack || !["bridge_port", "bridge_vlan"].includes(step.resource)) conflict("Нет точного ACK и checkpoint ID созданного VLAN объекта")
created.push({ resource: step.resource as "bridge_port" | "bridge_vlan", id: ack })
} else if (step.action === "delete") {
if (!step.id || !["bridge_port", "bridge_vlan"].includes(step.resource)) conflict("Не подтверждён ID удалённого VLAN объекта")
deleted.push({ resource: step.resource as "bridge_port" | "bridge_vlan", id: step.id })
}
expected.set(key(after), after)
}
const bridgeNames = new Set(native.map(step => {
const rows = step.resource === "bridge" ? baselineState.bridges : step.resource === "bridge_port" ? baselineState.ports : baselineState.vlans
const row = rows.find(item => item[".id"] === step.id)
const name = step.resource === "bridge" ? row?.name : step.fields.bridge ?? row?.bridge
if (typeof name !== "string" || !name) conflict("Не подтверждён исходный bridge VLAN")
return name
}))
const bridges = baselineState.bridges.filter(row => bridgeNames.has(String(row.name)))
if (bridgeNames.size !== 1 || bridges.length !== 1) conflict("Обратный VLAN план требует один однозначный bridge")
const bridgeId = String(bridges[0][".id"]), desired: BridgeVlanDesired = bridgeVlanCurrentDesired(baselineState, bridgeId)
for (const item of deleted) {
const terminal = expected.get(key({ serverId, resource: item.resource }))
const original = item.resource === "bridge_port" ? desired.ports.find(row => row.id === item.id) : desired.vlans.find(row => row.id === item.id)
if (!original || !terminal || terminal.rows.some(row => row[".id"] === item.id)) conflict("Нет подтверждённого удаления исходной VLAN identity")
delete original.id
}
for (const item of created) {
const originalRows = baseline.get(key({ serverId, resource: item.resource }))!.rows
const terminalRows = expected.get(key({ serverId, resource: item.resource }))!.rows
if (originalRows.some(row => row[".id"] === item.id) || !terminalRows.some(row => row[".id"] === item.id)) conflict("ACK ID VLAN не является подтверждённым новым объектом")
if (item.resource === "bridge_port") desired.removePortIds.push(item.id)
else desired.removeVlanIds.push(item.id)
}
const compare = async (snapshots: RosSnapshot[]): Promise<RosSnapshot[]> => {
const captured = new Map(snapshots.map(snapshot => [key(snapshot), snapshot]))
for (const snapshot of snapshots) {
const terminal = expected.get(key(snapshot))
if (!terminal || !sameSnapshot(terminal, snapshot)) conflict("После операции изменились VLAN конфигурация, зависимости или источник; обратный план запрещён")
}
for (const [resourceKey, terminal] of expected) {
const current = captured.get(resourceKey) ?? await readRosSnapshot(serverId, terminal.resource, undefined, terminal.sourceProof)
if (!sameSnapshot(terminal, current)) conflict("После операции обнаружен drift зависимости VLAN; обратный план запрещён")
captured.set(resourceKey, current)
}
return [...captured.values()]
}
await compare((await readBridgeVlanState(serverId, context)).snapshots)
// Reconstruct through the same model, with a freshly verified management
// channel. Old receipts never confer authority to run the inverse.
const inverse = await prepareBridgeVlanConfiguration({ serverId, bridgeId, desired }, context)
const snapshots = await compare(inverse.snapshots)
const objects = new Map([...evidence.objects, ...inverse.objects].map(object => [`${object.serverId}/${object.kind}/${object.id}`, object]))
return { ...inverse, snapshots, objects: [...objects.values()], requiresHuman: true,
warnings: [`Типизированный обратный VLAN план операции ${operationId}; все исходные шаги и ACK проверены.`, ...inverse.warnings,
"Удалённые port/VLAN rows создаются с новыми ID RouterOS. Unknown операции не повторяются; восстановление самого bridge не поддерживается."],
}
}
@@ -1,6 +1,9 @@
import assert from "node:assert/strict" import assert from "node:assert/strict"
import { createHash, randomUUID } from "node:crypto" import { createHash, randomUUID } from "node:crypto"
import { createDiagnosticCancellationSourceForTests, createReadServerProbeExecutionSourceForTests } from "./diagnostic-cancellation-source.js" import { mkdtemp, readdir, rm, rmdir } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { createDiagnosticCancellationSourceForTests, createReadServerProbeExecutionSourceForTests, readServerProbeExecutionSource } from "./diagnostic-cancellation-source.js"
import { fingerprint, redact } from "./security.js" import { fingerprint, redact } from "./security.js"
const proof = async (value: unknown) => createHash("sha256").update(fingerprint(value)).digest("hex") const proof = async (value: unknown) => createHash("sha256").update(fingerprint(value)).digest("hex")
@@ -25,6 +28,7 @@ function fixture() {
return { rows } return { rows }
} } } }
return { load: () => load(client, operationId, owner), read: () => read(snapshot, operationId, owner), readSql, return { load: () => load(client, operationId, owner), read: () => read(snapshot, operationId, owner), readSql,
readWithExistingKey: () => readServerProbeExecutionSource(snapshot, operationId, owner),
refresh, binding, input, evidence, row, sealed, setRows: (value: typeof rows) => { rows=value }, calls: () => calls, operationId, credentialId } refresh, binding, input, evidence, row, sealed, setRows: (value: typeof rows) => { rows=value }, calls: () => calls, operationId, credentialId }
} }
{ {
@@ -100,4 +104,22 @@ for (const mutate of mutations) {
assert.equal(source.originalContext.tool, "bandwidth"); assert.equal(source.stageBudget, 1) assert.equal(source.originalContext.tool, "bandwidth"); assert.equal(source.stageBudget, 1)
assert.equal(source.originalContext.identity.credentialId, f.credentialId) assert.equal(source.originalContext.identity.credentialId, f.credentialId)
} }
console.log(`diagnostic-cancellation-source: locked/read-only source, expired original contexts and ${mutations.length} shared tamper checks passed`) {
const state = await mkdtemp(join(tmpdir(), "mm-legacy-source-test-"))
const previousState = process.env.MCP_STATE_DIR
process.env.MCP_STATE_DIR = state
try {
const f = fixture()
await f.refresh()
f.row.evidence = { sealed: Buffer.alloc(32).toString("base64") }
await assert.rejects(f.readWithExistingKey, { code: "STATE_CONFLICT" })
assert.equal(f.readSql.length, 1)
assert.deepEqual(await readdir(state), [], "Historical source reads must leave missing encryption state untouched")
} finally {
if (previousState === undefined) delete process.env.MCP_STATE_DIR
else process.env.MCP_STATE_DIR = previousState
await rm(join(state, "encryption-key"), { force: true })
await rmdir(state)
}
}
console.log(`diagnostic-cancellation-source: locked/read-only source, missing key, expired original contexts and ${mutations.length} shared tamper checks passed`)
@@ -3,7 +3,7 @@ import type { ProbeStorageScope, ProbeStorageSql } from "../../services/server-p
import type { ServerProbeJournalContext } from "../../services/server-probe-journal.js" import type { ServerProbeJournalContext } from "../../services/server-probe-journal.js"
import { SERVER_PROBE_OPERATION } from "../../services/server-probe-authority.js" import { SERVER_PROBE_OPERATION } from "../../services/server-probe-authority.js"
import { openServerProbeMcpBinding } from "./server-probe-actions.js" import { openServerProbeMcpBinding } from "./server-probe-actions.js"
import { decryptSecret, fingerprint, privateSecretFingerprint, redact, McpError } from "./security.js" import { decryptSecret, decryptSecretExisting, fingerprint, privateFingerprinterExisting, privateSecretFingerprint, redact, McpError } from "./security.js"
const MAX_BYTES = 2097152 const MAX_BYTES = 2097152
const uuid = z.string().uuid() const uuid = z.string().uuid()
@@ -113,5 +113,14 @@ export const loadDiagnosticCancellationSource = sourceLoader(productionCrypto)
/** Test-only crypto seam. Never accept these dependencies from tool JSON. */ /** Test-only crypto seam. Never accept these dependencies from tool JSON. */
export const createDiagnosticCancellationSourceForTests = sourceLoader export const createDiagnosticCancellationSourceForTests = sourceLoader
/** Historical authentication only; confers no execution/cancellation grant. */ /** Historical authentication only; confers no execution/cancellation grant. */
export const readServerProbeExecutionSource = readSourceLoader(productionCrypto) export const readServerProbeExecutionSource = readSourceLoader({
open: decryptSecretExisting,
proof: async value => (await privateFingerprinterExisting("server-probe-mcp-binding-v1"))(fingerprint(value)),
})
export const createReadServerProbeExecutionSourceForTests = readSourceLoader export const createReadServerProbeExecutionSourceForTests = readSourceLoader
/** Exact previously locked source projection for private history conversion. */
export function authenticateServerProbeExecutionSourceRow(row: Record<string, unknown>, operationId: string,
owner: Readonly<DiagnosticCancellationOwner>) {
checkLookup(operationId, owner)
return validateSourceRow(row, operationId, owner, productionCrypto)
}
@@ -0,0 +1,73 @@
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { test } from "node:test"
import type { McpAction, ActionContext } from "./action-types.js"
import { registerGlobalReconciliationActionsForTests } from "./global-reconciliation-actions.js"
type Register = Parameters<typeof registerGlobalReconciliationActionsForTests>[0]
type Dependencies = Parameters<typeof registerGlobalReconciliationActionsForTests>[1]
function fixture() {
const connectionId = randomUUID(), operationId = randomUUID(), ownerId = "current-retired-owner"
const context = { principal: { connectionId }, connection: { id: connectionId, owner_id: ownerId, owner_permissions: ["mm:servers:write"],
policy: { permissions: ["mm:servers:write"], scope: { allowGlobal: true, operationIds: ["servers.poll", "operations.global.reconcile"], serverIds: [1, 2] } } } } as ActionContext
let action!: Pick<McpAction, "target" | "read">
let history: { affectedServerIds: number[] } | undefined = { affectedServerIds: [1, 2] }
let revoke = false, retiredCalls = 0, live = false
const deps = {
connection: async () => {
if (revoke) throw Error("revoked-current-grant")
return structuredClone(context.connection)
},
query: async (sql: string) => {
if (sql.startsWith("SELECT id FROM servers")) return [{ id: 1 }]
if (live) return [{ id: operationId, state: "applied", operation_id: "servers.poll", evidence: { sealed: "live" } }]
return []
},
operation: (id: string) => ({ id, scope: "global", requiredPermissions: ["mm:servers:write"] }),
service: () => ({ reconcile: async () => ({ matchesDesiredState: true, matchesOriginalState: false }) }),
decrypt: async () => JSON.stringify({ target: { scope: "global" }, objects: [{ serverId: 1 }], steps: [{ kind: "service", service: "servers.poll", input: {} }] }),
retired: async (identity, authorize) => {
retiredCalls++
assert.deepEqual(identity, { operationId, connectionId, ownerId })
await authorize()
return history
},
} satisfies Omit<Dependencies, "operation" | "service"> & Partial<Record<"operation" | "service", unknown>>
registerGlobalReconciliationActionsForTests(((...args: Parameters<Register>) => { action = args[8] }) as Register, deps as unknown as Dependencies)
return { context, operationId, history: (value: typeof history) => { history = value }, live: () => { live = true },
revoke: () => { revoke = true }, calls: () => retiredCalls, read: () => action.read!({ operationId }, context),
target: () => action.target({ operationId }, context) }
}
test("global action reconciles retired poll only under current ownership and returns no archived material", async () => {
const f = fixture()
assert.deepEqual(await f.target(), { scope: "global" })
const result = await f.read() as { observedAt: string }
const { observedAt, ...rest } = result
assert(Number.isFinite(Date.parse(observedAt)))
assert.deepEqual(rest, { operationId: f.operationId, state: "applied", automaticRetry: false,
results: [{ stepIndex: 0, service: "servers.poll", matchesDesiredState: true, matchesOriginalState: false }] })
assert.equal(f.calls(), 2)
})
test("global action refuses missing or inaccessible retired history", async () => {
const f = fixture()
f.history(undefined)
await assert.rejects(f.read(), { code: "NOT_FOUND" })
f.history({ affectedServerIds: [1, 2] })
f.context.connection.policy.scope.serverIds = [1]
await assert.rejects(f.read(), { code: "SERVER_DENIED" })
f.context.connection.policy.scope.serverIds = [1, 2]
f.context.connection.policy.scope.operationIds = ["operations.global.reconcile"]
await assert.rejects(f.read(), { code: "PERMISSION_DENIED" })
f.revoke()
await assert.rejects(f.read(), /revoked-current-grant/)
})
test("global action uses the live reconciliation path while original source exists", async () => {
const f = fixture()
f.live()
await f.read()
assert.equal(f.calls(), 0)
})
@@ -1,5 +1,6 @@
import { z } from "zod" import { z } from "zod"
import { pool } from "../../db/index.js" import { pool } from "../../db/index.js"
import { readRetiredServerPollHistory } from "../../services/server-poll-history-reader.js"
import { hasPermission } from "../../lib/permissions.js" import { hasPermission } from "../../lib/permissions.js"
import { effectiveMcpConnection } from "./store.js" import { effectiveMcpConnection } from "./store.js"
import { decryptSecret, fingerprint, McpError } from "./security.js" import { decryptSecret, fingerprint, McpError } from "./security.js"
@@ -7,40 +8,84 @@ import { findMcpOperation } from "./registry.js"
import { findMcpService } from "./service-registry.js" import { findMcpService } from "./service-registry.js"
import type { add } from "./actions.js" import type { add } from "./actions.js"
import type { ActionContext, ChangeEvidence } from "./action-types.js" import type { ActionContext, ChangeEvidence } from "./action-types.js"
import type { ConnectionRow } from "./store.js"
async function authorizedSource(operationId: string, context: ActionContext) { const production = {
connection: effectiveMcpConnection,
query: async (sql: string, values: unknown[] = []) => (await pool.query(sql, values)).rows,
operation: findMcpOperation,
service: findMcpService,
decrypt: decryptSecret,
retired: readRetiredServerPollHistory,
}
type Dependencies = typeof production
const notFound = () => new McpError("NOT_FOUND", "Операция не найдена", 404)
async function currentAccess(context: ActionContext, deps: Dependencies) {
context.signal?.throwIfAborted() context.signal?.throwIfAborted()
const current = await effectiveMcpConnection(context.principal) const current = await deps.connection(context.principal)
if (!current.policy.scope.allowGlobal || fingerprint(current.policy) !== fingerprint(context.connection.policy) || fingerprint(current.owner_permissions) !== fingerprint(context.connection.owner_permissions)) throw new McpError("PERMISSION_DENIED", "Глобальный допуск сверки изменился", 403) if (!current.policy.scope.allowGlobal || fingerprint(current.policy) !== fingerprint(context.connection.policy) || fingerprint(current.owner_permissions) !== fingerprint(context.connection.owner_permissions)) throw new McpError("PERMISSION_DENIED", "Глобальный допуск сверки изменился", 403)
const ids = (await pool.query("SELECT id FROM servers ORDER BY id LIMIT 1001")).rows.map(row => Number(row.id)) const ids = (await deps.query("SELECT id FROM servers ORDER BY id LIMIT 1001")).map(row => Number(row.id))
if (ids.length > 1000) throw new McpError("LIMIT_EXCEEDED", "Каталог глобальной сверки превышает лимит", 409) if (ids.length > 1000) throw new McpError("LIMIT_EXCEEDED", "Каталог глобальной сверки превышает лимит", 409)
if (ids.some(id => !current.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Глобальная сверка требует доступа ко всему каталогу серверов", 403) if (ids.some(id => !current.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Глобальная сверка требует доступа ко всему каталогу серверов", 403)
const row = (await pool.query("SELECT o.id,o.state,p.operation_id,p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id WHERE o.id=$1 AND o.connection_id=$2", [operationId, current.id])).rows[0] return current
if (!row) throw new McpError("NOT_FOUND", "Операция не найдена", 404) }
const original = findMcpOperation(row.operation_id)
function requireOriginal(actionId: string, current: ConnectionRow, deps: Dependencies) {
const original = deps.operation(actionId)
if (!original || original.scope !== "global" || !current.policy.scope.operationIds.includes(original.id) || original.requiredPermissions.some(permission => !hasPermission(current.policy.permissions, permission) || !hasPermission(current.owner_permissions, permission))) throw new McpError("PERMISSION_DENIED", "Утрачено право на исходную глобальную операцию", 403) if (!original || original.scope !== "global" || !current.policy.scope.operationIds.includes(original.id) || original.requiredPermissions.some(permission => !hasPermission(current.policy.permissions, permission) || !hasPermission(current.owner_permissions, permission))) throw new McpError("PERMISSION_DENIED", "Утрачено право на исходную глобальную операцию", 403)
const evidence = JSON.parse(await decryptSecret(row.evidence.sealed)) as ChangeEvidence }
if (evidence.target.scope !== "global" || !evidence.steps.length || evidence.steps.some(step => step.kind !== "service" || !findMcpService(step.service)?.reconcile)) throw new McpError("UNSUPPORTED_CAPABILITY", "Для глобальной операции нет безопасной read-only сверки", 409)
const declared = evidence.objects.flatMap(object => object.serverId === undefined ? [] : [object.serverId]) function requireServerScope(declared: number[], current: ConnectionRow) {
if (declared.some(id => !current.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Зависимость операции вне текущего глобального допуска", 403) if (declared.some(id => !current.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Зависимость операции вне текущего глобального допуска", 403)
}
async function authorizedSource(operationId: string, context: ActionContext, deps: Dependencies) {
const current = await currentAccess(context, deps)
const row = (await deps.query("SELECT o.id,o.state,p.operation_id,p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id WHERE o.id=$1 AND o.connection_id=$2", [operationId, current.id]))[0]
if (!row) {
const authorize = async () => {
const fresh = await currentAccess(context, deps)
if (fresh.id !== current.id || fresh.owner_id !== current.owner_id) throw notFound()
}
const archived = await deps.retired({ operationId, connectionId: current.id, ownerId: current.owner_id }, authorize,
{ deadlineAt: Date.now() + 10000, signal: context.signal })
context.signal?.throwIfAborted()
if (!archived) throw notFound()
requireOriginal("servers.poll", current, deps)
requireServerScope(archived.affectedServerIds, current)
return { kind: "retired-poll" as const }
}
requireOriginal(row.operation_id, current, deps)
const evidence = JSON.parse(await deps.decrypt(row.evidence.sealed)) as ChangeEvidence
if (evidence.target.scope !== "global" || !evidence.steps.length || evidence.steps.some(step => step.kind !== "service" || !deps.service(step.service)?.reconcile)) throw new McpError("UNSUPPORTED_CAPABILITY", "Для глобальной операции нет безопасной read-only сверки", 409)
requireServerScope(evidence.objects.flatMap(object => object.serverId === undefined ? [] : [object.serverId]), current)
if (["pending", "running"].includes(row.state)) throw new McpError("STATE_CONFLICT", "Операция ещё выполняется", 409) if (["pending", "running"].includes(row.state)) throw new McpError("STATE_CONFLICT", "Операция ещё выполняется", 409)
context.signal?.throwIfAborted() context.signal?.throwIfAborted()
return { row, evidence } return { kind: "live" as const, row, evidence }
} }
export function registerGlobalReconciliationActions(register: typeof add) {
function registerActions(register: typeof add, deps: Dependencies) {
register("operations.global.reconcile", "mm_global_operation_reconcile", "Сверить глобальную сервисную операцию без повторной записи", "mcp", null, "read", "global", z.object({ operationId: z.string().uuid() }).strict(), { register("operations.global.reconcile", "mm_global_operation_reconcile", "Сверить глобальную сервисную операцию без повторной записи", "mcp", null, "read", "global", z.object({ operationId: z.string().uuid() }).strict(), {
target: async (input, context) => { await authorizedSource(String(input.operationId), context); return { scope: "global" } }, target: async (input, context) => { await authorizedSource(String(input.operationId), context, deps); return { scope: "global" } },
read: async (input, context) => { read: async (input, context) => {
const { row, evidence } = await authorizedSource(String(input.operationId), context) const operationId = String(input.operationId)
const authorize = async () => { await authorizedSource(String(input.operationId), context) } const source = await authorizedSource(operationId, context, deps)
if (source.kind === "retired-poll") return { operationId, state: "applied", observedAt: new Date().toISOString(),
results: [{ stepIndex: 0, service: "servers.poll", matchesDesiredState: true, matchesOriginalState: false }], automaticRetry: false }
const { row, evidence } = source
const authorize = async () => { if ((await authorizedSource(operationId, context, deps)).kind !== "live") throw notFound() }
const results = [] const results = []
for (const [index, step] of evidence.steps.entries()) { for (const [index, step] of evidence.steps.entries()) {
if (step.kind !== "service") throw new McpError("UNSUPPORTED_CAPABILITY", "Нативная глобальная сверка недоступна", 409) if (step.kind !== "service") throw new McpError("UNSUPPORTED_CAPABILITY", "Нативная глобальная сверка недоступна", 409)
await authorize() await authorize()
results.push({ stepIndex: index, service: step.service, ...await findMcpService(step.service)!.reconcile!(step.input, authorize) }) results.push({ stepIndex: index, service: step.service, ...await deps.service(step.service)!.reconcile!(step.input, authorize) })
} }
await authorize() await authorize()
return { operationId: row.id, state: row.state, observedAt: new Date().toISOString(), results, automaticRetry: false } return { operationId: row.id, state: row.state, observedAt: new Date().toISOString(), results, automaticRetry: false }
}, },
}) })
} }
export const registerGlobalReconciliationActions = (register: typeof add) => registerActions(register, production)
export const registerGlobalReconciliationActionsForTests = registerActions
+72 -1
View File
@@ -2889,6 +2889,53 @@ try {
assert.ok(!result.isError) assert.ok(!result.isError)
console.log("✓ Official SDK HTTP initialize, filtered tools/list and tools/call") console.log("✓ Official SDK HTTP initialize, filtered tools/list and tools/call")
} finally { await client.close() } } finally { await client.close() }
{
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({
...input, name: "Logs follow maintenance", permissions: ["mm:servers:read"],
scope: { serverIds: [1], operationIds: ["logs.follow"], allowGlobal: false },
}), actor)
const follower = new Client({ name: "logs-follow-maintenance", version: "1.0.0" })
const { sharedDatabaseGate } = await import("../../services/shared-database-gate.js")
const original = pool.query
let arrived!: () => void, release!: () => void
const reading = new Promise<void>(resolve => { arrived = resolve })
const held = new Promise<void>(resolve => { release = resolve })
try {
await follower.connect(new StreamableHTTPClientTransport(new URL(endpoint), { requestInit: { headers: { Authorization: `Bearer ${access.token}` } } }))
pool.query = (async (...args: Parameters<typeof original>) => {
const text = typeof args[0] === "string" ? args[0] : args[0]?.text
const result = await Reflect.apply(original, pool, args)
if (typeof text === "string" && text.startsWith("SELECT GREATEST(COALESCE(max(id),0)")) {
arrived()
await held
}
return result
}) as typeof original
const following = follower.callTool({ name: "mm_logs_follow", arguments: { serverId: 1, waitSeconds: 30 } })
try {
const observation = new AbortController()
try {
await Promise.race([reading, following.then(() => { throw new Error("Follow finished before its stream query") }),
delay(5000, undefined, { signal: observation.signal }).then(() => { throw new Error("Follow stream query was not observed") })])
} finally { observation.abort() }
let exclusiveEntered = false
const maintenance = sharedDatabaseGate.exclusive(async () => { exclusiveEntered = true }, 5000)
try {
await delay(25)
assert.equal(exclusiveEntered, false, "maintenance must wait for the pending database read")
} finally {
release()
await maintenance
}
const result = await following
assert.equal(result.isError, true)
assert.match(JSON.stringify(result), /RESET_REQUIRED/)
assert.doesNotMatch(JSON.stringify(result), /"cursor"/)
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_read_leases WHERE connection_id=$1", [access.connection.id])).rows[0].count, 0)
} 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 stdio = new Client({ name: "stdio-integration", version: "1.0.0" }) const stdio = new Client({ name: "stdio-integration", version: "1.0.0" })
try { 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" })) 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" }))
@@ -2907,17 +2954,41 @@ try {
await (await import("../../test/mcp-host-files.js")).verifyMcpHostFiles(actor,app) await (await import("../../test/mcp-host-files.js")).verifyMcpHostFiles(actor,app)
const readinessAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Actual readiness handshake", scope: { ...input.scope, operationIds: ["servers.list"] } }), actor) const readinessAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Actual readiness handshake", scope: { ...input.scope, operationIds: ["servers.list"] } }), actor)
const readinessHeaders = { authorization: "Bearer " + jwt } const readinessHeaders = { authorization: "Bearer " + jwt }
const simulatorHeaders = { authorization: "Bearer " + await new SignJWT({ sub: "policy-preview-admin", apps: ["mm"], permissions: ["mm:mcp:admin"] })
.setProtectedHeader({ alg: "HS256" }).setIssuer(env.authIssuer).setExpirationTime("1h").sign(new TextEncoder().encode(env.jwtSecret)) }
const policyPreview = (payload: Record<string, unknown>) => app!.inject({ method: "POST", url: "/api/mcp/policy/preview", headers: simulatorHeaders, payload })
const simulatedServerRead = { connectionId: readinessAccess.connection.id, operationId: "servers.list", target: { scope: "servers", serverIds: [1] } }
const simulation = await policyPreview(simulatedServerRead)
assert.equal(simulation.statusCode, 200, simulation.body)
assert.equal(simulation.json().result.decision, "allowed", "an administrator simulates the stored owner's rights, not the administrator's rights")
assert.equal(simulation.json().executionAvailable, false)
assert.equal((await policyPreview({ ...simulatedServerRead, ownerPermissions: ["mm:settings:admin"] })).statusCode, 400)
const ready = await app.inject({ method: "POST", url: `/api/mcp/connections/${readinessAccess.connection.id}/check`, headers: readinessHeaders, payload: {} }) const ready = await app.inject({ method: "POST", url: `/api/mcp/connections/${readinessAccess.connection.id}/check`, headers: readinessHeaders, payload: {} })
assert.equal(ready.statusCode, 200, ready.body) assert.equal(ready.statusCode, 200, ready.body)
assert.equal(ready.json().ok, true, ready.body); assert.equal(ready.json().checks.protocolHandshake, true) assert.equal(ready.json().ok, true, ready.body); assert.equal(ready.json().checks.protocolHandshake, true)
assert.equal(ready.json().protocolProbe.status, "passed"); assert.equal(ready.json().protocolProbe.clientCredentialsVerified, false) assert.equal(ready.json().protocolProbe.status, "passed"); assert.equal(ready.json().protocolProbe.clientCredentialsVerified, false)
assert.equal(ready.json().protocolProbe.capabilitiesRead, "skipped")
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_credentials WHERE connection_id=$1 AND client_id='mcp-readiness-probe'", [readinessAccess.connection.id])).rows[0].n, 0) assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_credentials WHERE connection_id=$1 AND client_id='mcp-readiness-probe'", [readinessAccess.connection.id])).rows[0].n, 0)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_grants WHERE connection_id=$1", [readinessAccess.connection.id])).rows[0].n, 0) assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_grants WHERE connection_id=$1", [readinessAccess.connection.id])).rows[0].n, 0)
const duplicateReady = await app.inject({ method: "POST", url: `/api/mcp/connections/${readinessAccess.connection.id}/check`, headers: readinessHeaders, payload: {} }) const duplicateReady = await app.inject({ method: "POST", url: `/api/mcp/connections/${readinessAccess.connection.id}/check`, headers: readinessHeaders, payload: {} })
assert.equal(duplicateReady.statusCode, 429) assert.equal(duplicateReady.statusCode, 429)
await setConnectionStatus(readinessAccess.connection.id, "paused", actor) await setConnectionStatus(readinessAccess.connection.id, "paused", actor)
const pausedSimulation = await policyPreview(simulatedServerRead)
assert.equal(pausedSimulation.statusCode, 200, pausedSimulation.body)
assert.equal(pausedSimulation.json().result.decision, "denied")
assert.ok(pausedSimulation.json().result.reasons.some((reason: { code: string }) => reason.code === "CONNECTION_INACTIVE"))
const pausedReady = await app.inject({ method: "POST", url: `/api/mcp/connections/${readinessAccess.connection.id}/check`, headers: readinessHeaders, payload: {} }) const pausedReady = await app.inject({ method: "POST", url: `/api/mcp/connections/${readinessAccess.connection.id}/check`, headers: readinessHeaders, payload: {} })
assert.equal(pausedReady.statusCode, 200); assert.equal(pausedReady.json().protocolProbe.status, "skipped") assert.equal(pausedReady.statusCode, 200); assert.equal(pausedReady.json().protocolProbe.status, "skipped")
const readableAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Actual readiness capability read", scope: { ...input.scope, operationIds: ["servers.list", "capabilities.read"] } }), actor)
const readable = await app.inject({ method: "POST", url: `/api/mcp/connections/${readableAccess.connection.id}/check`, headers: readinessHeaders, payload: {} })
assert.equal(readable.statusCode, 200, readable.body)
assert.equal(readable.json().ok, true, readable.body)
assert.equal(readable.json().protocolProbe.capabilitiesRead, "passed")
assert.equal(readable.json().protocolProbe.credentialType, "temporary-capabilities-read")
const readinessReads = (await pool.query("SELECT tool FROM mcp_requests WHERE connection_id=$1 AND tool NOT LIKE 'protocol:%'", [readableAccess.connection.id])).rows
assert.deepEqual(readinessReads.map(row => row.tool), ["mm_capabilities_get"])
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_credentials WHERE connection_id=$1 AND client_id='mcp-readiness-probe'", [readableAccess.connection.id])).rows[0].n, 0)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_grants WHERE connection_id=$1", [readableAccess.connection.id])).rows[0].n, 0)
const emptyGrantAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Readiness has no operation authority" }), actor) const emptyGrantAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Readiness has no operation authority" }), actor)
const { withMcpProbeCredential } = await import("./readiness.js") const { withMcpProbeCredential } = await import("./readiness.js")
let endedProbeToken = "" let endedProbeToken = ""
@@ -2933,7 +3004,7 @@ try {
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_grants WHERE connection_id=$1", [emptyGrantAccess.connection.id])).rows[0].n, 0) assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_grants WHERE connection_id=$1", [emptyGrantAccess.connection.id])).rows[0].n, 0)
const readinessAudit = (await pool.query("SELECT payload FROM mcp_audit WHERE connection_id=$1", [emptyGrantAccess.connection.id])).rows const readinessAudit = (await pool.query("SELECT payload FROM mcp_audit WHERE connection_id=$1", [emptyGrantAccess.connection.id])).rows
assert.ok(!JSON.stringify(readinessAudit).includes(endedProbeToken)) assert.ok(!JSON.stringify(readinessAudit).includes(endedProbeToken))
console.log("✓ Actual readiness initialize/tools-list over public HTTP: temporary empty grant, no tool authority, cleanup on success/error, cooldown and paused precheck skip") console.log("✓ Actual readiness initialize/tools-list and optional capabilities read over public HTTP: narrow grant, cleanup on success/error, cooldown and paused precheck skip")
const deviceConnection = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Web interfaces", scope: { ...input.scope, operationIds: ["ros.bridge.list", "ros.bridge.create"] } }), actor) const deviceConnection = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Web interfaces", scope: { ...input.scope, operationIds: ["ros.bridge.list", "ros.bridge.create"] } }), actor)
const deviceHeaders = { authorization: "Bearer " + jwt } const deviceHeaders = { authorization: "Bearer " + jwt }
const deviceCatalog = await app.inject({ url: "/api/mcp/devices/catalog?connectionId=" + deviceConnection.connection.id, headers: deviceHeaders }) const deviceCatalog = await app.inject({ url: "/api/mcp/devices/catalog?connectionId=" + deviceConnection.connection.id, headers: deviceHeaders })
@@ -0,0 +1,6 @@
import { McpError } from "./security.js"
export function logsFollowCancellation(maintenance?: AbortSignal, caller?: AbortSignal): McpError | undefined {
if (maintenance?.aborted) return maintenance.reason
if (caller?.aborted) return new McpError("READ_CANCELLED", "Ожидание новых событий отменено", 409)
}
@@ -0,0 +1,143 @@
import assert from "node:assert/strict"
import { 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 { mcpConnectionCreateSchema } from "@mmapp/contracts/mcp"
import { pool } from "../../db/index.js"
import type { AuthUser } from "../../lib/permissions.js"
import { createMcpConnection } from "./store.js"
export async function verifyLogsFollowSdkCancellation(app: FastifyInstance, endpoint: string, actor: AuthUser) {
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({
name: "Logs follow SDK cancellation", mode: "approval", permissions: ["mm:servers:read"],
expiresAt: new Date(Math.min(actor.expiresAt ?? Infinity, Date.now() + 600_000)).toISOString(),
scope: { serverIds: [1], operationIds: ["logs.follow", "logs.query"], allowGlobal: false },
}), actor)
const client = new Client({ name: "logs-follow-sdk-cancellation", version: "1.0.0" }, {
versionNegotiation: { mode: { pin: "2026-07-28" } },
})
const caller = new AbortController(), bound = new AbortController()
const deadline = delay(5000, undefined, { signal: bound.signal }).then(() => { throw new Error("SDK cancellation fixture deadline") })
void deadline.catch(() => undefined)
let entered!: () => void, release!: () => void, closed!: () => void
const enteredRead = new Promise<void>(resolve => { entered = resolve })
const holdRead = new Promise<void>(resolve => { release = resolve })
const responseClosed = new Promise<void>(resolve => { closed = resolve })
let readCount = 0
const originalQuery = pool.query
const observeClose = (request: IncomingMessage, response: ServerResponse) => {
if (request.method === "POST" && request.url === "/mcp") {
response.once("close", () => { if (!response.writableFinished) closed() })
}
}
try {
await client.connect(new StreamableHTTPClientTransport(new URL(endpoint), {
requestInit: { headers: { Authorization: `Bearer ${access.token}` } },
}))
app.server.on("request", observeClose)
pool.query = (async (...args: unknown[]) => {
const query = args[0]
const sql = typeof query === "object" && query !== null && "text" in query ? query.text : query
const result = await Reflect.apply(originalQuery, pool, args)
if (typeof sql === "string" && sql.startsWith("SELECT GREATEST(COALESCE(max(id),0)")) {
readCount++
entered()
await holdRead
}
return result
}) as typeof originalQuery
const follow = client.callTool({ name: "mm_logs_follow", arguments: { serverId: 1, waitSeconds: 30 } }, { signal: caller.signal })
const refused = assert.rejects(follow)
try {
await Promise.race([enteredRead, deadline])
caller.abort()
await Promise.race([Promise.all([refused, responseClosed]), deadline])
const live = await pool.query("SELECT count(*)::int AS count FROM mcp_read_leases WHERE connection_id=$1", [access.connection.id])
assert.equal(live.rows[0].count, 1, "caller cancellation must wait for the pending read before releasing the lease")
release()
for (;;) {
const { rows: [audit] } = await pool.query(
"SELECT status,error_code,result FROM mcp_requests WHERE connection_id=$1 AND tool='mm_logs_follow'", [access.connection.id],
)
const protocol = await pool.query("SELECT count(*)::int AS count FROM mcp_requests WHERE connection_id=$1 AND tool='protocol:tools/call' AND finished_at IS NOT NULL", [access.connection.id])
if (audit && audit.status !== "running" && protocol.rows[0].count === 1) {
assert.equal(audit.status, "error")
assert.equal(audit.error_code, "READ_CANCELLED")
assert.doesNotMatch(JSON.stringify(audit.result), /"cursor"/)
break
}
await Promise.race([delay(25), deadline])
}
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_read_leases WHERE connection_id=$1", [access.connection.id])).rows[0].count, 0)
assert.equal(readCount, 1, "cancelled follow does not dispatch a second read")
} finally {
caller.abort()
release()
await refused.catch(() => undefined)
}
pool.query = originalQuery
const from = "2000-01-01T00:00:00Z", to = "2000-01-01T00:30:00Z"
const followOnce = (filters: { from?: string; cursor?: string }) => client.callTool({
name: "mm_logs_follow", arguments: { serverId: 1, waitSeconds: 0, to, ...filters },
})
const initial = await followOnce({ from })
assert.ok(!initial.isError)
const cursor = (initial.structuredContent as { result?: { cursor?: unknown } } | undefined)?.result?.cursor
assert.ok(typeof cursor === "string" && cursor.length > 0)
assert.ok(!(await followOnce({ from, cursor })).isError)
assert.ok(!(await followOnce({ cursor })).isError, "omitting from must resume the historical cursor's period")
const changed = await followOnce({ from: "2000-01-01T00:01:00Z", cursor })
assert.equal(changed.isError, true)
assert.match(JSON.stringify(changed), /RESET_REQUIRED/)
assert.doesNotMatch(JSON.stringify(changed), /"cursor"/)
console.log("✓ MCP logs follow: historical cursor resumes, changed from requires reset")
const marker = `mcp-logs-cursor-${randomUUID()}`
const readFollowPage = async (options: { cursor?: string; limit?: number }) => {
const result = await client.callTool({ name: "mm_logs_follow", arguments: { serverId: 1, from, to, q: marker, waitSeconds: 0, ...options } })
assert.ok(!result.isError)
return (result.structuredContent as { result: { entries: { id: string }[]; cursor: string } }).result
}
const start = await readFollowPage({})
assert.deepEqual(start.entries, [])
try {
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 1,'cursor fixture','127.0.0.1','fixture','00:02:00',now(),$1::timestamptz+n*interval '2 minutes',ARRAY['system'],'info',$2,0
FROM generate_series(1,2) AS n`, [from, marker])
const defaultFollow = await readFollowPage({ cursor: start.cursor })
assert.equal(defaultFollow.entries.length, 2)
const firstFollow = await readFollowPage({ cursor: start.cursor, limit: 1 })
const secondFollow = await readFollowPage({ cursor: firstFollow.cursor, limit: 1 })
assert.equal(firstFollow.entries.length, 1)
assert.equal(secondFollow.entries.length, 1)
assert.deepEqual([...firstFollow.entries, ...secondFollow.entries].map(row => row.id), defaultFollow.entries.map(row => row.id))
assert.deepEqual((await readFollowPage({ cursor: secondFollow.cursor, limit: 1 })).entries, [])
console.log("✓ MCP logs follow: requested batch limit and lossless cursor progression")
const query = (filters: { from?: string; cursor?: string }) => client.callTool({
name: "mm_logs_query", arguments: { serverId: 1, q: marker, limit: 1, to, ...filters },
})
type Page = { entries: { id: string }[]; nextCursor: string | null }
const first = await query({ from })
assert.ok(!first.isError)
const firstPage = (first.structuredContent as { result: Page }).result
assert.equal(firstPage.entries.length, 1)
assert.ok(firstPage.nextCursor)
const next = await query({ cursor: firstPage.nextCursor })
assert.ok(!next.isError)
const nextPage = (next.structuredContent as { result: Page }).result
assert.equal(nextPage.entries.length, 1)
assert.notEqual(nextPage.entries[0].id, firstPage.entries[0].id)
assert.equal(nextPage.nextCursor, null)
console.log("✓ MCP logs query: historical pagination resumes without resending from")
} finally {
await pool.query("DELETE FROM router_log_entries WHERE server_id=1 AND message=$1", [marker])
}
} finally {
bound.abort()
pool.query = originalQuery
app.server.off("request", observeClose)
await client.close()
}
}
+12 -2
View File
@@ -41,7 +41,7 @@ export async function registerManagementChannel(raw: unknown, actor: AuthUser) {
}) })
return { id, serverId: input.serverId, expiresAt: input.expiresAt, verification: "identity-and-reachability", independence: "human-attestation" } return { id, serverId: input.serverId, expiresAt: input.expiresAt, verification: "identity-and-reachability", independence: "human-attestation" }
} }
export async function verifyManagementChannel(serverId: number, requiredId?: string) { async function verifiedManagementChannel(serverId: number, requiredId?: string) {
const { rows: [row] } = await pool.query("SELECT * FROM mcp_management_channels WHERE server_id=$1 AND revoked_at IS NULL AND expires_at>now()", [serverId]) const { rows: [row] } = await pool.query("SELECT * FROM mcp_management_channels WHERE server_id=$1 AND revoked_at IS NULL AND expires_at>now()", [serverId])
if (!row || requiredId && row.id !== requiredId || row.generation !== await accessGeneration()) throw new McpError("APPROVAL_REQUIRED", "Нет актуального проверенного независимого канала управления", 409) if (!row || requiredId && row.id !== requiredId || row.generation !== await accessGeneration()) throw new McpError("APPROVAL_REQUIRED", "Нет актуального проверенного независимого канала управления", 409)
const config = JSON.parse(await decryptSecret(row.config_sealed)) as MikrotikConnectParams const config = JSON.parse(await decryptSecret(row.config_sealed)) as MikrotikConnectParams
@@ -50,7 +50,17 @@ export async function verifyManagementChannel(serverId: number, requiredId?: str
if (hashes.some(value => value !== row.identity_hash)) throw new McpError("STATE_CONFLICT", "Идентичность основного или management-канала изменилась", 409) if (hashes.some(value => value !== row.identity_hash)) throw new McpError("STATE_CONFLICT", "Идентичность основного или management-канала изменилась", 409)
const changed = await pool.query("UPDATE mcp_management_channels SET verified_at=now() WHERE id=$1 AND revoked_at IS NULL AND expires_at>now() RETURNING id", [row.id]) const changed = await pool.query("UPDATE mcp_management_channels SET verified_at=now() WHERE id=$1 AND revoked_at IS NULL AND expires_at>now() RETURNING id", [row.id])
if (!changed.rowCount) throw new McpError("APPROVAL_REQUIRED", "Допуск management-канала был отозван во время проверки", 409) if (!changed.rowCount) throw new McpError("APPROVAL_REQUIRED", "Допуск management-канала был отозван во время проверки", 409)
return { channelId: row.id as string, serverId, expiresAt: row.expires_at.toISOString() as string } return { channelId: row.id as string, serverId, expiresAt: row.expires_at.toISOString() as string, host: config.host }
}
export async function verifyManagementChannel(serverId: number, requiredId?: string) {
const { host: _host, ...receipt } = await verifiedManagementChannel(serverId, requiredId)
return receipt
}
/** Internal VLAN preparation uses the verified endpoint to trace its actual
* address/interface. It is not accepted from tool input or returned in the
* public channel receipt. */
export async function verifyBridgeManagementChannel(serverId: number, requiredId?: string) {
return verifiedManagementChannel(serverId, requiredId)
} }
export async function managementEvidence(serverIds: number[]) { export async function managementEvidence(serverIds: number[]) {
try { return { requiresHuman: false, managementChannels: await Promise.all([...new Set(serverIds)].map(serverId => verifyManagementChannel(serverId))) } } try { return { requiresHuman: false, managementChannels: await Promise.all([...new Set(serverIds)].map(serverId => verifyManagementChannel(serverId))) } }
@@ -11,7 +11,7 @@ const creates = rosResources.filter(resource => !resource.readOnly && resource.c
assert.equal(creates.length, 30) assert.equal(creates.length, 30)
for (const resource of creates) { for (const resource of creates) {
const action = findAction(`mm_${resource.id}_create_plan`) const action = findAction(`mm_${resource.id}_create_plan`)
assert.equal(action.operation.version, ["ospf_template", "ipsec_peer"].includes(resource.id) ? 5 : ["wireguard", "vrf"].includes(resource.id) ? 4 : 3) assert.equal(action.operation.version, ["ospf_template", "ipsec_peer", "vrf"].includes(resource.id) ? 5 : ["wireguard", "bridge", "bridge_port", "bridge_vlan"].includes(resource.id) ? 4 : 3)
assert.equal(action.schema.safeParse({ serverId: 1, fields: {} }).success, true) assert.equal(action.schema.safeParse({ serverId: 1, fields: {} }).success, true)
for (const id of ["*OLD", undefined, null, ""]) { for (const id of ["*OLD", undefined, null, ""]) {
const rejected = action.schema.safeParse({ serverId: 1, fields: {}, id }) const rejected = action.schema.safeParse({ serverId: 1, fields: {}, id })
@@ -5,6 +5,8 @@ await prepareOwnedBandwidthState()
const { pool, closePool } = await import("../../db/index.js") const { pool, closePool } = await import("../../db/index.js")
try { try {
const { applySqlMigrations } = await import("../../db/migrate.js"); await applySqlMigrations(pool) const { applySqlMigrations } = await import("../../db/migrate.js"); await applySqlMigrations(pool)
const { acceptOperationReplayTombstones } = await import("./operation-replay-tombstones.sql-helper.js")
await acceptOperationReplayTombstones()
const { acceptOwnedMcpOperationRoot } = await import("./operation-execution-origin.sql-helper.js") const { acceptOwnedMcpOperationRoot } = await import("./operation-execution-origin.sql-helper.js")
console.log("Owned actual MCP operation root:", JSON.stringify(await acceptOwnedMcpOperationRoot())) console.log("Owned actual MCP operation root:", JSON.stringify(await acceptOwnedMcpOperationRoot()))
} finally { await closePool() } } finally { await closePool() }
@@ -0,0 +1,74 @@
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { assertOwnedBandwidthSql } from "../../services/bandwidth-admission-process.sql-helper.js"
export async function acceptOperationReplayTombstones() {
assertOwnedBandwidthSql()
assert.equal(process.env.MMAPP_OWN_MCP_OPERATION_ORIGIN, "true")
const { pool } = await import("../../db/index.js")
const { mcpConnectionCreateSchema } = await import("@mmapp/contracts/mcp")
const { createMcpConnection, authenticateMcpToken } = await import("./store.js")
const { applyPlan, invokeMcpTool } = await import("./runtime.js")
const { operationIdempotencyDigest } = await import("./operation-replay-tombstones.js")
const { encryptSecret, McpError } = await import("./security.js")
const { default: Fastify } = await import("fastify")
const { default: authPlugin } = await import("../../plugins/auth.js")
const { createMcpRoutes } = await import("../../routes/mcp.js")
const { env } = await import("../../config.js")
const { SignJWT } = await import("jose")
const makeConnection = async () => {
const actor = { id: randomUUID(), email: "owned-tombstone@example.test", name: "Owned replay fixture", apps: ["mm"],
permissions: ["mm:users:write", "mm:mcp:read"], 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: [], operationIds: ["users.create", "operations.read", "changes.apply"], allowGlobal: true },
limits: { callsPerMinute: 100, mutationsPerHour: 10 } }), actor)
return { actor, principal: await authenticateMcpToken(issued.token) }
}
const app = Fastify()
const ids: string[] = []
const matches = (code: string, status: number) => (error: unknown) =>
error instanceof McpError && error.code === code && error.statusCode === status
await pool.query("UPDATE public.mcp_settings SET enabled=true WHERE id=1")
try {
const owner = await makeConnection(), foreign = await makeConnection()
// A real completed plan has already initialized the owned encryption state.
// The fixture seeds its tombstone directly, so exercise the same initializer.
await encryptSecret("owned replay fixture state")
const connectionId = owner.principal.connectionId
ids.push(connectionId, foreign.principal.connectionId)
const operationId = randomUUID(), planId = randomUUID(), key = "owned-replay-" + randomUUID()
await pool.query(`INSERT INTO public.mcp_operation_replay_tombstones
(operation_id,plan_id,connection_id,idempotency_digest,action_id,target_server_ids,target_global)
VALUES($1,$2,$3,$4,'users.create','{}',true)`, [operationId, planId, connectionId, operationIdempotencyDigest(connectionId,key)])
await assert.rejects(applyPlan(owner.principal, planId, key), matches("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(applyPlan(owner.principal, planId, randomUUID()), matches("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(applyPlan(owner.principal, randomUUID(), key), matches("STATE_CONFLICT",409))
await assert.rejects(applyPlan(foreign.principal, planId, key), matches("NOT_FOUND",404))
await assert.rejects(invokeMcpTool(owner.principal,"mm_change_apply",{ planId,idempotencyKey:key }), matches("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(invokeMcpTool(owner.principal,"mm_operation_get",{ operationId }), matches("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(invokeMcpTool(foreign.principal,"mm_operation_get",{ operationId }), matches("NOT_FOUND",404))
await app.register(authPlugin)
await app.register(createMcpRoutes(async () => {}))
for (const subject of [owner, foreign]) {
const token = await new SignJWT({ sub: subject.actor.id, email: subject.actor.email, name: subject.actor.name,
apps: subject.actor.apps, permissions: subject.actor.permissions })
.setProtectedHeader({ alg:"HS256" }).setIssuer(env.authIssuer).setIssuedAt().setExpirationTime("4m")
.sign(new TextEncoder().encode(env.jwtSecret))
const reply = await app.inject({ method: "POST", url: "/mcp/plans/"+planId+"/apply", headers: { authorization:"Bearer "+token } })
assert.equal(reply.statusCode, subject === owner ? 410 : 404)
assert.equal(reply.json().code, subject === owner ? "OPERATION_HISTORY_EXPIRED" : "NOT_FOUND")
}
await pool.query("UPDATE public.mcp_connections SET policy=jsonb_set(policy,'{scope,allowGlobal}','false') WHERE id=$1", [connectionId])
await assert.rejects(applyPlan(owner.principal, planId, key), matches("NOT_FOUND",404))
await assert.rejects(invokeMcpTool(owner.principal,"mm_operation_get",{ operationId }), matches("NOT_FOUND",404))
const { rows:[proof] } = await pool.query(`SELECT
NOT EXISTS(SELECT 1 FROM public.mcp_operations WHERE connection_id=ANY($1::uuid[])) AS no_operation,
NOT EXISTS(SELECT 1 FROM public.mcp_budget_buckets WHERE connection_id=ANY($1::uuid[]) AND kind='mutations') AS no_write_budget`, [ids])
assert.deepEqual(proof,{no_operation:true,no_write_budget:true})
console.log("Owned operation tombstones: scoped MCP and Web 410, replay conflict and no dispatch")
} finally {
await app.close()
await pool.query("DELETE FROM public.mcp_operation_replay_tombstones WHERE connection_id=ANY($1::uuid[])", [ids])
}
}
@@ -0,0 +1,64 @@
import { createHash } from "node:crypto"
import type { PoolClient } from "pg"
import { hasPermission } from "../../lib/permissions.js"
import { listActions } from "./actions.js"
import { McpError } from "./security.js"
import { effectiveMcpConnection, type ConnectionRow, type McpPrincipal } from "./store.js"
type Query = Pick<PoolClient, "query">
export interface OperationReplayTombstone {
operation_id: string
plan_id: string
connection_id: string
action_id: string
target_server_ids: string[]
target_global: boolean
}
// Idempotency tokens are caller input and already stored in plaintext on live
// operations. This domain-separated digest avoids copying that plaintext and
// survives DB restore and access-key rotation. It is not a keyed secret proof.
export function operationIdempotencyDigest(connectionId: string, key: string): string {
return createHash("sha256").update("mcp-operation-replay-v1\0").update(connectionId).update("\0").update(key).digest("hex")
}
export function operationHistoryExpired(): never {
throw new McpError("OPERATION_HISTORY_EXPIRED", "Срок хранения завершённой операции истёк. Повторное применение запрещено.", 410)
}
export function assertOperationReplayScope(row: OperationReplayTombstone, connection: ConnectionRow): void {
const scope = connection.policy.scope
if (row.connection_id !== connection.id || !scope.operationIds.includes(row.action_id)) throw operationReplayNotFound()
const action = listActions().find(candidate => candidate.operation.id === row.action_id)
if (!action || action.operation.requiredPermissions.some(permission =>
!hasPermission(connection.policy.permissions, permission) || !hasPermission(connection.owner_permissions, permission))) throw operationReplayNotFound()
if (row.target_global && !scope.allowGlobal || row.target_server_ids.some(id => !scope.serverIds.some(server => String(server) === id))) throw operationReplayNotFound()
}
function operationReplayNotFound(): McpError {
return new McpError("NOT_FOUND", "Операция не найдена", 404)
}
const projection = "t.operation_id,t.plan_id,t.connection_id,t.action_id,t.target_server_ids::text[] AS target_server_ids,t.target_global"
export async function findOperationReplayTombstone(db: Query, connectionId: string, operationId: string) {
return (await db.query<OperationReplayTombstone>(`SELECT ${projection} FROM mcp_operation_replay_tombstones t
WHERE t.connection_id=$1 AND t.operation_id=$2`, [connectionId, operationId])).rows[0]
}
export async function findOwnerPlanReplayTombstone(db: Query, ownerId: string, planId: string) {
return (await db.query<OperationReplayTombstone>(`SELECT ${projection} FROM mcp_operation_replay_tombstones t
JOIN mcp_connections c ON c.id=t.connection_id WHERE c.owner_id=$1 AND t.plan_id=$2`, [ownerId, planId])).rows[0]
}
export async function refuseExpiredOperationReplay(db: Query, principal: McpPrincipal, connectionId: string, planId: string, key: string): Promise<void> {
const rows = (await db.query<OperationReplayTombstone>(`SELECT ${projection} FROM mcp_operation_replay_tombstones t
WHERE t.connection_id=$1 AND (t.plan_id=$2 OR t.idempotency_digest=$3)`,
[connectionId, planId, operationIdempotencyDigest(connectionId, key)])).rows
if (!rows.length) return
const conflict = rows.find(row => row.plan_id !== planId.toLowerCase())
const current = await effectiveMcpConnection(principal, db)
assertOperationReplayScope(conflict ?? rows[0], current)
if (conflict) throw new McpError("STATE_CONFLICT", "Ключ уже используется другим планом", 409)
operationHistoryExpired()
}
@@ -38,7 +38,7 @@ assert.throws(() => action.schema.parse({ serverId: 1, expectedServerProof: "for
assert.throws(() => action.schema.parse({ serverId: 1, version: 1 })) assert.throws(() => action.schema.parse({ serverId: 1, version: 1 }))
assert.equal(action.schema.parse({ serverId: 1 }).family, "ipv4") assert.equal(action.schema.parse({ serverId: 1 }).family, "ipv4")
assert.equal(findAction("mm_routing_measurement_plan").operation.version, 2) assert.equal(findAction("mm_routing_measurement_plan").operation.version, 2)
assert.equal(findAction("mm_operation_recovery_plan").operation.version, 7) assert.equal(findAction("mm_operation_recovery_plan").operation.version, 8)
assert.equal(findAction("mm_ospf_template_create_plan").operation.version, 5) assert.equal(findAction("mm_ospf_template_create_plan").operation.version, 5)
assert.equal(findAction("mm_ospf_template_update_plan").operation.version, 4) assert.equal(findAction("mm_ospf_template_update_plan").operation.version, 4)
assert.equal(findAction("mm_ospf_template_delete_plan").operation.version, 3) assert.equal(findAction("mm_ospf_template_delete_plan").operation.version, 3)
@@ -49,11 +49,11 @@ assert.equal(findAction("mm_wireguard_update_plan").operation.version, 3)
assert.equal(findAction("mm_wireguard_interface_edit_plan").operation.version, 3) assert.equal(findAction("mm_wireguard_interface_edit_plan").operation.version, 3)
assert.equal(findAction("mm_wireguard_interface_create_plan").operation.version, 3) assert.equal(findAction("mm_wireguard_interface_create_plan").operation.version, 3)
assert.equal(findAction("mm_wireguard_create_plan").operation.version, 4) assert.equal(findAction("mm_wireguard_create_plan").operation.version, 4)
assert.equal(findAction("mm_interfaces_update_plan").operation.version, 4) 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_delete_plan").operation.version, 3)
assert.equal(findAction("mm_ipsec_peer_update_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) 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", 7], ["mm_ecmp_plan", 2], ["mm_rpf_plan", 2], ["mm_vrf_plan", 3], ["mm_firewall_rule_update_plan", 2]] as const) { 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) {
const routed = findAction(tool) const routed = findAction(tool)
assert.equal(routed.operation.version, version) assert.equal(routed.operation.version, version)
assert.equal(findMcpOperation(routed.operation.id)?.version, version) assert.equal(findMcpOperation(routed.operation.id)?.version, version)
@@ -0,0 +1,226 @@
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { setTimeout as delay } from "node:timers/promises"
import { assertOwnedBandwidthSql,prepareOwnedBandwidthState } from "../../services/bandwidth-admission-process.sql-helper.js"
assertOwnedBandwidthSql()
assert.equal(process.env.MMAPP_OWN_MCP_OPERATION_ORIGIN,"true")
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 {invokeMcpTool,approvePlan,applyPlan}=await import("./runtime.js")
const {OrdinaryRetirementCommitUnknown,retireOrdinaryMcpOperations}=await import("./ordinary-operation-retirement.js")
const {tickMcpWorkers}=await import("./workers.js")
const {pauseSchedulerJobsAndWait,resumeSchedulerJobs}=await import("../../services/scheduler-running.js")
const {Client}=await import("pg")
const {McpError}=await import("./security.js")
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
try {
await applySqlMigrations(pool)
await pauseSchedulerJobsAndWait()
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(){
const actor={id:randomUUID(),email:"owned-logs-retirement@example.test",name:"Owned settings retention",apps:["mm"],
permissions:["mm:settings:admin","mm:mcp:approve","mm:traffic:write","mm:uptime:write","mm:servers:write","mm:network:write","mm:backups:write"],
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",
"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
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}
if(tool==="mm_geoip_settings_update_plan"||tool==="mm_backup_schedule_update_plan")assert.equal(plan.state,"awaiting-approval")
if(plan.state==="awaiting-approval")await approvePlan(plan.planId,actor,true)
const key=randomUUID(),applied=await applyPlan(principal,plan.planId,key)
let terminal
for(const deadline=Date.now()+15000;Date.now()<deadline;){
terminal=(await pool.query("SELECT state,verification,error_code FROM mcp_operations WHERE id=$1",[applied.id])).rows[0]
if(terminal&&!["pending","running"].includes(terminal.state))break
await delay(50)
}
assert.deepEqual(terminal,{state:"applied",verification:"passed",error_code:null})
// Only the disposable fixture ages rows, after real authenticated apply.
// No cutoff override or production encryption/approval bypass.
const finished=new Date(Date.now()-ageMs),created=new Date(finished.getTime()-1000)
await pool.query("UPDATE mcp_plans SET created_at=$2,approved_at=$2,expires_at=$3 WHERE id=$1",
[plan.planId,created,new Date(finished.getTime()+1000)])
await pool.query("UPDATE mcp_operations SET created_at=$2,finished_at=$3 WHERE id=$1",[applied.id,created,finished])
await pool.query("UPDATE mcp_operation_steps SET created_at=$2,finished_at=$3 WHERE operation_id=$1",[applied.id,created,finished])
return {id:applied.id,planId:plan.planId,key}
}
const age=180*86400000
const held=await operation(51,age+180000),spent=await operation(52,age+120000)
const due=await operation(53,age+60000),young=await operation(54,age-60000)
const boundary=await pool.connect()
try{
await boundary.query("BEGIN")
await boundary.query("UPDATE mcp_operations SET finished_at=transaction_timestamp()-interval '180 days' WHERE id=$1",[young.id])
await assert.rejects(boundary.query(`INSERT INTO mcp_ordinary_retirement_claims
(operation_id,plan_id,connection_id,source_proof,claim_token) VALUES($1,$2,$3,$4,$5)`,
[young.id,young.planId,principal.connectionId,"a".repeat(64),randomUUID()]),/cannot be replaced or reopened/)
}finally{await boundary.query("ROLLBACK");boundary.release()}
await pool.query("INSERT INTO mcp_drift(id,operation_id,expected,observed) VALUES($1,$2,'{}','{}')",[randomUUID(),held.id])
const ids=[held.id,spent.id,due.id,young.id]
const projection=async()=>(await pool.query(`SELECT i.id,o.state,p.state AS plan_state,s.state AS step_state,
k.operation_id AS checkpoint,t.operation_id AS tombstone,c.claim_token
FROM unnest($1::uuid[]) i(id)
LEFT JOIN mcp_operations o ON o.id=i.id LEFT JOIN mcp_plans p ON p.id=o.plan_id
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 originalQuery=Client.prototype.query
let commits=0
try{
Client.prototype.query=function(this:InstanceType<typeof Client>,...args:unknown[]){
if((args[0] as {text?:string})?.text==="COMMIT"&&++commits===3)
return Promise.reject(Error("owned ordinary destructive COMMIT transport loss"))
return Reflect.apply(originalQuery,this,args)
} as typeof Client.prototype.query
await assert.rejects(retireOrdinaryMcpOperations(),
(error:unknown)=>error instanceof OrdinaryRetirementCommitUnknown&&error.automaticReplay===false)
}finally{Client.prototype.query=originalQuery}
assert.equal(commits,3)
const before=await projection(),spentBefore=before.find(row=>row.id===spent.id)
assert.equal(spentBefore?.state,"applied");assert(spentBefore?.claim_token);assert.equal(spentBefore?.tombstone,null)
await tickMcpWorkers()
const after=await projection()
assert.deepEqual(after.filter(row=>row.id!==due.id),before.filter(row=>row.id!==due.id))
const retired=after.find(row=>row.id===due.id)
assert(retired?.claim_token);assert.equal(retired?.tombstone,due.id)
for(const field of ["state","plan_state","step_state","checkpoint"])assert.equal(retired[field],null)
assert.equal((await pool.query("SELECT 1 FROM mcp_plans WHERE id=$1",[due.planId])).rows.length,0)
assert.equal((await pool.query("SELECT retention_days FROM router_log_settings WHERE id=1")).rows[0].retention_days,54)
await tickMcpWorkers()
assert.deepEqual(await projection(),after)
await assert.rejects(applyPlan(principal,due.planId,due.key),errorIs("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(applyPlan(principal,randomUUID(),due.key),errorIs("STATE_CONFLICT",409))
await assert.rejects(invokeMcpTool(foreign,"mm_operation_get",{operationId:due.id}),errorIs("NOT_FOUND",404))
const telemetry=[
{action:"traffic.settings.update",version:2,record:await operation(55,age+60000,"mm_traffic_settings_update_plan")},
{action:"uptime.settings.update",version:2,record:await operation(56,age+60000,"mm_uptime_settings_update_plan",{enabled:false,retentionDays:56})},
{action:"servers.api-ping.settings.update",version:2,record:await operation(0,age+60000,"mm_servers_api_ping_settings_update_plan",{intervalSec:123})},
{action:"internet-path.settings.update",version:1,record:await operation(57,age+60000,"mm_internet_path_settings_update_plan")},
]
const telemetryRows=async()=>(await pool.query(`SELECT p.operation_id,p.operation_version,o.id
FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id WHERE o.id=ANY($1::uuid[]) ORDER BY p.operation_id`,
[telemetry.map(item=>item.record.id)])).rows
assert.deepEqual((await telemetryRows()).map(row=>[row.operation_id,row.operation_version]),
telemetry.map(item=>[item.action,item.version]).sort((a,b)=>String(a[0]).localeCompare(String(b[0]))))
const telemetrySettings=async()=>(await pool.query(`SELECT to_jsonb(t) AS traffic,to_jsonb(u) AS uptime,
to_jsonb(a) AS api_ping,to_jsonb(i) AS internet FROM traffic_settings t,uptime_settings u,
servers_api_ping_settings a,internet_path_settings i WHERE t.id=1 AND u.id=1 AND a.id=1 AND i.id=1`)).rows[0]
const settingsBefore=await telemetrySettings()
const traffic=telemetry[0].record
const {rows:[originalStep]}=await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1",[traffic.id])
await pool.query("UPDATE mcp_operation_steps SET result=jsonb_set(result,'{retentionDays}','999') WHERE operation_id=$1",[traffic.id])
await tickMcpWorkers()
assert.equal((await pool.query("SELECT 1 FROM mcp_ordinary_retirement_claims WHERE operation_id=$1",[traffic.id])).rows.length,0)
assert.equal((await pool.query("SELECT 1 FROM mcp_operations WHERE id=$1",[traffic.id])).rows.length,1)
await pool.query("UPDATE mcp_operation_steps SET result=$2 WHERE operation_id=$1",[traffic.id,originalStep.result])
await tickMcpWorkers()
await tickMcpWorkers()
// The keyset owner may need one tick to wrap after the rejected oldest row.
await tickMcpWorkers()
assert.deepEqual(await telemetryRows(),[])
assert.deepEqual(await telemetrySettings(),settingsBefore)
for(const {action,record} of telemetry){
const {rows:[proof]}=await pool.query(`SELECT t.action_id,t.target_global,t.target_server_ids::text[] AS server_ids,
NOT EXISTS(SELECT 1 FROM mcp_plans WHERE id=$2) AS plan_gone,
NOT EXISTS(SELECT 1 FROM mcp_operation_steps WHERE operation_id=$1) AS step_gone,
NOT EXISTS(SELECT 1 FROM mcp_operation_checkpoints WHERE operation_id=$1) AS checkpoint_gone
FROM mcp_operation_replay_tombstones t WHERE t.operation_id=$1 AND t.plan_id=$2`,[record.id,record.planId])
assert.deepEqual(proof,{action_id:action,target_global:true,server_ids:[String(serverId)],plan_gone:true,step_gone:true,checkpoint_gone:true})
await assert.rejects(applyPlan(principal,record.planId,record.key),errorIs("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(invokeMcpTool(foreign,"mm_operation_get",{operationId:record.id}),errorIs("NOT_FOUND",404))
}
// Keep the owned fixture offline even if a product scheduler refresh occurs.
await pool.query("UPDATE geoip_settings SET enabled=false WHERE id=1")
await touchGeoipRunMeta({lastCheckAt:new Date().toISOString(),lastError:"owned GeoIP metadata",etags:{"owned-test":"unchanged"}})
const geoip=await operation(0,age+60000,"mm_geoip_settings_update_plan",{enabled:false,updateIntervalSec:86400})
const geoipState=async()=>(await pool.query("SELECT to_jsonb(g) AS row FROM geoip_settings g WHERE id=1")).rows[0].row
const geoipBefore=await geoipState()
assert.equal(geoipBefore.enabled,false);assert.equal(geoipBefore.update_interval_sec,86400)
assert.equal(geoipBefore.last_error,"owned GeoIP metadata");assert.deepEqual(geoipBefore.etags_json,{"owned-test":"unchanged"})
const {rows:[geoipStep]}=await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1",[geoip.id])
const assertGeoipUnclaimed=async()=>{
assert.equal((await pool.query("SELECT 1 FROM mcp_ordinary_retirement_claims WHERE operation_id=$1",[geoip.id])).rows.length,0)
assert.equal((await pool.query("SELECT 1 FROM mcp_operations WHERE id=$1",[geoip.id])).rows.length,1)
assert.deepEqual(await geoipState(),geoipBefore)
}
await pool.query("UPDATE mcp_operation_steps SET result=jsonb_set(result,'{settings,updateIntervalSec}','172800') WHERE operation_id=$1",[geoip.id])
await tickMcpWorkers();await tickMcpWorkers();await assertGeoipUnclaimed()
await pool.query("UPDATE mcp_operation_steps SET result=$2 WHERE operation_id=$1",[geoip.id,geoipStep.result])
const geoipHold=randomUUID()
await pool.query("INSERT INTO mcp_drift(id,operation_id,expected,observed) VALUES($1,$2,'{}','{}')",[geoipHold,geoip.id])
await tickMcpWorkers();await assertGeoipUnclaimed()
await pool.query("DELETE FROM mcp_drift WHERE id=$1",[geoipHold])
await tickMcpWorkers();await tickMcpWorkers()
const {rows:[geoipTombstone]}=await pool.query(`SELECT t.action_id,t.target_global,t.target_server_ids::text[] AS server_ids,
NOT EXISTS(SELECT 1 FROM mcp_operations WHERE id=$1) AS operation_gone,
NOT EXISTS(SELECT 1 FROM mcp_plans WHERE id=$2) AS plan_gone,
NOT EXISTS(SELECT 1 FROM mcp_operation_steps WHERE operation_id=$1) AS step_gone,
NOT EXISTS(SELECT 1 FROM mcp_operation_checkpoints WHERE operation_id=$1) AS checkpoint_gone
FROM mcp_operation_replay_tombstones t WHERE t.operation_id=$1 AND t.plan_id=$2`,[geoip.id,geoip.planId])
assert.deepEqual(geoipTombstone,{action_id:"geoip.settings.update",target_global:true,server_ids:[String(serverId)],
operation_gone:true,plan_gone:true,step_gone:true,checkpoint_gone:true})
await assert.rejects(applyPlan(principal,geoip.planId,geoip.key),errorIs("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(applyPlan(principal,randomUUID(),geoip.key),errorIs("STATE_CONFLICT",409))
await assert.rejects(invokeMcpTool(foreign,"mm_operation_get",{operationId:geoip.id}),errorIs("NOT_FOUND",404))
assert.deepEqual(await geoipState(),geoipBefore)
await pool.query("UPDATE backup_schedule_settings SET enabled=false,server_ids_json='[]' WHERE id=1")
await touchBackupScheduleRunMeta({lastRunAt:new Date().toISOString(),lastDurationMs:123,lastError:"owned backup scheduler metadata"})
await pool.query(`INSERT INTO backup_entries(id,server_id,server_name,filename,size_bytes,kind,notes,created_at)
VALUES($1,$2,'owned fixture','owned-retention.rsc',123,'auto','keep',clock_timestamp())`,[randomUUID(),serverId])
const backupState=async()=>(await pool.query(`SELECT to_jsonb(s) AS schedule,
(SELECT jsonb_agg(to_jsonb(b) ORDER BY id) FROM backup_entries b) AS entries
FROM backup_schedule_settings s WHERE id=1`)).rows[0]
const backupOutside=await operation(0,age+120000,"mm_backup_schedule_update_plan",{enabled:false,serverIds:[String(serverId+1000)]})
const outsideState=await backupState()
await tickMcpWorkers();await tickMcpWorkers()
assert.equal((await pool.query("SELECT 1 FROM mcp_ordinary_retirement_claims WHERE operation_id=$1",[backupOutside.id])).rows.length,0)
assert.equal((await pool.query("SELECT 1 FROM mcp_operations WHERE id=$1",[backupOutside.id])).rows.length,1)
assert.deepEqual(await backupState(),outsideState)
await pool.query("UPDATE backup_schedule_settings SET server_ids_json='[]' WHERE id=1")
const backup=await operation(0,age+60000,"mm_backup_schedule_update_plan",{enabled:false,keepCount:9,serverIds:[String(serverId)]})
const backupBefore=await backupState()
assert.equal(backupBefore.schedule.enabled,false);assert.equal(backupBefore.schedule.keep_count,9)
assert.deepEqual(backupBefore.schedule.server_ids_json,[String(serverId)])
assert.equal(backupBefore.schedule.last_duration_ms,123);assert.equal(backupBefore.schedule.last_error,"owned backup scheduler metadata")
assert.deepEqual(backupBefore.entries,outsideState.entries)
const {rows:[backupStep]}=await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1",[backup.id])
await pool.query("UPDATE mcp_operation_steps SET result=jsonb_set(result,'{keepCount}','10') WHERE operation_id=$1",[backup.id])
await tickMcpWorkers();await tickMcpWorkers()
assert.equal((await pool.query("SELECT 1 FROM mcp_ordinary_retirement_claims WHERE operation_id=$1",[backup.id])).rows.length,0)
assert.deepEqual(await backupState(),backupBefore)
await pool.query("UPDATE mcp_operation_steps SET result=$2 WHERE operation_id=$1",[backup.id,backupStep.result])
await tickMcpWorkers();await tickMcpWorkers()
const {rows:[backupTombstone]}=await pool.query(`SELECT t.action_id,t.target_global,t.target_server_ids::text[] AS server_ids,
NOT EXISTS(SELECT 1 FROM mcp_operations WHERE id=$1) AS operation_gone,
NOT EXISTS(SELECT 1 FROM mcp_plans WHERE id=$2) AS plan_gone,
NOT EXISTS(SELECT 1 FROM mcp_operation_steps WHERE operation_id=$1) AS step_gone,
NOT EXISTS(SELECT 1 FROM mcp_operation_checkpoints WHERE operation_id=$1) AS checkpoint_gone
FROM mcp_operation_replay_tombstones t WHERE t.operation_id=$1 AND t.plan_id=$2`,[backup.id,backup.planId])
assert.deepEqual(backupTombstone,{action_id:"backups.schedule.update",target_global:true,server_ids:[String(serverId)],
operation_gone:true,plan_gone:true,step_gone:true,checkpoint_gone:true})
await assert.rejects(applyPlan(principal,backup.planId,backup.key),errorIs("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(applyPlan(principal,randomUUID(),backup.key),errorIs("STATE_CONFLICT",409))
await assert.rejects(invokeMcpTool(foreign,"mm_operation_get",{operationId:backup.id}),errorIs("NOT_FOUND",404))
assert.deepEqual(await backupState(),backupBefore)
await pool.query("UPDATE mcp_connections SET policy=jsonb_set(policy,'{scope,serverIds}','[]') WHERE id=$1",[principal.connectionId])
await assert.rejects(applyPlan(principal,due.planId,due.key),errorIs("NOT_FOUND",404))
for(const {record} of telemetry)await assert.rejects(applyPlan(principal,record.planId,record.key),errorIs("NOT_FOUND",404))
await assert.rejects(applyPlan(principal,geoip.planId,geoip.key),errorIs("NOT_FOUND",404))
await assert.rejects(applyPlan(principal,backup.planId,backup.key),errorIs("NOT_FOUND",404))
console.log("Owned settings operations: seven exact families, authenticated apply, physical 180-day purge, preserved settings and scoped replay")
}finally{resumeSchedulerJobs();await closePool()}
@@ -0,0 +1,221 @@
import { randomUUID } from "node:crypto"
import { logSettingsPatchSchema, logSettingsSchema } from "@mmapp/contracts"
import { geoipSettingsDtoSchema, geoipSettingsPatchSchema } from "@mmapp/contracts/geoip"
import { backupScheduleSettingsDtoSchema, putBackupScheduleSettingsSchema } from "@mmapp/contracts/backups"
import { z } from "zod"
import { connectProbeStorage } from "../../services/server-probe-storage-connection.js"
import type { ProbeStorageSql } from "../../services/server-probe-journal-maintenance.js"
import { sharedDatabaseGate } from "../../services/shared-database-gate.js"
import { operationIdempotencyDigest } from "./operation-replay-tombstones.js"
import { decryptSecretExisting, fingerprint, privateFingerprinterExisting, redact } from "./security.js"
const uuid = z.string().uuid()
const positive = z.number().int().positive().safe()
const object = z.record(z.string(),z.unknown())
const sealed = z.object({sealed:z.string().min(1).max(262144)}).strict()
const ids = z.array(positive).max(1000)
const retention = z.number().int().min(1).max(365)
const bool = z.boolean()
const interval = (min:number,max:number)=>z.number().int().min(min).max(max)
const backupServerIds=z.array(z.string().regex(/^[1-9]\d{0,15}$/).refine(value=>Number.isSafeInteger(Number(value))))
.max(1000).refine(values=>new Set(values).size===values.length)
const backupSchedule=backupScheduleSettingsDtoSchema.extend({serverIds:backupServerIds}).strict()
const backupBefore=z.object({value:backupSchedule,serverIds:ids}).strict()
// Drizzle's string-mode timestamptz keeps PostgreSQL's space and hour-only
// offset. Accept that exact wire form as well as an ISO timestamptz.
const timestamp = z.string().regex(/^\d{4}-\d\d-\d\d[ T]\d\d:\d\d:\d\d(?:\.\d{1,6})?(?:Z|[+-]\d\d(?::\d\d)?)$/)
.refine(value=>Number.isFinite(Date.parse(value.replace(" ","T").replace(/([+-]\d\d)$/,"$1:00"))))
const metadata = {
id:z.literal(1),createdAt:timestamp,updatedAt:timestamp,
lastCollectedAt:timestamp.nullable(),lastDurationMs:z.number().int().safe().nullable(),
lastError:z.string().max(16384).nullable(),
}
function telemetryFamily(version:number,domain:string,settings:z.ZodObject,hasEpoch:boolean,hasConfigurationProof:boolean){
const originalSettings=hasConfigurationProof?settings.extend({configurationProof:z.string().regex(/^[0-9a-f]{64}$/)}):settings
const effect=settings.extend({...metadata,...(hasEpoch?{pollConfigurationEpoch:uuid}:{})}).strict()
return {version,domain,patch:settings.partial().strict().refine(value=>Object.keys(value).length>0),
before:z.object({value:originalSettings.strict(),serverIds:ids}).strict(),effect}
}
const families={
"logs.settings.update":{version:1,domain:null,patch:logSettingsPatchSchema,before:logSettingsSchema.strict(),effect:logSettingsSchema.strict()},
"traffic.settings.update":telemetryFamily(2,"traffic-settings",z.object({enabled:bool,intervalSec:interval(5,3600),retentionDays:retention}),true,true),
"uptime.settings.update":telemetryFamily(2,"uptime-settings",z.object({enabled:bool,resourcesEnabled:bool,pingEnabled:bool,
speedEnabled:bool,intervalSec:interval(5,86400),probeIntervalSec:interval(5,86400),speedIntervalSec:interval(10,86400),retentionDays:retention}),true,true),
"servers.api-ping.settings.update":telemetryFamily(2,"servers-api-ping-settings",z.object({enabled:bool,intervalSec:interval(10,86400)}),false,true),
"internet-path.settings.update":telemetryFamily(1,"internet-path-settings",z.object({enabled:bool,intervalSec:interval(30,86400),retentionDays:retention}),true,false),
"geoip.settings.update":{
version:1,domain:"geoip-settings",
patch:geoipSettingsPatchSchema.strict().refine(value=>Object.keys(value).length>0),
before:z.object({value:z.object({enabled:bool,updateIntervalSec:positive}).strict(),serverIds:ids}).strict(),
effect:z.object({settings:geoipSettingsDtoSchema.strict(),verificationScope:z.literal("configuration-persistence")})
.strict().transform(result=>result.settings),
},
"backups.schedule.update":{
version:1,domain:"backup-schedule",
patch:putBackupScheduleSettingsSchema.extend({serverIds:backupServerIds.optional()}).strict().refine(value=>Object.keys(value).length>0),
before:backupBefore,effect:backupSchedule,
},
} as const
const planEvidence = z.object({
target:z.object({scope:z.literal("global")}).strict(),
objects:z.array(z.object({kind:z.string(),id:z.string(),serverId:positive.optional()}).strict()).max(1001),
steps:z.tuple([z.object({kind:z.literal("service"),service:z.string(),input:z.object({settings:object}).strict()}).strict()]),
snapshots:z.tuple([]),before:object,warnings:z.array(z.string()).max(8),
verificationScope:z.literal("configuration").optional(),
requiresHuman:z.literal(true).optional(),
}).strict()
class RetirementRefused extends Error {}
export class OrdinaryRetirementCommitUnknown extends Error {
readonly automaticReplay = false
constructor() { super("Ordinary operation retirement outcome unknown; automatic replay forbidden") }
}
const requireThat: (value: unknown) => asserts value = value => {
if (!value) throw new RetirementRefused()
}
// Fixed reviewed settings families without artifact, historic receipt or
// inverse-plan consumers. Other action families need separate review.
const ELIGIBLE = `FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=0
JOIN public.mcp_operation_checkpoints k ON k.operation_id=o.id AND k.step_index=0
WHERE (p.operation_id,p.operation_version) IN (
('logs.settings.update',1),('traffic.settings.update',2),('uptime.settings.update',2),
('servers.api-ping.settings.update',2),('internet-path.settings.update',1),('geoip.settings.update',1),('backups.schedule.update',1)
) AND p.state='completed'
AND o.state='applied' AND o.verification='passed' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND isfinite(p.expires_at) AND p.expires_at<transaction_timestamp()
AND s.state='verified' AND s.server_id IS NULL AND isfinite(s.finished_at)
AND k.after_sealed IS NOT NULL
AND (SELECT count(*) FROM public.mcp_operation_steps WHERE operation_id=o.id)=1
AND (SELECT count(*) FROM public.mcp_operation_checkpoints WHERE operation_id=o.id)=1
AND NOT EXISTS(SELECT 1 FROM public.mcp_drift WHERE operation_id=o.id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_recovery_intents WHERE operation_id=o.id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_scoped_recovery_intents WHERE source_operation_id=o.id OR recovery_operation_id=o.id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_routing_reservations WHERE source_operation_id=o.id OR source_plan_id=p.id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_routing_forward_charges WHERE source_operation_id=o.id OR source_plan_id=p.id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_routing_manual_inverse_charges WHERE source_operation_id=o.id OR recovery_operation_id=o.id OR source_plan_id=p.id OR recovery_plan_id=p.id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_diagnostic_cancellations WHERE operation_id=o.id)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_storage_admissions WHERE identity_kind='mcp' AND execution_id=o.id)
AND NOT EXISTS(SELECT 1 FROM public.server_poll_storage_admissions WHERE identity_kind='mcp' AND execution_id=o.id)
AND NOT EXISTS(SELECT 1 FROM public.vrf_production_tickets WHERE plan_id=p.id)`
const CANDIDATES = `SELECT o.id,o.connection_id,o.finished_at::text AS finished_at ${ELIGIBLE}
AND NOT EXISTS(SELECT 1 FROM public.mcp_ordinary_retirement_claims WHERE operation_id=o.id)
AND ($1::timestamptz IS NULL OR (o.finished_at,o.id)>($1::timestamptz,$2::uuid))
ORDER BY o.finished_at,o.id LIMIT 8`
const SOURCE = `SELECT to_jsonb(o) AS operation,to_jsonb(p) AS plan,to_jsonb(s) AS step,to_jsonb(k) AS checkpoint ${ELIGIBLE}
AND o.id=$1 AND o.connection_id=$2
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> {
const sql = await connectProbeStorage({deadlineAt})
let committing=false,committed=false
try {
await sql.query("BEGIN")
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
await sql.query("COMMIT")
committed=true
return result
} catch(error) {
if(committing)throw new OrdinaryRetirementCommitUnknown()
try{await sql.query("ROLLBACK")}catch{/* Destroy this checkout. */}
throw error
} finally {
sql.release(!committed)
}
}
async function prepare(sql:ProbeStorageSql, operationId:string, connectionId:string) {
requireThat((await sql.query("SELECT id FROM public.mcp_connections WHERE id=$1 FOR UPDATE NOWAIT",[connectionId])).rows.length===1)
requireThat((await sql.query("SELECT pg_try_advisory_xact_lock(hashtextextended($1,0)) AS locked",["mcp-operation:"+operationId])).rows[0]?.locked===true)
const rows=(await sql.query(SOURCE,[operationId,connectionId])).rows
requireThat(rows.length===1)
const raw=rows[0], operation=object.parse(raw.operation), plan=object.parse(raw.plan)
const step=object.parse(raw.step),checkpoint=object.parse(raw.checkpoint)
const open=async(value:unknown)=>JSON.parse(await decryptSecretExisting(sealed.parse(value).sealed))
const original=planEvidence.parse(await open(plan.evidence))
const action=z.enum(Object.keys(families) as [keyof typeof families,...(keyof typeof families)[]]).parse(plan.operation_id)
const family=families[action],service=original.steps[0]
requireThat(plan.operation_version===family.version&&service.service===action)
requireThat(fingerprint(await open(plan.payload))===fingerprint(service.input))
requireThat(fingerprint(step.intended_state)===fingerprint(redact(service)))
const patch=object.parse(family.patch.parse(service.input.settings))
const effect=object.parse(family.effect.parse(step.result))
requireThat(Object.entries(patch).every(([key,value])=>fingerprint(effect[key])===fingerprint(value)))
requireThat(fingerprint(operation.result)===fingerprint({confirmedWrites:1,verificationScope:"configuration"}))
for(const value of [checkpoint.before_sealed,checkpoint.after_sealed]){
requireThat(typeof value==="string"&&value.length<=1024&&fingerprint(await open({sealed:value}))===fingerprint([]))
}
const serverObjects=original.objects.filter(item=>item.kind==="server")
const serverIds=ids.parse(serverObjects.map(item=>item.serverId))
requireThat(new Set(serverIds).size===serverIds.length&&serverObjects.every(item=>item.id===String(item.serverId)))
const domainObjects=original.objects.filter(item=>item.kind!=="server")
const before=family.before.parse(original.before)
if(family.domain===null){
requireThat(domainObjects.length===0&&original.requiresHuman===undefined)
}else{
requireThat(original.requiresHuman===true&&original.verificationScope==="configuration"
&&domainObjects.length===1&&domainObjects[0].kind===family.domain&&domainObjects[0].id==="1"&&domainObjects[0].serverId===undefined)
requireThat("serverIds" in before&&fingerprint(before.serverIds)===fingerprint(serverIds))
}
if(action==="backups.schedule.update"){
const scope=new Set(serverIds.map(String))
requireThat([...backupBefore.parse(before).value.serverIds,...backupSchedule.parse(effect).serverIds].every(id=>scope.has(id)))
}
const signer=await privateFingerprinterExisting("mcp-ordinary-retirement-source-v1")
return {planId:uuid.parse(plan.id),action,serverIds,key:z.string().min(1).max(200).parse(operation.idempotency_key),proof:signer(fingerprint(raw))}
}
async function retire(operationId:string,connectionId:string,deadlineAt:number) {
const token=randomUUID()
await transaction(deadlineAt,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=>{
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)
const source=await prepare(sql,operationId,connectionId)
requireThat(claim[0].source_proof===source.proof&&claim[0].plan_id===source.planId)
const tombstone=await sql.query(`INSERT INTO public.mcp_operation_replay_tombstones
(operation_id,plan_id,connection_id,idempotency_digest,action_id,target_server_ids,target_global)
VALUES($1,$2,$3,$4,$5,$6,true) RETURNING operation_id`,
[operationId,source.planId,connectionId,operationIdempotencyDigest(connectionId,source.key),source.action,source.serverIds])
requireThat(tombstone.rows.length===1&&tombstone.rows[0].operation_id===operationId)
const checkpoint=await sql.query("DELETE FROM public.mcp_operation_checkpoints WHERE operation_id=$1 AND step_index=0 RETURNING operation_id",[operationId])
const step=await sql.query("DELETE FROM public.mcp_operation_steps WHERE operation_id=$1 AND step_index=0 RETURNING operation_id",[operationId])
const operation=await sql.query("DELETE FROM public.mcp_operations WHERE id=$1 AND connection_id=$2 AND plan_id=$3 RETURNING id AS operation_id",[operationId,connectionId,source.planId])
const plan=await sql.query("DELETE FROM public.mcp_plans WHERE id=$1 AND connection_id=$2 RETURNING id AS plan_id",[source.planId,connectionId])
requireThat([checkpoint,step,operation].every(result=>result.rows.length===1&&result.rows[0].operation_id===operationId)
&&plan.rows.length===1&&plan.rows[0].plan_id===source.planId)
})
}
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)
requireThat(rows.length<=8)
let scanned=0,retired=0
for(const row of rows){
if(Date.now()>=deadlineAt-2000||retired>=2)break
const id=uuid.parse(row.id),connectionId=uuid.parse(row.connection_id)
requireThat(typeof row.finished_at==="string"&&Number.isFinite(Date.parse(row.finished_at)))
cursor={finishedAt:row.finished_at,operationId:id}
scanned++
try{await retire(id,connectionId,deadlineAt);retired++}
catch(error){if(error instanceof OrdinaryRetirementCommitUnknown)throw error}
}
if(scanned===rows.length&&rows.length<8)cursor=undefined
return {scanned,retired,automaticReplay:false as const}
})
}
@@ -2,7 +2,7 @@ import assert from "node:assert/strict"
import { createOspfOwnerSourceGuard, verifyOspfOwnerGraph, verifyOspfOwnerGraphAtPhase } from "./ospf-owner-graph.js" import { createOspfOwnerSourceGuard, verifyOspfOwnerGraph, verifyOspfOwnerGraphAtPhase } from "./ospf-owner-graph.js"
import { fingerprint, McpError, redact } from "./security.js" import { fingerprint, McpError, redact } from "./security.js"
import { createRosPrefix } from "./ros-prefix.js" import { createRosPrefix } from "./ros-prefix.js"
import { ROS_SNAPSHOT_VERSION, type RosSnapshot } from "./resources.js" import { rosSnapshotVersion, type RosSnapshot } from "./resources.js"
const ids = [1, 2, 3] const ids = [1, 2, 3]
const graph = { family: "ipv4" as const, sourceSecretProofs: ids.map(serverId => ({ serverId, proof: fingerprint({ serverId }) })), instances: ids.map(serverId => ({ serverId, fingerprint: fingerprint({ serverId, native: "instances" }) })) } const graph = { family: "ipv4" as const, sourceSecretProofs: ids.map(serverId => ({ serverId, proof: fingerprint({ serverId }) })), instances: ids.map(serverId => ({ serverId, fingerprint: fingerprint({ serverId, native: "instances" }) })) }
@@ -54,7 +54,7 @@ const catalogAuthorize = async () => {
await sourceGuard() await sourceGuard()
} }
const catalogRows = [{ ".id": "*A", name: "keep", disabled: "false" }] const catalogRows = [{ ".id": "*A", name: "keep", disabled: "false" }]
const catalog: RosSnapshot = { serverId: 1, resource: "bridge", rows: catalogRows, fingerprint: fingerprint(catalogRows), projectionVersion: ROS_SNAPSHOT_VERSION, sourceProof: "a".repeat(64) } const catalog: RosSnapshot = { serverId: 1, resource: "bridge", rows: catalogRows, fingerprint: fingerprint(catalogRows), projectionVersion: rosSnapshotVersion("bridge"), sourceProof: "a".repeat(64) }
reads = [] reads = []
const catalogPrefix = createRosPrefix([catalog], async (_id, _resource, beforeRead = async () => {}) => { const catalogPrefix = createRosPrefix([catalog], async (_id, _resource, beforeRead = async () => {}) => {
// Match readRosSnapshot's normal three grant/source fences without polling a // Match readRosSnapshot's normal three grant/source fences without polling a
@@ -3,6 +3,7 @@ import { z } from "zod"
export const pathProofPurposeSchema = z.enum(["community", "control-path"]) export const pathProofPurposeSchema = z.enum(["community", "control-path"])
export type PathProofPurpose = z.infer<typeof pathProofPurposeSchema> export type PathProofPurpose = z.infer<typeof pathProofPurposeSchema>
export const PATH_PROOF_VERSION = 2 export const PATH_PROOF_VERSION = 2
export const COMMUNITY_PATH_MAX_AGE_MS = 90000
export const pathProofTag = (purpose: PathProofPurpose) => "path-check:" + purpose + ":v" + PATH_PROOF_VERSION + ":" export const pathProofTag = (purpose: PathProofPurpose) => "path-check:" + purpose + ":v" + PATH_PROOF_VERSION + ":"
export function isCommunityPathEvidence(value: Record<string, unknown>) { export function isCommunityPathEvidence(value: Record<string, unknown>) {
return value.proofPurpose === "community" && value.proofVersion === PATH_PROOF_VERSION return value.proofPurpose === "community" && value.proofVersion === PATH_PROOF_VERSION
+3 -3
View File
@@ -8,7 +8,7 @@ import { fingerprint, McpError } from "./security.js"
import { readRouteCommunityContext, routeContextAccess, type RouteContextOptions } from "./routing-community-context.js" import { readRouteCommunityContext, routeContextAccess, type RouteContextOptions } from "./routing-community-context.js"
import type { add } from "./actions.js" import type { add } from "./actions.js"
import { readControlPathContext } from "./control-path-context.js" import { readControlPathContext } from "./control-path-context.js"
import { isCommunityPathEvidence, PATH_PROOF_VERSION, pathProofPurposeSchema, type PathProofPurpose } from "./path-proof-purpose.js" import { COMMUNITY_PATH_MAX_AGE_MS, isCommunityPathEvidence, PATH_PROOF_VERSION, pathProofPurposeSchema, type PathProofPurpose } from "./path-proof-purpose.js"
export function inspectControlPing(rows: Parameters<typeof parsePing>[0]) { export function inspectControlPing(rows: Parameters<typeof parsePing>[0]) {
const parsed = parsePing(rows) const parsed = parsePing(rows)
@@ -107,10 +107,10 @@ export function registerPathActions(register: typeof add) {
export async function requireRecentPath(profile: RouteAiProfile, candidateId: string, connectionId: string, options: RouteContextOptions = {}) { export async function requireRecentPath(profile: RouteAiProfile, candidateId: string, connectionId: string, options: RouteContextOptions = {}) {
// Compare the database timestamp with the database clock. The signed // Compare the database timestamp with the database clock. The signed
// observedAt below is independently checked against the application clock. // observedAt below is independently checked against the application clock.
const { rows: [row] } = await pool.query("SELECT id,status,created_at,evidence,created_at BETWEEN clock_timestamp()-interval '90 seconds' AND clock_timestamp() AS database_fresh FROM mcp_path_checks WHERE connection_id=$1 AND profile_hash=$2 AND candidate_id=$3 AND evidence->>'proofPurpose'='community' AND evidence->>'proofVersion'='2' ORDER BY created_at DESC,id DESC LIMIT 1", [connectionId, fingerprint(profile), candidateId]) const { rows: [row] } = await pool.query("SELECT id,status,created_at,evidence,created_at BETWEEN clock_timestamp()-$4::int * interval '1 millisecond' AND clock_timestamp() AS database_fresh FROM mcp_path_checks WHERE connection_id=$1 AND profile_hash=$2 AND candidate_id=$3 AND evidence->>'proofPurpose'='community' AND evidence->>'proofVersion'='2' ORDER BY created_at DESC,id DESC LIMIT 1", [connectionId, fingerprint(profile), candidateId, COMMUNITY_PATH_MAX_AGE_MS])
if (!row || row.status !== "passed" || row.database_fresh !== true) throw new McpError("MEASUREMENT_UNKNOWN", "Нет свежего подтверждения контрольного пути", 409) if (!row || row.status !== "passed" || row.database_fresh !== true) throw new McpError("MEASUREMENT_UNKNOWN", "Нет свежего подтверждения контрольного пути", 409)
const { proof, ...evidence } = row.evidence ?? {} const { proof, ...evidence } = row.evidence ?? {}
if (!isCommunityPathEvidence(evidence) || evidence.id !== row.id || evidence.connectionId !== connectionId || evidence.profileHash !== fingerprint(profile) || evidence.candidateId !== candidateId || evidence.status !== "passed" || !Array.isArray(evidence.sourceProofs) || !evidence.sourceProofs.length || !Number.isFinite(Date.parse(evidence.observedAt)) || Date.now() - Date.parse(evidence.observedAt) > 90000 || Date.parse(evidence.observedAt) > Date.now()) throw new McpError("MEASUREMENT_UNKNOWN", "Подтверждение пути не связано с Community purpose, источником и контекстом", 409) if (!isCommunityPathEvidence(evidence) || evidence.id !== row.id || evidence.connectionId !== connectionId || evidence.profileHash !== fingerprint(profile) || evidence.candidateId !== candidateId || evidence.status !== "passed" || !Array.isArray(evidence.sourceProofs) || !evidence.sourceProofs.length || !Number.isFinite(Date.parse(evidence.observedAt)) || Date.now() - Date.parse(evidence.observedAt) > COMMUNITY_PATH_MAX_AGE_MS || Date.parse(evidence.observedAt) > Date.now()) throw new McpError("MEASUREMENT_UNKNOWN", "Подтверждение пути не связано с Community purpose, источником и контекстом", 409)
const current = await readRouteCommunityContext(profile, { ...options, selectedCandidateId: candidateId, expectedSources: evidence.sourceProofs }) const current = await readRouteCommunityContext(profile, { ...options, selectedCandidateId: candidateId, expectedSources: evidence.sourceProofs })
if (!current.verifyEvidence(evidence, proof)) throw new McpError("MEASUREMENT_UNKNOWN", "Зависимости пути или сохранённое подтверждение изменились", 409) if (!current.verifyEvidence(evidence, proof)) throw new McpError("MEASUREMENT_UNKNOWN", "Зависимости пути или сохранённое подтверждение изменились", 409)
return row.id as string return row.id as string
@@ -5,6 +5,14 @@ import { preparedPlanExpiry } from "./plan-expiry.js"
const iso = (milliseconds: number) => new Date(Date.now() + milliseconds).toISOString() const iso = (milliseconds: number) => new Date(Date.now() + milliseconds).toISOString()
const poll = (validUntil?: string) => preparedPlanExpiry("servers.poll", "servers", { validUntil }) const poll = (validUntil?: string) => preparedPlanExpiry("servers.poll", "servers", { validUntil })
test("Community honors sealed validity but keeps the ordinary lifetime without a bound", () => {
const bound = iso(90000)
assert.equal(preparedPlanExpiry("routing.community.switch", "network", { validUntil: bound })?.toISOString(), bound)
assert.equal(preparedPlanExpiry("routing.community.switch", "network", { validUntil: iso(-1000) }), null)
const before = Date.now(), manual = preparedPlanExpiry("routing.community.switch", "network", {})!.getTime()
assert(manual >= before + 300000 && manual <= Date.now() + 300000)
})
test("Poll uses its existing sealed five-minute validity as the nominal window", () => { test("Poll uses its existing sealed five-minute validity as the nominal window", () => {
const bound = iso(300000) const bound = iso(300000)
assert.equal(poll(bound)?.toISOString(), bound) assert.equal(poll(bound)?.toISOString(), bound)
+5 -3
View File
@@ -3,11 +3,13 @@ import type { ChangeEvidence } from "./action-types.js"
/** Internal value calculation only; evidence comes from action.prepare, never /** Internal value calculation only; evidence comes from action.prepare, never
* caller JSON. Generic plans may outlive their preparation access credential. * caller JSON. Generic plans may outlive their preparation access credential.
* A missing result makes the runtime refuse Poll before storing a new plan. */ * A missing result refuses a time-bound plan before storage. */
export function preparedPlanExpiry(operationId: string, section: string, evidence: Pick<ChangeEvidence, "validUntil">): Date | null { export function preparedPlanExpiry(operationId: string, section: string, evidence: Pick<ChangeEvidence, "validUntil">): Date | null {
const now = Date.now(), ordinaryExpiry = now + (section === "network" ? 300000 : 900000) const now = Date.now(), ordinaryExpiry = now + (section === "network" ? 300000 : 900000)
if (operationId !== "servers.poll") return new Date(ordinaryExpiry) if (operationId !== "servers.poll" && operationId !== "routing.community.switch") return new Date(ordinaryExpiry)
const validity = z.string().datetime({ offset: true }).safeParse(evidence.validUntil) const validUntil = evidence.validUntil
if (operationId === "routing.community.switch" && validUntil === undefined) return new Date(ordinaryExpiry)
const validity = z.string().datetime({ offset: true }).safeParse(validUntil)
const bound = validity.success ? Date.parse(validity.data) : NaN const bound = validity.success ? Date.parse(validity.data) : NaN
if (!Number.isFinite(bound) || bound <= now) return null if (!Number.isFinite(bound) || bound <= now) return null
return new Date(Math.min(ordinaryExpiry, bound)) return new Date(Math.min(ordinaryExpiry, bound))
+47 -1
View File
@@ -21,7 +21,7 @@ test("failed/malformed/redirected and stalled protocol probes are bounded and ne
try { try {
for (const state of ["malformed", "redirect", "large", "announced", "stalled"]) { for (const state of ["malformed", "redirect", "large", "announced", "stalled"]) {
mode = state mode = state
const started = performance.now(), result = await runMcpProtocolProbe(endpoint, "private-readiness-fixture", 300) const started = performance.now(), result = await runMcpProtocolProbe(endpoint, "private-readiness-fixture", { timeoutMs: 300 })
assert.equal(result.status, "failed", state) assert.equal(result.status, "failed", state)
assert.ok(performance.now() - started < 3000, state) assert.ok(performance.now() - started < 3000, state)
assert.ok(!JSON.stringify(result).includes("private-readiness-fixture")) assert.ok(!JSON.stringify(result).includes("private-readiness-fixture"))
@@ -29,3 +29,49 @@ test("failed/malformed/redirected and stalled protocol probes are bounded and ne
assert.equal(forwarded, 0); assert.ok(requests >= 5) assert.equal(forwarded, 0); assert.ok(requests >= 5)
} finally { server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve())) } } finally { server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve())) }
}) })
test("capabilities readiness refuses widened scope and checks the tool result", async () => {
const capability = { id: "capabilities.read", toolName: "mm_capabilities_get" }
const tool = { name: capability.toolName, inputSchema: { type: "object" } }
let mode = "extra-tool", calls = 0
const server = createServer(async (req, res) => {
if (req.method !== "POST") { res.writeHead(405).end(); return }
const chunks: Buffer[] = []
for await (const chunk of req) chunks.push(chunk)
const { id, method, params } = JSON.parse(Buffer.concat(chunks).toString())
if (id === undefined) { res.writeHead(202).end(); return }
let result: unknown
if (method === "initialize") result = { protocolVersion: params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "fixture", version: "1.0.0" } }
if (method === "tools/list") result = { tools: mode === "extra-tool" ? [tool, { ...tool, name: "mm_servers_list" }] : [tool] }
if (method === "tools/call") {
calls++
assert.equal(params.name, capability.toolName)
if (mode === "timeout") { res.writeHead(200, { "content-type": "application/json" }).write("{"); return }
const operations = mode === "extra-operation" ? [capability, { id: "servers.list" }] : [capability]
result = { content: [], isError: mode === "tool-error", structuredContent: { result: { schemaVersion: 1, operations } } }
}
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ jsonrpc: "2.0", id, result }))
})
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve))
const endpoint = new URL(`http://127.0.0.1:${(server.address() as { port: number }).port}/mcp`)
const probe = () => runMcpProtocolProbe(endpoint, "private-readiness-fixture", { timeoutMs: 400, readCapabilities: true })
try {
const widened = await probe()
assert.equal(widened.reason, "SCOPE_NOT_ENFORCED")
assert.equal(widened.capabilitiesRead, "skipped")
assert.equal(calls, 0)
for (const state of ["tool-error", "extra-operation", "timeout"]) {
mode = state
const result = await probe()
assert.equal(result.status, "failed", state)
assert.equal(result.capabilitiesRead, "failed", state)
assert.equal(result.reason, state === "timeout" ? "TIMEOUT" : "CAPABILITIES_READ_FAILED", state)
}
mode = "valid"
const valid = await probe()
assert.equal(valid.status, "passed")
assert.equal(valid.capabilitiesRead, "passed")
assert.equal(valid.clientCredentialsVerified, false)
assert.equal(calls, 4)
} finally { server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve())) }
})
+28 -12
View File
@@ -9,15 +9,19 @@ export interface ProtocolProbe {
status: "passed" | "failed" | "skipped"; status: "passed" | "failed" | "skipped";
elapsedMs: number; elapsedMs: number;
reason?: string; reason?: string;
credentialType: "temporary-no-operations"; credentialType: "temporary-no-operations" | "temporary-capabilities-read";
capabilitiesRead: "passed" | "failed" | "skipped";
clientCredentialsVerified: false; clientCredentialsVerified: false;
} }
/** A temporary access credential may negotiate metadata, but its empty grant const capabilitiesOperation = "capabilities.read"
* permits no tools. The ordinary connection credentials are never recovered. */ const capabilitiesTool = "mm_capabilities_get"
export async function withMcpProbeCredential<T>(connectionId: string, actor: AuthUser, work: (token: string) => Promise<T>): Promise<T> {
/** The probe may read its own narrowed capabilities when the connection grants
* that operation. The ordinary connection credentials are never recovered. */
export async function withMcpProbeCredential<T>(connectionId: string, actor: AuthUser, work: (token: string, readCapabilities: boolean) => Promise<T>): Promise<T> {
const credentialId = randomUUID(), grantId = randomUUID(), token = randomToken("access") const credentialId = randomUUID(), grantId = randomUUID(), token = randomToken("access")
await transaction(async client => { const readCapabilities = await transaction(async client => {
await client.query("SELECT id FROM mcp_connections WHERE id=$1 FOR UPDATE", [connectionId]) await client.query("SELECT id FROM mcp_connections WHERE id=$1 FOR UPDATE", [connectionId])
const connection = await getConnection(connectionId, client) const connection = await getConnection(connectionId, client)
assertConnectionVisible(connection, actor) assertConnectionVisible(connection, actor)
@@ -27,14 +31,16 @@ export async function withMcpProbeCredential<T>(connectionId: string, actor: Aut
const expired = (await client.query("DELETE FROM mcp_credentials WHERE connection_id=$1 AND client_id='mcp-readiness-probe' AND expires_at<=now() RETURNING id", [connectionId])).rows.map(row => row.id) const expired = (await client.query("DELETE FROM mcp_credentials WHERE connection_id=$1 AND client_id='mcp-readiness-probe' AND expires_at<=now() RETURNING id", [connectionId])).rows.map(row => row.id)
if (expired.length) await client.query("DELETE FROM mcp_grants WHERE connection_id=$1 AND payload->>'credentialId'=ANY($2::text[])", [connectionId, expired]) if (expired.length) await client.query("DELETE FROM mcp_grants WHERE connection_id=$1 AND payload->>'credentialId'=ANY($2::text[])", [connectionId, expired])
const expiresAt = new Date(Math.min(Date.now() + 20000, Date.parse(connection.policy.expiresAt), connection.owner_valid_until.getTime())).toISOString() const expiresAt = new Date(Math.min(Date.now() + 20000, Date.parse(connection.policy.expiresAt), connection.owner_valid_until.getTime())).toISOString()
const grant = mcpTemporaryGrantSchema.parse({ id: grantId, startsAt: new Date(Date.now() - 1000).toISOString(), expiresAt, scope: { serverIds: [], operationIds: [], allowGlobal: false } }) const allowed = connection.policy.scope.operationIds.includes(capabilitiesOperation)
const grant = mcpTemporaryGrantSchema.parse({ id: grantId, startsAt: new Date(Date.now() - 1000).toISOString(), expiresAt, scope: { serverIds: [], operationIds: allowed ? [capabilitiesOperation] : [], allowGlobal: false } })
await client.query("INSERT INTO mcp_credentials(id,connection_id,digest,kind,expires_at,audience,client_id) VALUES($1,$2,$3,'access',$4,$5,'mcp-readiness-probe')", [credentialId, connectionId, tokenDigest(token), expiresAt, canonicalMcpUrl().href]) await client.query("INSERT INTO mcp_credentials(id,connection_id,digest,kind,expires_at,audience,client_id) VALUES($1,$2,$3,'access',$4,$5,'mcp-readiness-probe')", [credentialId, connectionId, tokenDigest(token), expiresAt, canonicalMcpUrl().href])
await client.query("INSERT INTO mcp_grants(id,connection_id,payload) VALUES($1,$2,$3::jsonb)", [grantId, connectionId, JSON.stringify({ ...grant, credentialId })]) await client.query("INSERT INTO mcp_grants(id,connection_id,payload) VALUES($1,$2,$3::jsonb)", [grantId, connectionId, JSON.stringify({ ...grant, credentialId })])
await validatePrincipal({ connectionId, credentialId, credentialExpiresAt: expiresAt }, client) await validatePrincipal({ connectionId, credentialId, credentialExpiresAt: expiresAt }, client)
await audit(client, actor.id, "connection.handshake.start", connectionId, { credentialId, expiresAt }) await audit(client, actor.id, "connection.handshake.start", connectionId, { credentialId, expiresAt })
return allowed
}) })
let completed = false let completed = false
try { const result = await work(token); completed = true; return result } try { const result = await work(token, readCapabilities); completed = true; return result }
finally { finally {
await transaction(async client => { await transaction(async client => {
await client.query("DELETE FROM mcp_grants WHERE id=$1 AND connection_id=$2", [grantId, connectionId]) await client.query("DELETE FROM mcp_grants WHERE id=$1 AND connection_id=$2", [grantId, connectionId])
@@ -45,11 +51,12 @@ export async function withMcpProbeCredential<T>(connectionId: string, actor: Aut
} }
/** One deadline covers negotiation, metadata and HTTP response bodies. */ /** One deadline covers negotiation, metadata and HTTP response bodies. */
export async function runMcpProtocolProbe(endpoint: URL, token: string, timeoutMs = 8000): Promise<ProtocolProbe> { export async function runMcpProtocolProbe(endpoint: URL, token: string, { timeoutMs = 8000, readCapabilities = false } = {}): Promise<ProtocolProbe> {
const started = performance.now(), controller = new AbortController() const started = performance.now(), controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs) const timer = setTimeout(() => controller.abort(), timeoutMs)
const client = new Client({ name: "MikrotikManager readiness", version: "1.0.0" }) const client = new Client({ name: "MikrotikManager readiness", version: "1.0.0" })
const result = (status: ProtocolProbe["status"], reason?: string): ProtocolProbe => ({ status, reason, elapsedMs: Math.round(performance.now() - started), credentialType: "temporary-no-operations", clientCredentialsVerified: false }) let capabilitiesRead: ProtocolProbe["capabilitiesRead"] = "skipped"
const result = (status: ProtocolProbe["status"], reason?: string): ProtocolProbe => ({ status, reason, elapsedMs: Math.round(performance.now() - started), credentialType: readCapabilities ? "temporary-capabilities-read" : "temporary-no-operations", capabilitiesRead, clientCredentialsVerified: false })
try { try {
const transport = new StreamableHTTPClientTransport(endpoint, { requestInit: { headers: { Authorization: `Bearer ${token}` }, redirect: "error" }, redirectPolicy: "follow", const transport = new StreamableHTTPClientTransport(endpoint, { requestInit: { headers: { Authorization: `Bearer ${token}` }, redirect: "error" }, redirectPolicy: "follow",
fetch: async (url, init) => { fetch: async (url, init) => {
@@ -67,13 +74,22 @@ export async function runMcpProtocolProbe(endpoint: URL, token: string, timeoutM
} }) } })
await client.connect(transport, { timeout: timeoutMs }) await client.connect(transport, { timeout: timeoutMs })
const tools = await client.listTools({}, { timeout: timeoutMs }) const tools = await client.listTools({}, { timeout: timeoutMs })
if (tools.tools.length !== 0 || tools.nextCursor) return result("failed", "SCOPE_NOT_ENFORCED") const expectedTools = readCapabilities ? [capabilitiesTool] : []
if (tools.nextCursor || tools.tools.length !== expectedTools.length || tools.tools.some((tool, index) => tool.name !== expectedTools[index])) return result("failed", "SCOPE_NOT_ENFORCED")
if (readCapabilities) {
capabilitiesRead = "failed"
const call = await client.callTool({ name: capabilitiesTool, arguments: {} }, { timeout: timeoutMs })
const output = (call.structuredContent as { result?: { schemaVersion?: unknown; operations?: unknown } } | undefined)?.result
const operations = output?.operations
if (call.isError || output?.schemaVersion !== 1 || !Array.isArray(operations) || operations.length !== 1 || operations[0]?.id !== capabilitiesOperation || operations[0]?.toolName !== capabilitiesTool) return result("failed", "CAPABILITIES_READ_FAILED")
capabilitiesRead = "passed"
}
return result("passed") return result("passed")
} catch { return result("failed", controller.signal.aborted ? "TIMEOUT" : "TRANSPORT_FAILED") } } catch { return result("failed", controller.signal.aborted ? "TIMEOUT" : capabilitiesRead === "failed" ? "CAPABILITIES_READ_FAILED" : "TRANSPORT_FAILED") }
finally { clearTimeout(timer); controller.abort(); await client.close().catch(() => {}) } finally { clearTimeout(timer); controller.abort(); await client.close().catch(() => {}) }
} }
export async function checkMcpProtocol(connectionId: string, actor: AuthUser) { export async function checkMcpProtocol(connectionId: string, actor: AuthUser) {
const endpoint = canonicalMcpUrl() const endpoint = canonicalMcpUrl()
return withMcpProbeCredential(connectionId, actor, token => runMcpProtocolProbe(endpoint, token)) return withMcpProbeCredential(connectionId, actor, (token, readCapabilities) => runMcpProtocolProbe(endpoint, token, { readCapabilities }))
} }
+6 -4
View File
@@ -6,13 +6,14 @@ import { decryptSecret, fingerprint, McpError } from "./security.js"
import { effectiveMcpConnection } from "./store.js" import { effectiveMcpConnection } from "./store.js"
import { findMcpService } from "./service-registry.js" import { findMcpService } from "./service-registry.js"
import { findMcpOperation } from "./registry.js" import { findMcpOperation } from "./registry.js"
import { readRosSnapshot, resourceById, routerClient, validateRosStep, splitOspfPassiveUpdate, ROS_SNAPSHOT_VERSION, type RosSnapshot, type RosStep } from "./resources.js" import { readRosSnapshot, resourceById, routerClient, validateRosStep, splitOspfPassiveUpdate, rosSnapshotVersion, type RosSnapshot, type RosStep } from "./resources.js"
import { verifyRosStepSnapshots } from "./ros-step-verification.js" import { verifyRosStepSnapshots } from "./ros-step-verification.js"
import type { add } from "./actions.js" import type { add } from "./actions.js"
import type { ActionContext, ChangeEvidence, ChangeStep } from "./action-types.js" import type { ActionContext, ChangeEvidence, ChangeStep } from "./action-types.js"
import { loadScopedRecoverySource } from "./routing-community-scoped-recovery-source.js" import { loadScopedRecoverySource } from "./routing-community-scoped-recovery-source.js"
import { prepareScopedRecovery, registerScopedRecoveryService } from "./routing-community-scoped-recovery.js" import { prepareScopedRecovery, registerScopedRecoveryService } from "./routing-community-scoped-recovery.js"
import { reconcileVrfProductionStep } from "../../services/vrf-production-execution.js" import { reconcileVrfProductionStep } from "../../services/vrf-production-execution.js"
import { needsBridgeVlanRecovery, prepareBridgeVlanRecovery } from "./bridge-vlan-recovery.js"
async function source(operationId: string, context: ActionContext, allowServices = false) { 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]) 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])
@@ -86,7 +87,7 @@ export function registerRecoveryActions(register: typeof add) {
}, },
}) })
register("operations.recover", "mm_operation_recovery_plan", "Подготовить отдельный обратный план сетевой операции", "network", "mm:network:write", "write", "servers", z.object({ operationId: z.string().uuid() }).strict(), { register("operations.recover", "mm_operation_recovery_plan", "Подготовить отдельный обратный план сетевой операции", "network", "mm:network:write", "write", "servers", z.object({ operationId: z.string().uuid() }).strict(), {
version: 7, version: 8,
target: async (input, context) => { target: async (input, context) => {
const { evidence } = await source(String(input.operationId), context, true) const { evidence } = await source(String(input.operationId), context, true)
if (evidence.steps.some(step => step.kind === "ros" && step.step.resource === "vrf")) throw new McpError("UNSUPPORTED_CAPABILITY", "VRF не имеет проверенной обратной ревизии; используйте read-only сверку и отдельный approved план", 409) if (evidence.steps.some(step => step.kind === "ros" && step.step.resource === "vrf")) throw new McpError("UNSUPPORTED_CAPABILITY", "VRF не имеет проверенной обратной ревизии; используйте read-only сверку и отдельный approved план", 409)
@@ -106,6 +107,7 @@ export function registerRecoveryActions(register: typeof add) {
const checkpoints = (await pool.query("SELECT step_index,before_sealed,after_sealed FROM mcp_operation_checkpoints WHERE operation_id=$1 ORDER BY step_index DESC", [row.id])).rows const checkpoints = (await pool.query("SELECT step_index,before_sealed,after_sealed FROM mcp_operation_checkpoints WHERE operation_id=$1 ORDER BY step_index DESC", [row.id])).rows
const intended = (await pool.query("SELECT step_index,state,result FROM mcp_operation_steps WHERE operation_id=$1 ORDER BY step_index DESC", [row.id])).rows const intended = (await pool.query("SELECT step_index,state,result FROM mcp_operation_steps WHERE operation_id=$1 ORDER BY step_index DESC", [row.id])).rows
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 (!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 snapshots = new Map<string, RosSnapshot>(), steps: ChangeStep[] = [], objects: ChangeEvidence["objects"] = [...evidence.objects]
for (const checkpoint of checkpoints) { for (const checkpoint of checkpoints) {
const original = evidence.steps[checkpoint.step_index] const original = evidence.steps[checkpoint.step_index]
@@ -113,7 +115,7 @@ export function registerRecoveryActions(register: typeof add) {
const step = original.step, resource = resourceById(step.resource) const step = original.step, resource = resourceById(step.resource)
if (!context.connection.policy.scope.serverIds.includes(step.serverId)) throw new McpError("SERVER_DENIED", "Зависимость вне допуска") if (!context.connection.policy.scope.serverIds.includes(step.serverId)) throw new McpError("SERVER_DENIED", "Зависимость вне допуска")
const before = JSON.parse(await decryptSecret(checkpoint.before_sealed)) as RosSnapshot, after = JSON.parse(await decryptSecret(checkpoint.after_sealed)) as RosSnapshot const before = JSON.parse(await decryptSecret(checkpoint.before_sealed)) as RosSnapshot, after = JSON.parse(await decryptSecret(checkpoint.after_sealed)) as RosSnapshot
if (before.projectionVersion !== ROS_SNAPSHOT_VERSION || after.projectionVersion !== ROS_SNAPSHOT_VERSION || !before.sourceProof || before.sourceProof !== after.sourceProof || before.fingerprint !== fingerprint(before.rows) || after.fingerprint !== fingerprint(after.rows) || !verifyRosStepSnapshots(step, intended.find(item => item.step_index === checkpoint.step_index)?.result, before, after)) throw new McpError("STATE_CONFLICT", "Исходный переход каталога или источник не подтверждён; обратный план запрещён", 409) if (before.projectionVersion !== rosSnapshotVersion(step.resource) || after.projectionVersion !== rosSnapshotVersion(step.resource) || !before.sourceProof || before.sourceProof !== after.sourceProof || before.fingerprint !== fingerprint(before.rows) || after.fingerprint !== fingerprint(after.rows) || !verifyRosStepSnapshots(step, intended.find(item => item.step_index === checkpoint.step_index)?.result, before, after)) throw new McpError("STATE_CONFLICT", "Исходный переход каталога или источник не подтверждён; обратный план запрещён", 409)
const key = `${step.serverId}:${step.resource}` const key = `${step.serverId}:${step.resource}`
if (!snapshots.has(key)) { if (!snapshots.has(key)) {
const current = await readRosSnapshot(step.serverId, step.resource, undefined, after.sourceProof) const current = await readRosSnapshot(step.serverId, step.resource, undefined, after.sourceProof)
@@ -143,7 +145,7 @@ export function registerRecoveryActions(register: typeof add) {
const key = `${dependency.serverId}:${dependency.resource}` const key = `${dependency.serverId}:${dependency.resource}`
if (snapshots.has(key)) continue if (snapshots.has(key)) continue
const current = await readRosSnapshot(dependency.serverId, dependency.resource, undefined, dependency.sourceProof) const current = await readRosSnapshot(dependency.serverId, dependency.resource, undefined, dependency.sourceProof)
if (current.fingerprint !== dependency.fingerprint || current.sourceProof !== dependency.sourceProof || dependency.projectionVersion !== ROS_SNAPSHOT_VERSION) throw new McpError("STATE_CONFLICT", "Зависимость или источник исходного плана изменились", 409) if (current.fingerprint !== dependency.fingerprint || current.sourceProof !== dependency.sourceProof || dependency.projectionVersion !== rosSnapshotVersion(dependency.resource)) throw new McpError("STATE_CONFLICT", "Зависимость или источник исходного плана изменились", 409)
snapshots.set(key, current) snapshots.set(key, current)
} }
objects.push(...[...new Set([...snapshots.values()].map(snapshot => snapshot.serverId))].map(serverId => ({ kind: "server", id: String(serverId), serverId }))) objects.push(...[...new Set([...snapshots.values()].map(snapshot => snapshot.serverId))].map(serverId => ({ kind: "server", id: String(serverId), serverId })))
+22 -6
View File
@@ -5,14 +5,19 @@ import { eq } from "drizzle-orm"
import { db } from "../../db/index.js" import { db } from "../../db/index.js"
import { servers } from "../../db/schema.js" import { servers } from "../../db/schema.js"
import { MikrotikClient } from "../../services/mikrotik.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, privateSecretFingerprint } from "./security.js"
import { readOspfEffectiveDisabled } from "../../services/ospf-effective-disabled.js" import { readOspfEffectiveDisabled } from "../../services/ospf-effective-disabled.js"
import { greNativeFieldsSchema } from "../../services/gre-mutation-input.js" import { greNativeFieldsSchema } from "../../services/gre-mutation-input.js"
import { canonicalVrfMembers } from "../../services/vrf-members.js" import { canonicalVrfMembers } from "../../services/vrf-members.js"
import { canonicalBridgeVlanValue } from "../../services/bridge-vlan-values.js"
const text = z.string().max(1000) const text = z.string().max(1000)
const name = z.string().trim().min(1).max(100) const name = z.string().trim().min(1).max(100)
const bool = z.boolean() const bool = z.boolean()
const vlanIds = z.string().min(1).max(16384).refine(value => {
try { canonicalBridgeVlanValue("vlan-ids", value); return true } catch { return false }
}, "Требуется непересекающийся список VLAN ID/диапазонов 1..4094")
const integer = (min: number, max: number) => z.number().int().min(min).max(max) const integer = (min: number, max: number) => z.number().int().min(min).max(max)
const common = { name: name.optional(), comment: text.optional(), disabled: bool.optional() } const common = { name: name.optional(), comment: text.optional(), disabled: bool.optional() }
const ipv4 = name.refine(value => isIP(value) === 4, "Требуется IPv4 router/area ID") const ipv4 = name.refine(value => isIP(value) === 4, "Требуется IPv4 router/area ID")
@@ -31,13 +36,15 @@ const route = {
export interface RosResourceSpec { export interface RosResourceSpec {
id: string; path: string; title: string; permission: string; fields: z.ZodObject; id: string; path: string; title: string; permission: string; fields: z.ZodObject;
create: boolean; remove: boolean; requiredOnCreate?: string[]; readOnly?: boolean create: boolean; remove: boolean; requiredOnCreate?: string[]; readOnly?: boolean
/** Native configuration sealed for drift checks, never accepted as writes. */
observeFields?: readonly string[]
} }
export const rosResources: RosResourceSpec[] = [ export const rosResources: RosResourceSpec[] = [
{ id: "routing_filter", path: "/routing/filter/rule", title: "Фильтры маршрутизации", permission: "mm:filters", fields: z.object({ chain: text.optional(), rule: text.optional(), comment: text.optional(), disabled: bool.optional() }).strict(), create: false, remove: false, readOnly: true }, { id: "routing_filter", path: "/routing/filter/rule", title: "Фильтры маршрутизации", permission: "mm:filters", fields: z.object({ chain: text.optional(), rule: text.optional(), comment: text.optional(), disabled: bool.optional() }).strict(), create: false, remove: false, readOnly: true },
{ id: "ethernet", path: "/interface/ethernet", title: "Ethernet", permission: "mm:network", fields: z.object({ ...common, mtu: integer(68, 65535).optional(), "auto-negotiation": bool.optional() }).strict(), create: false, remove: false }, { id: "ethernet", path: "/interface/ethernet", title: "Ethernet", permission: "mm:network", fields: z.object({ ...common, mtu: integer(68, 65535).optional(), "auto-negotiation": bool.optional() }).strict(), create: false, remove: false },
{ id: "bridge", path: "/interface/bridge", title: "Bridge", permission: "mm:network", fields: z.object({ ...common, mtu: integer(68, 65535).optional(), "vlan-filtering": bool.optional(), pvid: integer(1, 4094).optional(), "protocol-mode": z.enum(["none", "stp", "rstp", "mstp"]).optional() }).strict(), create: true, remove: true, requiredOnCreate: ["name"] }, { id: "bridge", path: "/interface/bridge", title: "Bridge", permission: "mm:network", fields: z.object({ ...common, mtu: integer(68, 65535).optional(), "vlan-filtering": bool.optional(), pvid: integer(1, 4094).optional(), "frame-types": z.enum(["admit-all", "admit-only-vlan-tagged", "admit-only-untagged-and-priority-tagged"]).optional(), "ingress-filtering": bool.optional(), "protocol-mode": z.enum(["none", "stp", "rstp", "mstp"]).optional() }).strict(), observeFields: ["mvrp"], create: true, remove: true, requiredOnCreate: ["name"] },
{ id: "bridge_port", path: "/interface/bridge/port", title: "Порты bridge", permission: "mm:network", fields: z.object({ bridge: name.optional(), interface: name.optional(), pvid: integer(1, 4094).optional(), "ingress-filtering": bool.optional(), "frame-types": z.enum(["admit-all", "admit-only-vlan-tagged", "admit-only-untagged-and-priority-tagged"]).optional(), disabled: bool.optional(), comment: text.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["bridge", "interface"] }, { id: "bridge_port", path: "/interface/bridge/port", title: "Порты bridge", permission: "mm:network", fields: z.object({ bridge: name.optional(), interface: name.optional(), pvid: integer(1, 4094).optional(), "ingress-filtering": bool.optional(), "frame-types": z.enum(["admit-all", "admit-only-vlan-tagged", "admit-only-untagged-and-priority-tagged"]).optional(), disabled: bool.optional(), comment: text.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["bridge", "interface"] },
{ id: "bridge_vlan", path: "/interface/bridge/vlan", title: "Bridge VLAN membership", permission: "mm:network", fields: z.object({ bridge: name.optional(), "vlan-ids": name.optional(), tagged: text.optional(), untagged: text.optional(), disabled: bool.optional(), comment: text.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["bridge", "vlan-ids"] }, { id: "bridge_vlan", path: "/interface/bridge/vlan", title: "Bridge VLAN membership", permission: "mm:network", fields: z.object({ bridge: name.optional(), "vlan-ids": vlanIds.optional(), tagged: text.optional(), untagged: text.optional(), disabled: bool.optional(), comment: text.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["bridge", "vlan-ids"] },
{ id: "vlan", path: "/interface/vlan", title: "VLAN", permission: "mm:network", fields: z.object({ ...common, interface: name.optional(), "vlan-id": integer(1, 4094).optional(), mtu: integer(68, 65535).optional(), "use-service-tag": bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["name", "interface", "vlan-id"] }, { id: "vlan", path: "/interface/vlan", title: "VLAN", permission: "mm:network", fields: z.object({ ...common, interface: name.optional(), "vlan-id": integer(1, 4094).optional(), mtu: integer(68, 65535).optional(), "use-service-tag": bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["name", "interface", "vlan-id"] },
{ id: "ipv4_address", path: "/ip/address", title: "IPv4-адреса", permission: "mm:network", fields: z.object({ address: name.optional(), interface: name.optional(), network: name.optional(), comment: text.optional(), disabled: bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["address", "interface"] }, { id: "ipv4_address", path: "/ip/address", title: "IPv4-адреса", permission: "mm:network", fields: z.object({ address: name.optional(), interface: name.optional(), network: name.optional(), comment: text.optional(), disabled: bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["address", "interface"] },
{ 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: "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"] },
@@ -70,6 +77,7 @@ export const rosResources: RosResourceSpec[] = [
export type ConfigurationRow = Record<string, unknown> 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 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 ROS_SNAPSHOT_VERSION = 3
export const rosSnapshotVersion = (resource: string): number => ["bridge", "bridge_port", "bridge_vlan"].includes(resource) ? 4 : ROS_SNAPSHOT_VERSION
export interface RosSnapshot { serverId: number; resource: string; rows: ConfigurationRow[]; fingerprint: string; projectionVersion?: number; sourceProof?: string } export interface RosSnapshot { serverId: number; resource: string; rows: ConfigurationRow[]; fingerprint: string; projectionVersion?: number; sourceProof?: string }
export function resourceById(id: string): RosResourceSpec { export function resourceById(id: string): RosResourceSpec {
const resource = rosResources.find(item => item.id === id) const resource = rosResources.find(item => item.id === id)
@@ -93,13 +101,21 @@ export function configurationRows(resource: RosResourceSpec, payload: unknown):
// RouterOS creates/removes IPv6 link-local rows with interface runtime state. // RouterOS creates/removes IPv6 link-local rows with interface runtime state.
// They cannot be configured independently and must not invalidate a sealed // They cannot be configured independently and must not invalidate a sealed
// inverse plan when its own GRE is disabled. Explicit addresses remain sealed. // inverse plan when its own GRE is disabled. Explicit addresses remain sealed.
const configuration = rows.filter(row => !(resource.id === "ipv6_address" && [true, "true"].includes((row as ConfigurationRow).dynamic as boolean | string) && /^fe80:/i.test(String((row as ConfigurationRow).address)))) const configuration = rows.filter(row => !(resource.id === "ipv6_address" && [true, "true"].includes((row as ConfigurationRow).dynamic as boolean | string) && /^fe80:/i.test(String((row as ConfigurationRow).address)))
// These rows are generated from bridge/port PVID, not independently
// configurable VLAN entries. Their inputs remain sealed in bridge/ports.
&& !(resource.id === "bridge_vlan" && [true, "true", "yes"].includes((row as ConfigurationRow).dynamic as boolean | string)))
const selectedRows = configuration.map(row => { const selectedRows = configuration.map(row => {
const selected = Object.fromEntries(Object.entries(row as ConfigurationRow).filter(([key]) => [".id", "dynamic", "builtin", "default", ...fields].includes(key))) const selected = Object.fromEntries(Object.entries(row as ConfigurationRow).filter(([key]) => [".id", "dynamic", "builtin", "default", ...fields, ...(resource.observeFields ?? [])].includes(key)))
// RouterOS 7.24.5 returns an explicit empty fib flag for non-FIB tables. // RouterOS 7.24.5 returns an explicit empty fib flag for non-FIB tables.
// Normalize only this observed field; missing values must remain unknown. // Normalize only this observed field; missing values must remain unknown.
if (resource.id === "routing_table" && selected.fib === "") selected.fib = "false" if (resource.id === "routing_table" && selected.fib === "") selected.fib = "false"
if (resource.id === "vrf" && selected.interfaces !== undefined) selected.interfaces = canonicalVrfMembers(selected.interfaces) if (resource.id === "vrf" && selected.interfaces !== undefined) selected.interfaces = canonicalVrfMembers(selected.interfaces)
if (["bridge_port", "bridge_vlan"].includes(resource.id) && selected.comment === undefined) selected.comment = ""
if (resource.id === "bridge_vlan") {
try { for (const field of ["vlan-ids", "tagged", "untagged"]) selected[field] = canonicalBridgeVlanValue(field, selected[field]) }
catch { throw new McpError("STATE_CONFLICT", "Недостоверная конфигурация VLAN membership", 409) }
}
return selected return selected
}) })
// Reordering an ordered catalog changes configuration even with unchanged rows. // Reordering an ordered catalog changes configuration even with unchanged rows.
@@ -118,10 +134,10 @@ export async function readRosSnapshot(serverId: number, resourceId: string, befo
const payload = resourceId === "ospf_template" ? await client.getOspfInterfaceTemplates(checkSource) : ["ospf_instance", "ospf_area", "bgp_instance", "bgp_connection"].includes(resourceId) ? await readOspfEffectiveDisabled(await client.get<unknown>(resource.path), async id => { const payload = resourceId === "ospf_template" ? await client.getOspfInterfaceTemplates(checkSource) : ["ospf_instance", "ospf_area", "bgp_instance", "bgp_connection"].includes(resourceId) ? await readOspfEffectiveDisabled(await client.get<unknown>(resource.path), async id => {
await checkSource() await checkSource()
return client.post(resource.path + "/get", { ".id": id, "value-name": "disabled" }) return client.post(resource.path + "/get", { ".id": id, "value-name": "disabled" })
}) : await client.get<unknown>(resource.path) }) : await readRouterOsSnapshotGet<unknown>({ client, path: resource.path, assertCurrent: checkSource })
await checkSource() await checkSource()
const rows = configurationRows(resource, payload) const rows = configurationRows(resource, payload)
return { serverId, resource: resourceId, rows, fingerprint: fingerprint(rows), projectionVersion: ROS_SNAPSHOT_VERSION, sourceProof } return { serverId, resource: resourceId, rows, fingerprint: fingerprint(rows), projectionVersion: rosSnapshotVersion(resourceId), sourceProof }
} }
export async function rosSnapshotSourceProof(server: Awaited<ReturnType<typeof routerClient>>["server"]): Promise<string> { export async function rosSnapshotSourceProof(server: Awaited<ReturnType<typeof routerClient>>["server"]): Promise<string> {
return privateSecretFingerprint(fingerprint({ id: server.id, enabled: server.enabled, host: server.host, port: server.port, useSsl: server.useSsl, verifySsl: server.verifySsl, username: server.username, password: server.password, epoch: server.routingEndpointEpoch }), "mcp-ros-snapshot-source-v3") return privateSecretFingerprint(fingerprint({ id: server.id, enabled: server.enabled, host: server.host, port: server.port, useSsl: server.useSsl, verifySsl: server.verifySsl, username: server.username, password: server.password, epoch: server.routingEndpointEpoch }), "mcp-ros-snapshot-source-v3")
+2 -2
View File
@@ -1,11 +1,11 @@
import { fingerprint, McpError } from "./security.js" import { fingerprint, McpError } from "./security.js"
import { readRosSnapshot, ROS_SNAPSHOT_VERSION, type RosSnapshot, type RosStep } from "./resources.js" import { readRosSnapshot, rosSnapshotVersion, type RosSnapshot, type RosStep } from "./resources.js"
import { verifyRosStepSnapshots } from "./ros-step-verification.js" import { verifyRosStepSnapshots } from "./ros-step-verification.js"
import { acknowledgedCreateId } from "./native-create-identity.js" import { acknowledgedCreateId } from "./native-create-identity.js"
const key = (snapshot: Pick<RosSnapshot, "serverId" | "resource">) => `${snapshot.serverId}:${snapshot.resource}` const key = (snapshot: Pick<RosSnapshot, "serverId" | "resource">) => `${snapshot.serverId}:${snapshot.resource}`
function valid(snapshot: RosSnapshot): boolean { function valid(snapshot: RosSnapshot): boolean {
if (snapshot.projectionVersion !== ROS_SNAPSHOT_VERSION || typeof snapshot.sourceProof !== "string" || !/^[a-f0-9]{64}$/.test(snapshot.sourceProof) || !Array.isArray(snapshot.rows) || snapshot.fingerprint !== fingerprint(snapshot.rows)) return false if (snapshot.projectionVersion !== rosSnapshotVersion(snapshot.resource) || typeof snapshot.sourceProof !== "string" || !/^[a-f0-9]{64}$/.test(snapshot.sourceProof) || !Array.isArray(snapshot.rows) || snapshot.fingerprint !== fingerprint(snapshot.rows)) return false
if (["ip_settings", "ipv6_settings"].includes(snapshot.resource)) return snapshot.rows.length === 1 && !!snapshot.rows[0] && typeof snapshot.rows[0] === "object" && !Array.isArray(snapshot.rows[0]) if (["ip_settings", "ipv6_settings"].includes(snapshot.resource)) return snapshot.rows.length === 1 && !!snapshot.rows[0] && typeof snapshot.rows[0] === "object" && !Array.isArray(snapshot.rows[0])
const ids = snapshot.rows.map(acknowledgedCreateId) const ids = snapshot.rows.map(acknowledgedCreateId)
return ids.every(id => id !== undefined) && new Set(ids).size === ids.length return ids.every(id => id !== undefined) && new Set(ids).size === ids.length
@@ -1,5 +1,6 @@
import { isDeepStrictEqual } from "node:util" import { isDeepStrictEqual } from "node:util"
import { acknowledgedCreateId } from "./native-create-identity.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, type ConfigurationRow, type RosResourceSpec, type RosSnapshot, type RosStep } from "./resources.js"
function booleanValue(value: unknown): unknown { function booleanValue(value: unknown): unknown {
@@ -23,6 +24,9 @@ function requested(resource: RosResourceSpec, fields: RosStep["fields"], row: Co
const schema = resource.fields.shape[key] const schema = resource.fields.shape[key]
if (!schema || typeof value !== "string" || !Object.hasOwn(row, key) || row[key] === undefined || row[key] === null) return false if (!schema || typeof value !== "string" || !Object.hasOwn(row, key) || row[key] === undefined || row[key] === null) return false
if (schema.safeParse(true).success) return ["true", "false"].includes(value) && booleanValue(row[key]) === (value === "true") if (schema.safeParse(true).success) return ["true", "false"].includes(value) && booleanValue(row[key]) === (value === "true")
if (resource.id === "bridge_vlan" && ["vlan-ids", "tagged", "untagged"].includes(key)) {
try { return canonicalBridgeVlanValue(key, row[key]) === canonicalBridgeVlanValue(key, value) } catch { return false }
}
return String(row[key]) === value return String(row[key]) === value
}) })
} }
@@ -1,5 +1,8 @@
import assert from "node:assert/strict" import assert from "node:assert/strict"
import { readFile } from "node:fs/promises"
import { test } from "node:test" import { test } from "node:test"
import { runInNewContext } from "node:vm"
import ts from "typescript"
import type { ActionContext } from "./action-types.js" import type { ActionContext } from "./action-types.js"
import { loadReservedScopedRecoverySource } from "./routing-community-scoped-recovery-source.js" import { loadReservedScopedRecoverySource } from "./routing-community-scoped-recovery-source.js"
import { executeReservedScopedRecovery, scopedRecoveryIntentFromCheckpoint, validateReservedInverseVerified } from "./routing-community-scoped-recovery.js" import { executeReservedScopedRecovery, scopedRecoveryIntentFromCheckpoint, validateReservedInverseVerified } from "./routing-community-scoped-recovery.js"
@@ -16,6 +19,67 @@ test("a serialized allocation cannot enter the shared inverse engine", async ()
await assert.rejects(executeReservedScopedRecovery(forged, {} as Parameters<typeof executeReservedScopedRecovery>[1]), /capability/) await assert.rejects(executeReservedScopedRecovery(forged, {} as Parameters<typeof executeReservedScopedRecovery>[1]), /capability/)
}) })
test("confirmed inverse spend quarantines the exact slot on post-spend refusal without replay", async () => {
// Execute the actual boundary body with only its I/O dependencies substituted.
// This does not mint a production capability or claim native/SQL acceptance.
const source = ts.createSourceFile("routing-community-scoped-recovery.ts",
await readFile(new URL("./routing-community-scoped-recovery.ts", import.meta.url), "utf8"), ts.ScriptTarget.ES2022)
const declaration = source.statements.find(node => ts.isFunctionDeclaration(node) && node.name?.text === "executeReservedScopedRecovery")
assert(declaration)
const compiled = ts.transpileModule(declaration.getText(source), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText
const allocation = Object.freeze({ kind: "routing-reserved-inverse-allocation" })
const execution = Object.freeze({ kind: "routing-reserved-inverse-execution" })
const principal = { connectionId: "owned", credentialId: "original" }
const original = new Error("original authority failure"), result = Object.freeze({ outcome: "recovered" })
const scenarios = {
spend: ["spend"],
inspect: ["spend", "inspect", "quarantine"],
authority: ["spend", "inspect", "authority", "quarantine"],
source: ["spend", "inspect", "authority", "source", "quarantine"],
proof: ["spend", "inspect", "authority", "source", "proof", "quarantine"],
engine: ["spend", "inspect", "authority", "source", "proof", "engine", "quarantine"],
success: ["spend", "inspect", "authority", "source", "proof", "engine"],
} as const
for (const [scenario, expected] of Object.entries(scenarios)) {
const events: string[] = [], exports: Record<string, unknown> = {}, capabilities = new WeakMap()
let request: object | undefined
const at = (phase: string) => { events.push(phase); if (phase === scenario) throw original }
runInNewContext(compiled, {
exports, reservedExecutions: capabilities, fingerprint: JSON.stringify,
createRoutingReservationStore: () => ({
async beginInverseAttempt(value: unknown) {
assert.equal(value, allocation); at("spend"); return { reservationId: "spent-slot", execution }
},
async inspectInverseExecution(value: unknown) {
assert.equal(value, execution); at("inspect")
return { principal, sourceOperationId: "source", deadline: "2026-10-29T01:00:00.000Z" }
},
async markUnknown(id: string, lane: string) {
assert.equal(id, "spent-slot"); assert.equal(lane, "inverse"); at("quarantine")
throw new Error("quarantine persistence unavailable")
},
}),
effectiveMcpConnection: async () => { at("authority"); return { owner_id: "owner", policy: {}, owner_permissions: [] } },
loadReservedScopedRecoverySource: async () => {
at("source"); return { profile: { serverId: 1 }, input: {}, configuration: { packet: {} } }
},
accessGeneration: async () => "generation",
deadline: () => Date.parse("2026-10-29T01:00:00.000Z"),
sealedProof: async () => { at("proof"); return "proof" },
executeScopedRecovery: async (value: object) => {
request = value; assert.equal(capabilities.get(value), execution); at("engine"); return result
},
})
const execute = exports.executeReservedScopedRecovery as
(allocation: unknown, expected: { sourceOperationId: string; principal: typeof principal }) => Promise<unknown>
const outcome = execute(allocation, { sourceOperationId: "source", principal })
if (scenario === "success") assert.equal(await outcome, result)
else await assert.rejects(outcome, error => error === original)
assert.deepEqual(events, expected, scenario)
if (request) assert.equal(capabilities.has(request), false)
}
})
test("configuration equality or generic VERIFIED cannot replace reserved signed receipts", async () => { test("configuration equality or generic VERIFIED cannot replace reserved signed receipts", async () => {
const expected = {} as Parameters<typeof validateReservedInverseVerified>[1] const expected = {} as Parameters<typeof validateReservedInverseVerified>[1]
for (const row of [{}, { state: "verified", phase: "RECOVERY_VERIFIED", configurationVerified: true }, { inverse_intent_sealed: "x".repeat(524289) }]) { for (const row of [{}, { state: "verified", phase: "RECOVERY_VERIFIED", configurationVerified: true }, { inverse_intent_sealed: "x".repeat(524289) }]) {
@@ -22,33 +22,38 @@ import { compileManagedFilterRules } from "../../services/filter-rules-write-mod
import { attestRoutingFixture } from "../../test/mcp-routing-fixture.js" import { attestRoutingFixture } from "../../test/mcp-routing-fixture.js"
import { createMcpConnection, authenticateMcpToken, setConnectionStatus, effectiveMcpConnection } from "./store.js" import { createMcpConnection, authenticateMcpToken, setConnectionStatus, effectiveMcpConnection } from "./store.js"
import { invokeMcpTool, approvePlan, applyPlan, publicPlan, loadPlan } from "./runtime.js" import { invokeMcpTool, approvePlan, applyPlan, publicPlan, loadPlan } from "./runtime.js"
import { accessGeneration, decryptSecret, fingerprint, McpError } from "./security.js" import { accessGeneration, decryptSecret, encryptSecret, fingerprint, McpError } from "./security.js"
import type { McpPrincipal } from "./store.js" import type { McpPrincipal } from "./store.js"
import { loadScopedRecoverySource } from "./routing-community-scoped-recovery-source.js" import { loadScopedRecoverySource } from "./routing-community-scoped-recovery-source.js"
import { findMcpService } from "./service-registry.js" import { findMcpService } from "./service-registry.js"
import { prepareScopedCommunitySwitch } from "./routing-community-scoped-service.js" import { prepareScopedCommunitySwitch } from "./routing-community-scoped-service.js"
import { resolveRoutingNetworkConfigurationContext, resolveRoutingNetworkConfigurationContextFromSharedContext, consumeRoutingNetworkConfigurationContext, closeRoutingNetworkConfigurationContext, import { resolveRoutingNetworkConfigurationContext, consumeRoutingNetworkConfigurationContext, closeRoutingNetworkConfigurationContext } from "./routing-network-configuration-bridge.js"
resolveRoutingNetworkForwardRoundConfigurationContext, closeRoutingNetworkForwardRoundConfigurationContext } from "./routing-network-configuration-bridge.js"
import { createRoutingNetworkNativeSession } from "./routing-network-native-session.js" import { createRoutingNetworkNativeSession } from "./routing-network-native-session.js"
import { requireRecentPath } from "./path-verification.js" import { requireRecentPath } from "./path-verification.js"
import { registerManagementChannel } from "./management.js" import { registerManagementChannel } from "./management.js"
import { isSchedulerJobsPaused, pauseSchedulerJobsAndWait, resumeSchedulerJobs } from "../../services/scheduler-running.js" import { isSchedulerJobsPaused, pauseSchedulerJobsAndWait, resumeSchedulerJobs } from "../../services/scheduler-running.js"
/** Disposable SQL + loopback HTTP only. Does not access or configure CHR. */ /** Disposable SQL + loopback HTTP only. Does not access or configure CHR. */
export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options: { originalEffectsOnly?: boolean; originalCredentialRevocationOnly?: boolean; forwardRoundsOnly?: boolean; manualInverseChargeOnly?: boolean; manualInverseChargeCase?: "positive" | "quota" | "concurrent" | "unknown" | "revoked" | "nonpersistent" | "mixed-forward" } = {}) { 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" } = {}) {
const sid = 131, writer: AuthUser = { ...actor, permissions: [...new Set([...actor.permissions, "mm:filters:write"])] } 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 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 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" }))
let mode: "normal" | "ignored" | "lost" | "collateral" | "post-failed" | "post-unknown" = "normal", writes = 0, requests = 0 let mode: "normal" | "ignored" | "lost" | "collateral" | "post-failed" | "post-unknown" = "normal", writes = 0, requests = 0
let controllerProbeUnknown = false
let controllerFailoverProbeSources: string[] | undefined
let controllerFailoverReserveHealthy = true
let pendingPostCheckpoint = false, postCheckpointsObserved = 0 let pendingPostCheckpoint = false, postCheckpointsObserved = 0
let afterForwardWrite: (() => Promise<void>) | undefined let afterForwardWrite: (() => Promise<void>) | undefined
let afterConfigurationCheckpoint: ((operationId: string) => Promise<void>) | undefined let afterConfigurationCheckpoint: ((operationId: string) => Promise<void>) | undefined
let forwardRoundBulk = 0, afterRoundLogRead: (() => Promise<void>) | undefined let forwardRoundBulk = 0, afterRoundLogRead: (() => Promise<void>) | undefined
let primaryDown = false, primaryControlRequests = 0, pendingInversePost = false, inversePostChecks = 0 let primaryDown = false, primaryControlRequests = 0, pendingInversePost = false, inversePostChecks = 0
const observedVrfs = new Set<string>() const observedVrfs = new Set<string>()
// Only this separately selected SQL/localhost phase changes its wire data. const paddedEffectResources = new Set(["/routing/route", "/routing/bgp/session", "/routing/bfd/session", "/log"])
// These are declared synthetic RouterOS responses, never actual CHR facts. // Persistent fixtures need an original RIB selection. These are declared
// synthetic RouterOS responses, never actual CHR facts.
let originalEffectCase: "transition" | "no-op" | "ambiguous" = "transition" let originalEffectCase: "transition" | "no-op" | "ambiguous" = "transition"
let exposeEffectCatalogue = false
const originalEffectWire: Array<{ method: string; path: string; afterPatch: boolean }> = [] const originalEffectWire: Array<{ method: string; path: string; afterPatch: boolean }> = []
const originalEffectChanged = () => originalEffectCase === "no-op" || filters[0].rule !== initial const originalEffectChanged = () => originalEffectCase === "no-op" || filters[0].rule !== initial
const effectRoutes = () => { const effectRoutes = () => {
@@ -62,25 +67,31 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
const effectConnection = { ".id": "*ca", name: "peer-a", instance: "default", vrf: "main", "routing-table": "tenant-a", afi: "ip", const effectConnection = { ".id": "*ca", name: "peer-a", instance: "default", vrf: "main", "routing-table": "tenant-a", afi: "ip",
"input.filter": "custom-a", "output.filter-chain": "", templates: "", disabled: "false", "use-bfd": "false", "input.filter": "custom-a", "output.filter-chain": "", templates: "", disabled: "false", "use-bfd": "false",
"local.address": "198.18.0.1", "remote.address": "198.18.0.2", "remote.as": "65001" } "local.address": "198.18.0.1", "remote.address": "198.18.0.2", "remote.as": "65001" }
const { vrf: effectTransportVrf, ...effectSessionFields } = effectConnection
const effectSession = { ...effectSessionFields, ".id": "*sa", "local.afi": "ip", "remote.afi": "ip", established: "true",
"local.address": `${effectConnection["local.address"]}@${effectTransportVrf}`, "remote.address": `${effectConnection["remote.address"]}@${effectTransportVrf}` }
const collections = (path: string): Record<string, unknown>[] => ({ const collections = (path: string): Record<string, unknown>[] => ({
"/system/resource": [{ version: "7.20.1" }], "/system/resource": [{ version: "7.20.1" }],
...Object.fromEntries(["/routing/route", "/routing/bgp/session", "/routing/bfd/session", "/log"].map(resource => [resource, "/routing/table": ["main", "tenant-a", "tenant-b", "probe-a", "probe-b"].map((name, index) => ({ ".id": "*t" + index, name, fib: "true", disabled: "false", invalid: "false" })),
Array.from({ length: forwardRoundBulk }, (_, index) => ({ ".id": "*" + index, synthetic: "bounded owned budget payload" }))])),
"/routing/table": ["main", "tenant-a", "tenant-b", "probe-a", "probe-b"].map((name, index) => ({ ".id": "*t" + index, name, fib: "true" })),
"/ip/vrf": ["a", "b"].map(key => ({ ".id": "*v" + key, name: "probe-" + key, interfaces: "wan-" + key, disabled: "false" })).concat([{ ".id": "*vm", name: "main", interfaces: "all", disabled: "false" }]), "/ip/vrf": ["a", "b"].map(key => ({ ".id": "*v" + key, name: "probe-" + key, interfaces: "wan-" + key, disabled: "false" })).concat([{ ".id": "*vm", name: "main", interfaces: "all", disabled: "false" }]),
"/routing/bgp/connection": ["a", "b"].map(key => ({ ".id": "*c" + key, name: "peer-" + key, instance: "default", vrf: "main", "routing-table": "tenant-" + key, afi: "ip", "input.filter": "custom-" + key, "output.filter-chain": "", templates: "", disabled: "false" })), "/routing/bgp/connection": [effectConnection, { ...effectConnection, ".id": "*cb", name: "peer-b", "routing-table": "tenant-b", "input.filter": "custom-b" }],
"/routing/bgp/instance": [{ ".id": "*i", name: "default", vrf: "main", "routing-table": "main", disabled: "false" }], "/routing/bgp/instance": [{ ".id": "*i", name: "default", vrf: "main", "routing-table": "main", disabled: "false" }],
"/routing/bgp/template": [], "/routing/filter/rule": filters, "/routing/bgp/template": [], "/routing/filter/rule": filters,
"/ip/route": [1, 9].map(value => ({ ".id": "*r" + value, "dst-address": "192.0.2." + value + "/32", "routing-table": "main", disabled: "false", comment: managedComment("recursive"), opaque: "NEVER_SCOPED_ROUTE" })), "/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" })), "/interface": ["wan-a", "wan-b", "hop", "target"].map((name, index) => ({ ".id": "*if" + index, name, 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" })), "/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" })) : []),
],
...(options.originalEffectsOnly ? { ...(options.originalEffectsOnly ? {
"/routing/table": ["main", "tenant-a", "tenant-b", "probe-a", "probe-b"].map((name, index) => ({ ".id": "*t" + index, name, fib: "", disabled: "false", invalid: "false" })), "/routing/table": ["main", "tenant-a", "tenant-b", "probe-a", "probe-b"].map((name, index) => ({ ".id": "*t" + index, name, fib: "", disabled: "false", invalid: "false" })),
"/routing/route": effectRoutes(), "/routing/bgp/connection": [effectConnection], "/routing/bgp/connection": [effectConnection],
"/routing/bgp/session": [{ ...effectConnection, ".id": "*sa", "local.afi": "ip", "remote.afi": "ip", established: "true" }], "/log": [{ ".id": "*l1", topics: "bgp,route,info", message: "Declared synthetic localhost effect observation; interval coverage unknown" }],
"/routing/bfd/session": [], "/log": [{ ".id": "*l1", topics: "bgp,route,info", message: "Declared synthetic localhost effect observation; interval coverage unknown" }], } : {}),
"/ip/route": [1, 9].map(value => ({ ".id": "*r" + value, "dst-address": "192.0.2." + value + "/32", "routing-table": "main", disabled: "false", ...(exposeEffectCatalogue || options.originalEffectsOnly ? {
"immediate-gw": "203.0.113." + value + "%wan-a", comment: managedComment("recursive"), opaque: "NEVER_SCOPED_ROUTE" })), "/routing/route": effectRoutes(),
"/routing/bgp/session": [effectSession],
} : {}), } : {}),
} as Record<string, Record<string, unknown>[]>)[path] ?? [] } as Record<string, Record<string, unknown>[]>)[path] ?? []
let requestFailure: { error: unknown } | undefined let requestFailure: { error: unknown } | undefined
@@ -92,7 +103,7 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
} }
let raw = ""; for await (const chunk of req) raw += chunk let raw = ""; for await (const chunk of req) raw += chunk
const args = raw ? JSON.parse(raw) : {}, path = decodeURIComponent((req.url ?? "").replace(/^\/rest/, "")) const args = raw ? JSON.parse(raw) : {}, path = decodeURIComponent((req.url ?? "").replace(/^\/rest/, ""))
if (options.originalEffectsOnly) originalEffectWire.push({ method: req.method ?? "", path, afterPatch: filters[0].rule !== initial }) if (options.originalEffectsOnly || options.forwardRoundsOnly) originalEffectWire.push({ method: req.method ?? "", path, afterPatch: filters[0].rule !== initial })
if (req.socket.localAddress === "127.0.0.1") { if (req.socket.localAddress === "127.0.0.1") {
if (/^\/(routing|tool)\//.test(path) || path === "/system/license") primaryControlRequests++ if (/^\/(routing|tool)\//.test(path) || path === "/system/license") primaryControlRequests++
if (primaryDown) { req.socket.destroy(); return } if (primaryDown) { req.socket.destroy(); return }
@@ -101,28 +112,36 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
if (req.method === "GET") { if (req.method === "GET") {
const payload = collections(path) const payload = collections(path)
if (path === "/log" && afterRoundLogRead) { const hook = afterRoundLogRead; afterRoundLogRead = undefined; await hook() } if (path === "/log" && afterRoundLogRead) { const hook = afterRoundLogRead; afterRoundLogRead = undefined; await hook() }
res.end(JSON.stringify(payload)); return // Force the cumulative 10,000-row budget without exceeding the native
// cap of 1,000 rows on any individual response.
const padding = forwardRoundBulk && paddedEffectResources.has(path)
? Array.from({ length: 1000 - payload.length }, (_, index) => ({ ".id": "*padding" + index })) : []
res.end(JSON.stringify(padding.length ? [...payload, ...padding] : payload)); return
} }
if (req.method === "POST" && path.endsWith("/get")) { const row = collections(path.slice(0, -4)).find(row => row[".id"] === args[".id"]), value = row?.[args["value-name"]]; res.end(JSON.stringify(value === undefined ? [] : { ret: value })); return } if (req.method === "POST" && path.endsWith("/get")) { const row = collections(path.slice(0, -4)).find(row => row[".id"] === args[".id"]), value = row?.[args["value-name"]]; res.end(JSON.stringify(value === undefined ? [] : { ret: value })); return }
if (req.method === "POST" && path === "/tool/ping") { if (req.method === "POST" && path === "/tool/ping") {
if (pendingInversePost) { if (pendingInversePost) {
const intents = (await pool.query("SELECT source_operation_id FROM mcp_scoped_recovery_intents WHERE state='intent'")).rows assert.ok(connectionId)
const intents = (await pool.query("SELECT i.source_operation_id FROM mcp_scoped_recovery_intents i JOIN mcp_operations o ON o.id=i.recovery_operation_id WHERE i.state='intent' AND o.connection_id=$1", [connectionId])).rows
assert.equal(intents.length, 1, "Post-inverse probe must follow one durable single-use inverse intent") assert.equal(intents.length, 1, "Post-inverse probe must follow one durable single-use inverse intent")
pendingInversePost = false; inversePostChecks++ pendingInversePost = false; inversePostChecks++
} }
if (pendingPostCheckpoint) { if (pendingPostCheckpoint) {
const checkpoint = (await pool.query("SELECT c.configuration_sealed,c.configuration_proof FROM mcp_operation_checkpoints c JOIN mcp_operations o ON o.id=c.operation_id WHERE o.state='running' AND c.configuration_sealed IS NOT NULL")).rows assert.ok(connectionId)
const checkpoint = (await pool.query("SELECT c.configuration_sealed,c.configuration_proof FROM mcp_operation_checkpoints c JOIN mcp_operations o ON o.id=c.operation_id WHERE o.state='running' AND c.configuration_sealed IS NOT NULL AND o.connection_id=$1", [connectionId])).rows
assert.equal(checkpoint.length, 1, "Exact CONFIG_APPLIED must commit before the post-change network probe") assert.equal(checkpoint.length, 1, "Exact CONFIG_APPLIED must commit before the post-change network probe")
const packet = JSON.parse(await decryptSecret(checkpoint[0].configuration_sealed)) const packet = JSON.parse(await decryptSecret(checkpoint[0].configuration_sealed))
assert.equal(packet.phase, "CONFIG_APPLIED"); assert.equal(packet.originalRule, initial) assert.equal(packet.phase, "CONFIG_APPLIED"); assert.equal(packet.originalRule, initial)
assert.equal(packet.originalCandidateId, "a"); assert.equal(packet.desiredCandidateId, "b") 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(packet.desiredRule, filters.find(row => row[".id"] === packet.nativeId)!.rule)
assert.ok(checkpoint[0].configuration_proof); pendingPostCheckpoint = false; postCheckpointsObserved++ assert.ok(checkpoint[0].configuration_proof); pendingPostCheckpoint = false; postCheckpointsObserved++
const sharedCheck = afterConfigurationCheckpoint; afterConfigurationCheckpoint = undefined const sharedCheck = afterConfigurationCheckpoint; afterConfigurationCheckpoint = undefined
await sharedCheck?.(packet.operationId) await sharedCheck?.(packet.operationId)
} }
observedVrfs.add(args.vrf) observedVrfs.add(args.vrf)
res.end(JSON.stringify(mode === "post-unknown" && writes ? [{ message: "unsupported fixture output" }] : [{ sent: "3", received: mode === "post-failed" && writes ? "0" : "3", "packet-loss": mode === "post-failed" && writes ? "100" : "0", "avg-rtt": "1ms" }])) if (controllerFailoverProbeSources) controllerFailoverProbeSources.push(args["src-address"])
const probeFailed = controllerFailoverProbeSources && (!controllerFailoverReserveHealthy || args["src-address"] !== "198.51.100.13") || mode === "post-failed" && writes
res.end(JSON.stringify(controllerProbeUnknown || mode === "post-unknown" && writes ? [{ message: "unsupported fixture output" }] : [{ sent: "3", received: probeFailed ? "0" : "3", "packet-loss": probeFailed ? "100" : "0", "avg-rtt": "1ms" }]))
return return
} }
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 === "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 }
@@ -180,15 +199,16 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
} while(!workerLocked) } 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]) 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 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", "operations.reconcile", "operations.recover"], allowGlobal: false }, limits: { callsPerMinute: 1000, mutationsPerHour: 300 } }), writer) 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)
connectionId = access.connection.id; const principal = await authenticateMcpToken(access.token) 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", expectedHops: [{ serverId: sid, interface: "hop" }, { serverId: sid, interface: "target" }] } })) }) 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"]) { for (const key of ["a", "b"]) for (const iface of ["wan-" + key, "hop"]) {
const id = 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',$4,1,0,100,100,now(),now(),true,'fixture')", [id, sid, iface, "tenant-" + key]); await attestRoutingFixture(id) const id = 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',$4,1,0,100,100,now(),now(),true,'fixture')", [id, sid, iface, "tenant-" + key]); await attestRoutingFixture(id)
} }
await pool.query("INSERT INTO filter_rules(server_id,sort_order,community,community_name,action,gateway,gateway_tunnel_id,description) VALUES($1,77,'65000:10','NEVER_SCOPED_SQL_NAME','route','203.0.113.99','NEVER_SCOPED_SQL_TUNNEL','NEVER_SCOPED_SQL_DESCRIPTION')", [sid]) await pool.query("INSERT INTO filter_rules(server_id,sort_order,community,community_name,action,gateway,gateway_tunnel_id,description) VALUES($1,77,'65000:10','NEVER_SCOPED_SQL_NAME','route','203.0.113.99','NEVER_SCOPED_SQL_TUNNEL','NEVER_SCOPED_SQL_DESCRIPTION')", [sid])
const dbBefore = (await pool.query("SELECT * FROM filter_rules WHERE server_id=$1 ORDER BY id", [sid])).rows const dbBefore = (await pool.query("SELECT * FROM filter_rules WHERE server_id=$1 ORDER BY id", [sid])).rows
const prepare = async (key = "a", persistent?: { id: string; version: number }, exactProfile?: ReturnType<typeof profile>, assertApprovalRequired = false) => { const prepare = async (key = "a", persistent?: { id: string; version: number }, exactProfile?: ReturnType<typeof profile>, assertApprovalRequired = false, approver: AuthUser | null = writer) => {
exposeEffectCatalogue = !!persistent
const current = exactProfile === undefined ? profile(key) : routeAiProfileSchema.parse(exactProfile) const current = exactProfile === undefined ? profile(key) : routeAiProfileSchema.parse(exactProfile)
assert.equal((await invokeMcpTool(principal, "mm_route_path_verify", { profile: current, candidateId: "b" }) as { status: string }).status, "passed") assert.equal((await invokeMcpTool(principal, "mm_route_path_verify", { profile: current, candidateId: "b" }) as { status: string }).status, "passed")
const plan = await invokeMcpTool(principal, "mm_route_optimizer_plan", { profile: current, candidateId: "b" }, undefined, const plan = await invokeMcpTool(principal, "mm_route_optimizer_plan", { profile: current, candidateId: "b" }, undefined,
@@ -209,15 +229,20 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
await assert.rejects(applyPlan(principal, plan.planId, randomUUID()), (error: unknown) => error instanceof McpError && error.code === "APPROVAL_REQUIRED") await assert.rejects(applyPlan(principal, plan.planId, randomUUID()), (error: unknown) => error instanceof McpError && error.code === "APPROVAL_REQUIRED")
assert.deepEqual({ writes, requests }, beforeApproval) assert.deepEqual({ writes, requests }, beforeApproval)
} }
await approvePlan(plan.planId, writer, true); return { plan, input } if (approver) await approvePlan(plan.planId, approver, true)
return { plan, input, evidence }
} }
const execute = async (planId: string, executingPrincipal: McpPrincipal = principal) => { const waitForOperation = async (operationId: string) => {
const op = await applyPlan(executingPrincipal, planId, randomUUID()); let state = "" let state = ""
const deadline = Date.now() + 140000 const deadline = Date.now() + 140000
do { state = (await pool.query("SELECT state FROM mcp_operations WHERE id=$1", [op.id])).rows[0].state; if (!["pending", "running"].includes(state)) break; await delay(20) } while (Date.now() < deadline) do { state = (await pool.query("SELECT state FROM mcp_operations WHERE id=$1", [operationId])).rows[0].state; if (!["pending", "running"].includes(state)) break; await delay(20) } while (Date.now() < deadline)
if(requestFailure)throw requestFailure.error if(requestFailure)throw requestFailure.error
assert.ok(!["pending", "running"].includes(state), "The bounded executor must reach a terminal state before assertions or fixture cleanup") assert.ok(!["pending", "running"].includes(state), "The bounded executor must reach a terminal state before assertions or fixture cleanup")
return { ...op, state } return state
}
const execute = async (planId: string, executingPrincipal: McpPrincipal = principal) => {
const op = await applyPlan(executingPrincipal, planId, randomUUID())
return { ...op, state: await waitForOperation(op.id) }
} }
const reconcile = async (operationId: string) => (await invokeMcpTool(principal, "mm_operation_reconcile", { operationId }) as { results: Array<{ matchesDesiredState: boolean; configurationMatchesDesired: boolean; configurationCheckpointAvailable: boolean }> }).results[0] const reconcile = async (operationId: string) => (await invokeMcpTool(principal, "mm_operation_reconcile", { operationId }) as { results: Array<{ matchesDesiredState: boolean; configurationMatchesDesired: boolean; configurationCheckpointAvailable: boolean }> }).results[0]
const reset = () => { filters.forEach(row => { row.rule = initial; row.opaque = "NEVER_SCOPED_OPAQUE" }); mode = "normal"; pendingPostCheckpoint = false; pendingInversePost = false; primaryDown = false } const reset = () => { filters.forEach(row => { row.rule = initial; row.opaque = "NEVER_SCOPED_OPAQUE" }); mode = "normal"; pendingPostCheckpoint = false; pendingInversePost = false; primaryDown = false }
@@ -225,8 +250,8 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
await pool.query("UPDATE mcp_routing_observations SET ping_at=now(),bandwidth_at=now(),loss_pct=0 WHERE src_server_id=$1", [sid]) await pool.query("UPDATE mcp_routing_observations SET ping_at=now(),bandwidth_at=now(),loss_pct=0 WHERE src_server_id=$1", [sid])
for (const row of (await pool.query("SELECT id FROM mcp_routing_observations WHERE src_server_id=$1", [sid])).rows) await attestRoutingFixture(row.id) for (const row of (await pool.query("SELECT id FROM mcp_routing_observations WHERE src_server_id=$1", [sid])).rows) await attestRoutingFixture(row.id)
} }
// Persistent enabled rows here are synthetic authority fixtures only. The // Seed authority for fault cases. The public save/approval path is
// public scoped controller enable guard is not removed or exercised. // exercised separately below without inserting a profile directly.
const persistentFixture = async (config = profile("a")) => { const persistentFixture = async (config = profile("a")) => {
const id = randomUUID(), current = await effectiveMcpConnection(principal) const id = randomUUID(), current = await effectiveMcpConnection(principal)
const expiry = new Date(Math.min(Date.now() + 600000, current.owner_valid_until.getTime(), Date.parse(principal.credentialExpiresAt), Date.parse(current.policy.expiresAt))) const expiry = new Date(Math.min(Date.now() + 600000, current.owner_valid_until.getTime(), Date.parse(principal.credentialExpiresAt), Date.parse(current.policy.expiresAt)))
@@ -241,33 +266,197 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
return { id, version: 1 } return { id, version: 1 }
} }
const disablePersistent = (id: string) => pool.query("UPDATE mcp_routing_profiles SET enabled=false WHERE id=$1", [id]) const disablePersistent = (id: string) => pool.query("UPDATE mcp_routing_profiles SET enabled=false WHERE id=$1", [id])
const executeWithCheckpointObservation = async (planId: string, check: (operationId: string) => Promise<void>) => {
const observer = observeCommittedScopedCheckpoint(pool, check, {
databaseUrl: process.env.DATABASE_URL ?? "", stateDir: process.env.MCP_STATE_DIR ?? "",
})
try {
const operation = await execute(planId)
observer.assertCompleted()
return operation
} finally { observer.close() }
}
const acceptControllerFailover = async () => {
reset(); await refresh(); exposeEffectCatalogue = true; originalEffectCase = "transition"
controllerFailoverProbeSources = []
const profileId = randomUUID(), initialWrites = writes
persistentProfileIds.push(profileId)
try {
const configured = profile("a"), original = configured.candidates[0]
configured.candidates = [1, 10, 11, 9].map((gateway, index) => ({ ...original,
id: ["a", "b", "c", "d"][index], gateway: "192.0.2." + gateway,
gatewayRef: { resource: "ipv4_route", id: "*r" + gateway },
controlProbe: { ...original.controlProbe!, sourceAddress: "198.51.100." + [1, 11, 12, 13][index] },
}))
const analysis = await invokeMcpTool(principal, "mm_route_optimizer_analyze", { profile: configured }) as { currentCandidateId: string; ranking: Array<{ eligible: boolean; candidate: { id: string } }> }
assert.equal(analysis.currentCandidateId, "a")
assert.deepEqual(analysis.ranking.filter(row => row.eligible).map(row => row.candidate.id), ["a", "b", "c", "d"])
const enrollment = await invokeMcpTool(principal, "mm_route_profile_save_plan", {
profileId, profile: configured, enabled: true, expiresAt: new Date(Date.now() + 600000).toISOString(),
}) as { planId: string }
await approvePlan(enrollment.planId, writer, true)
assert.equal((await execute(enrollment.planId)).state, "applied")
const readController = async () => (await pool.query("SELECT enabled,state FROM mcp_routing_profiles WHERE id=$1", [profileId])).rows[0]
const failures = new Set<string>()
let switchPlanId: string | undefined
for (const [index, alternate] of [12, 13, 13, 12, 13].entries()) {
// Drive the normal scheduled entry point without waiting a minute.
await pool.query("UPDATE mcp_routing_profiles SET state=state-'probedAt' WHERE id=$1", [profileId])
controllerFailoverProbeSources.length = 0
// The previously passing reserve must be rechecked, and its prior
// pass cannot authorize failover if the current check fails.
controllerFailoverReserveHealthy = index !== 2
await tickRoutingProfiles()
const row = await readController()
assert.equal(row.enabled, true, String(row.state.pausedReason))
assert.deepEqual(controllerFailoverProbeSources, [1, 11, alternate].map(last => "198.51.100." + last))
failures.add(row.state.failureKey)
assert.equal(failures.size, index + 1, "Each counted failure must come from a distinct path check")
assert.equal(writes, initialWrites)
if (index < 4) {
assert.equal(row.state.failureCount, index + 1)
assert.equal(row.state.planId, undefined, "Earlier healthy reserve evidence cannot replace a fresh passing probe")
} else {
assert.equal(row.state.candidateId, "d")
switchPlanId = row.state.planId
}
}
assert.ok(switchPlanId)
assert.equal((await loadPlan(switchPlanId)).state, "awaiting-approval")
await tickRoutingProfiles()
assert.equal(writes, initialWrites)
await approvePlan(switchPlanId, writer, true)
await tickRoutingProfiles()
const operationId: string = (await readController()).state.operationId
assert.ok(operationId)
assert.equal(await waitForOperation(operationId), "applied")
await tickRoutingProfiles()
const completed = await readController()
assert.equal(completed.enabled, true, String(completed.state.pausedReason))
assert.equal(completed.state.baseline.packet.candidateId, "d")
assert.equal(completed.state.baseline.packet.transition.operationId, operationId)
assert.equal(completed.state.failoverReserveCursor, undefined)
assert.equal(writes, initialWrites + 1)
const result = { accepted: true, stableRanking: ["a", "b", "c", "d"], selectedReserve: "d", probeCallsPerTick: 3,
distinctFailuresBeforePlan: failures.size, priorPassedProbeNotReused: true, humanApproval: true, nativeWrites: 1 }
console.log("Bounded controller reserve SQL/HTTP:", JSON.stringify(result))
return result
} finally { await disablePersistent(profileId); controllerFailoverProbeSources = undefined; controllerFailoverReserveHealthy = true }
}
if (options.controllerFailoverOnly) return { ...await acceptControllerFailover(), persistentAuthorityCases: 0 }
if (options.originalEffectsOnly) { if (options.originalEffectsOnly) {
const cases: Array<Record<string, unknown>> = [] const cases: Array<Record<string, unknown>> = []
let publicManualRevision2: Record<string, unknown> | undefined let publicManualRevision2: Record<string, unknown> | undefined
for (const scenario of ["transition", "no-op", "ambiguous", "public-manual-rev2"] as const) { const delegatedAdmin: AuthUser = { ...writer, id: "scoped-delegated-approval-admin", permissions: [...new Set([...writer.permissions, "mm:mcp:admin", "mm:mcp:approve"])] }
const snapshotApprovalAndAdmission = async (planId: string) => (await pool.query(`SELECT
(SELECT count(*)::int FROM mcp_operations WHERE plan_id=$1) AS operations,
(SELECT count(*)::int FROM mcp_routing_forward_charges WHERE source_plan_id=$1) AS forward_charges,
(SELECT COALESCE(jsonb_object_agg(bucket,used),'{}'::jsonb) FROM mcp_budget_buckets WHERE connection_id=$2 AND kind='mutations') AS mutation_buckets,
(SELECT count(*)::int FROM mcp_audit WHERE connection_id=$2 AND action='plan.approve' AND payload->>'planId'=$1::text) AS approval_audit`, [planId, connectionId])).rows[0]
const assertOwnerApproval = async (planId: string) => {
const nativeBefore = { requests, writes }
const before = await snapshotApprovalAndAdmission(planId)
assert.equal(before.operations, 0); assert.equal(before.forward_charges, 0); assert.equal(before.approval_audit, 0)
await assert.rejects(approvePlan(planId, delegatedAdmin, true), (error: unknown) => error instanceof McpError && error.code === "PERMISSION_DENIED")
const denied = await loadPlan(planId)
assert.equal(denied.state, "awaiting-approval"); assert.equal(denied.approved_by, null)
assert.deepEqual(await snapshotApprovalAndAdmission(planId), before)
// Simulate a delegated approval stored by an older deployment.
await pool.query("UPDATE mcp_plans SET state='approved',approved_by=$2,approved_at=now() WHERE id=$1", [planId, delegatedAdmin.id])
try {
await assert.rejects(applyPlan(principal, planId, randomUUID()), (error: unknown) => error instanceof McpError && error.code === "APPROVAL_REQUIRED")
const stale = await loadPlan(planId)
assert.equal(stale.state, "approved"); assert.equal(stale.approved_by, delegatedAdmin.id)
assert.deepEqual(await snapshotApprovalAndAdmission(planId), before)
assert.deepEqual({ requests, writes }, nativeBefore)
} finally {
await pool.query("UPDATE mcp_plans SET state='awaiting-approval',approved_by=NULL,approved_at=NULL WHERE id=$1", [planId])
}
await approvePlan(planId, writer, true)
}
for (const scenario of ["transition", "no-op", "ambiguous", "baseline-drift", "public-manual-rev2"] as const) {
const manual = scenario === "public-manual-rev2" const manual = scenario === "public-manual-rev2"
const effectCase = manual ? "transition" : scenario const effectCase = manual || scenario === "baseline-drift" ? "transition" : scenario
reset(); await refresh(); originalEffectCase = effectCase reset(); await refresh(); originalEffectCase = effectCase
const config = profile("a") const config = profile("a")
for (const candidate of config.candidates) candidate.controlProbe!.sourceAddress = "198.51.100.1"
assert.equal(config.autoRecovery, false) assert.equal(config.autoRecovery, false)
// The baseline and persistent authority are issued by the SAME actual // The baseline and persistent authority are issued by the SAME actual
// fixed readers/CAS used by existing tests, not hand-written proofs. // fixed readers/CAS used by existing tests, not hand-written proofs.
const persistent = manual ? undefined : await persistentFixture(config) const persistent = manual ? undefined : await persistentFixture(config)
try { try {
const { plan, input } = await prepare("a", persistent, config, manual) const { plan, input, evidence } = await prepare("a", persistent, config, manual, manual ? delegatedAdmin : scenario === "transition" ? null : writer)
if (scenario === "transition") await assertOwnerApproval(plan.planId)
assert.equal(input.manualEffectPolicy, manual ? "observational-v1" : undefined) assert.equal(input.manualEffectPolicy, manual ? "observational-v1" : undefined)
if (manual) assert.equal(input.persistentProfileAuthority, undefined) if (manual) assert.equal(input.persistentProfileAuthority, undefined)
const sealedSelection = originalEffectSelectionSchema.parse(input.originalEffectSelection) const sealedSelection = originalEffectSelectionSchema.parse(input.originalEffectSelection)
assert.equal(sealedSelection.status, effectCase === "ambiguous" ? "unknown" : "prepared") assert.equal(sealedSelection.status, effectCase === "ambiguous" ? "unknown" : "prepared")
const storedPlan = await loadPlan(plan.planId) const storedPlan = await loadPlan(plan.planId)
assert.equal(storedPlan.approved_by, writer.id) assert.equal(storedPlan.approved_by, manual ? delegatedAdmin.id : writer.id)
if (manual) assert.equal(evidence.validUntil, undefined, "Manual scoped plans keep their existing preparation lifetime")
if (scenario === "transition") {
type ProbeRow = { created_at: Date; evidence: { observedAt: string } }
const probe: ProbeRow = (await pool.query<ProbeRow>(
"SELECT created_at,evidence FROM mcp_path_checks WHERE id=$1 AND connection_id=$2", [input.pathCheckId, connectionId])).rows[0]
const probeDeadline = Math.min(probe.created_at.getTime(), Date.parse(probe.evidence.observedAt)) + 90_000
const authorityDeadline = Date.parse(input.persistentProfileAuthority.packet.validUntil)
assert(probeDeadline < authorityDeadline, "The fixture must exercise the probe cap, not the authority cap")
assert.equal(evidence.validUntil, new Date(probeDeadline).toISOString())
assert.equal(storedPlan.expires_at.getTime(), probeDeadline)
// A newer successful observation cannot extend an already approved plan.
const newerProbe = await invokeMcpTool(principal, "mm_route_path_verify", { profile: config, candidateId: "b" }) as { id: string; status: string; observedAt: string }
assert.equal(newerProbe.status, "passed")
assert.notEqual(newerProbe.id, input.pathCheckId)
assert(Date.parse(newerProbe.observedAt) + 90_000 > probeDeadline + 1)
const nativeBefore = { requests, writes }, admissionBefore = await snapshotApprovalAndAdmission(plan.planId)
const refuseAfterProbeDeadline = async (code: string) => {
const realNow = Date.now
try {
Date.now = () => probeDeadline + 1
await assert.rejects(applyPlan(principal, plan.planId, randomUUID()), (error: unknown) => error instanceof McpError && error.code === code)
} finally { Date.now = realNow }
assert.deepEqual(await snapshotApprovalAndAdmission(plan.planId), admissionBefore)
assert.deepEqual({ requests, writes }, nativeBefore)
assert.equal((await loadPlan(plan.planId)).state, "approved")
}
await refuseAfterProbeDeadline("PLAN_EXPIRED")
// The old deployment stored only the authority deadline in evidence.
const legacyValidity = input.persistentProfileAuthority.packet.validUntil as string
const legacyEvidence = { sealed: await encryptSecret(JSON.stringify({ ...evidence, validUntil: legacyValidity })) }
await pool.query("UPDATE mcp_plans SET expires_at=$2,evidence=$3::jsonb WHERE id=$1", [plan.planId, legacyValidity, JSON.stringify(legacyEvidence)])
try {
await refuseAfterProbeDeadline("MEASUREMENT_UNKNOWN")
} finally {
await pool.query("UPDATE mcp_plans SET expires_at=$2,evidence=$3::jsonb WHERE id=$1", [plan.planId, storedPlan.expires_at, JSON.stringify(storedPlan.evidence)])
}
cases.push({ case: "expired-approved-probe", newerProbeCannotRenew: true, scopedPlanCode: "PLAN_EXPIRED", legacyPlanCode: "MEASUREMENT_UNKNOWN", nativeRequests: 0, nativeWrites: 0, admissions: 0 })
}
const publicProjection = JSON.stringify(await publicPlan(storedPlan)) const publicProjection = JSON.stringify(await publicPlan(storedPlan))
assert(!publicProjection.includes("originalEffectSelection")) assert(!publicProjection.includes("originalEffectSelection"))
assert(!publicProjection.includes("manualEffectPolicy")) assert(!publicProjection.includes("manualEffectPolicy"))
const beforeWrites = writes, wireStart = originalEffectWire.length const beforeWrites = writes, wireStart = originalEffectWire.length
const configurationIntents = async () => (await pool.query(`SELECT count(*)::int AS n FROM mcp_audit
WHERE connection_id=$1 AND action='configuration.intent'`, [connectionId])).rows[0].n
const intentsBefore = await configurationIntents()
if (scenario === "baseline-drift") originalEffectCase = "no-op"
const operation = await execute(plan.planId) const operation = await execute(plan.planId)
assert.equal(operation.state, "applied", "Optional effect unknown must preserve genuine manual configuration/probe acceptance") if (scenario === "ambiguous" || scenario === "baseline-drift") {
assert.equal(operation.state, "unknown")
const { rows: [refusal] } = await pool.query("SELECT error_code FROM mcp_operations WHERE id=$1", [operation.id])
assert.equal(refusal.error_code, "MEASUREMENT_UNKNOWN")
assert.equal(writes, beforeWrites)
assert.equal(filters[0].rule, initial)
assert.equal(await configurationIntents(), intentsBefore)
const receiptCount: number = (await pool.query<{ n: number }>(`SELECT count(*)::int AS n FROM mcp_audit
WHERE connection_id=$1 AND action='routing.scoped.attempt' AND payload->>'receiptId'=$2`, [connectionId, input.receiptId])).rows[0].n
assert.equal(receiptCount, 0)
const runningWire = originalEffectWire.slice(wireStart)
assert.equal(runningWire.filter(row => row.method === "GET" && row.path === "/routing/route").length, scenario === "baseline-drift" ? 1 : 0)
cases.push({ case: scenario, state: operation.state, errorCode: refusal.error_code, nativeWrites: 0, attemptReceipts: 0 })
continue
}
assert.equal(operation.state, "applied")
assert.equal(writes - beforeWrites, 1) assert.equal(writes - beforeWrites, 1)
const rows: Array<{ created_at: Date; state: string; operation_id: string; operation_version: number; step_state: string; const rows: Array<{ created_at: Date; state: string; operation_id: string; operation_version: number; step_state: string;
result: { configurationVerified: boolean; controlProbeVerified: boolean; originalEffects: unknown } }> = (await pool.query(`SELECT o.created_at,o.state,p.operation_id,p.operation_version,s.state AS step_state,s.result result: { configurationVerified: boolean; controlProbeVerified: boolean; originalEffects: unknown } }> = (await pool.query(`SELECT o.created_at,o.state,p.operation_id,p.operation_version,s.state AS step_state,s.result
@@ -293,8 +482,8 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
console.log("Owned original effect diagnostic:", JSON.stringify({ case: scenario, state: operation.state, console.log("Owned original effect diagnostic:", JSON.stringify({ case: scenario, state: operation.state,
status: report.status, reason: report.reason, rounds: report.rounds.length, budget: report.budget ?? null, status: report.status, reason: report.reason, rounds: report.rounds.length, budget: report.budget ?? null,
ribBefore, ribAfter, runtimeNativeRequests: runningWire.length, lastNativePaths: runningWire.slice(-8).map(row => row.path) })) ribBefore, ribAfter, runtimeNativeRequests: runningWire.length, lastNativePaths: runningWire.slice(-8).map(row => row.path) }))
assert.equal(ribBefore, effectCase === "ambiguous" ? 0 : 1) assert.equal(ribBefore, 1)
assert.equal(ribAfter, effectCase === "ambiguous" ? 0 : 2) assert.equal(ribAfter, 2)
assert.equal(report.status, effectCase === "transition" ? "observed-expected" : "unknown") assert.equal(report.status, effectCase === "transition" ? "observed-expected" : "unknown")
assert.equal(report.switchEffectObserved, effectCase === "transition") assert.equal(report.switchEffectObserved, effectCase === "transition")
assert.equal(report.logCoverage, "unknown"); assert.equal(report.routeOriginVerified, false) assert.equal(report.logCoverage, "unknown"); assert.equal(report.routeOriginVerified, false)
@@ -353,24 +542,95 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
assert.equal(repeated.state, "applied") assert.equal(repeated.state, "applied")
assert.equal(Number((await pool.query("SELECT count(*) AS n FROM mcp_operations WHERE plan_id=$1", [plan.planId])).rows[0].n), 1) assert.equal(Number((await pool.query("SELECT count(*) AS n FROM mcp_operations WHERE plan_id=$1", [plan.planId])).rows[0].n), 1)
assert.equal(writes - beforeWrites, 1); assert.equal(requests, repeatReads, "Completed approved plan cannot repeat native work") assert.equal(writes - beforeWrites, 1); assert.equal(requests, repeatReads, "Completed approved plan cannot repeat native work")
// Preserve the existing scoped-controller activation guard. A real // Link the applied operation as the controller does when it issues
// tick pauses HERE before effect dispatch; never call this an // a plan. This checks both outcomes at the real worker boundary.
// effect-triggered scheduler pause or enable that unsupported lane. 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() await tickRoutingProfiles()
const paused = (await pool.query("SELECT enabled,state FROM mcp_routing_profiles WHERE id=$1", [persistent.id])).rows[0] const completed = (await pool.query("SELECT enabled,state FROM mcp_routing_profiles WHERE id=$1", [persistent.id])).rows[0]
assert.equal(paused.enabled, false); assert.equal(typeof paused.state.pausedReason, "string") if (effectCase === "transition") {
assert.deepEqual(paused.state.baseline, row.state.baseline) assert.equal(completed.enabled, true)
assert.equal(completed.state.baseline.packet.transition.operationId, operation.id)
assert.equal(completed.state.expectedCandidateId, "b")
} else {
assert.equal(completed.enabled, false)
assert.equal(completed.state.pausedReason, "STATE_CONFLICT")
assert.deepEqual(completed.state.baseline, row.state.baseline)
}
cases.push({ case: effectCase, manualState: operation.state, configurationCheckpoint: true, actualPrivateExecutionOrigin: true, cases.push({ case: effectCase, manualState: operation.state, configurationCheckpoint: true, actualPrivateExecutionOrigin: true,
effectStatus: report.status, observedFibTransition: report.switchEffectObserved, nativeRibBefore: ribBefore, nativeRibAfter: ribAfter, effectStatus: report.status, observedFibTransition: report.switchEffectObserved, nativeRibBefore: ribBefore, nativeRibAfter: ribAfter,
sharedBudget: report.budget ?? null, effectBaselineAdvanceAccepted: effectCase === "transition", effectGateNativeReads: effectCase === "transition" ? "fresh-native-checked" : 0, sharedBudget: report.budget ?? null, effectBaselineAdvanceAccepted: effectCase === "transition", effectGateNativeReads: effectCase === "transition" ? "fresh-native-checked" : 0,
actualTickPausedByExistingScopedGuard: true, tickEffectPauseAccepted: false, publicManualRevision2EffectOwnerAccepted: false }) tickEffectPauseAccepted: effectCase !== "transition", publicManualRevision2EffectOwnerAccepted: false })
} finally { if (persistent) await disablePersistent(persistent.id) } } finally { if (persistent) await disablePersistent(persistent.id) }
} }
assert(publicManualRevision2) assert(publicManualRevision2)
reset(); await refresh(); originalEffectCase = "transition"; exposeEffectCatalogue = true
const controllerConfig = profile("a")
controllerConfig.confirmationSamples = 1
controllerConfig.weights = { ping: 100, speed: 0, loss: 0 }
controllerConfig.candidates[1].downstreamInterface = "target"
const fast = randomUUID()
await pool.query("UPDATE mcp_routing_observations SET ping_ms=100 WHERE src_server_id=$1 AND routing_table='tenant-a' AND src_interface='hop'", [sid])
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,'target','peer','ipv4','tenant-a',1,0,100,100,now(),now(),true,'fixture')", [fast, sid])
await refresh()
const activeProfileId = randomUUID(), beforeControllerWrites = writes
persistentProfileIds.push(activeProfileId)
const saveProfile = (config: typeof controllerConfig) => invokeMcpTool(principal, "mm_route_profile_save_plan", {
profileId: activeProfileId, profile: config, enabled: true, expiresAt: new Date(Date.now() + 600000).toISOString(),
}) as Promise<{ planId: string }>
for (const invalid of [
{ ...controllerConfig, bgpContext: undefined },
{ ...controllerConfig, autoRecovery: true },
{ ...controllerConfig, candidates: controllerConfig.candidates.map(candidate => ({ ...candidate, controlProbe: { ...candidate.controlProbe!, sourceAddress: undefined } })) },
]) {
await assert.rejects(saveProfile(invalid), (error: unknown) => error instanceof McpError && error.code === "UNSUPPORTED_CAPABILITY")
}
const savedProfile = await saveProfile(controllerConfig)
assert.equal((await loadPlan(savedProfile.planId)).state, "awaiting-approval")
await approvePlan(savedProfile.planId, writer, true)
assert.equal((await execute(savedProfile.planId)).state, "applied")
const readController = async () => (await pool.query("SELECT enabled,state FROM mcp_routing_profiles WHERE id=$1", [activeProfileId])).rows[0]
const enrolled = await readController()
assert.equal(enrolled.enabled, true); assert.equal(enrolled.state.baseline.packet.candidateId, "a")
controllerProbeUnknown = true
try { await tickRoutingProfiles() } finally { controllerProbeUnknown = false }
const waitingForProbe = await readController()
assert.equal(waitingForProbe.enabled, true, String(waitingForProbe.state.pausedReason))
assert.equal(waitingForProbe.state.planId, undefined)
assert.equal(waitingForProbe.state.pendingCandidateId, "b", String(waitingForProbe.state.reason))
assert.equal(writes, beforeControllerWrites)
// Let the next tick run without waiting for the production polling interval.
await pool.query("UPDATE mcp_routing_profiles SET state=state-'probedAt' WHERE id=$1", [activeProfileId])
await tickRoutingProfiles()
const proposed = await readController()
assert.equal(proposed.enabled, true, String(proposed.state.pausedReason))
assert.equal(proposed.state.candidateId, "b", String(proposed.state.reason))
const switchPlanId: string = proposed.state.planId
assert.equal((await loadPlan(switchPlanId)).state, "awaiting-approval")
await tickRoutingProfiles()
assert.equal(writes, beforeControllerWrites)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_operations WHERE plan_id=$1", [switchPlanId])).rows[0].n, 0)
await approvePlan(switchPlanId, writer, true)
await tickRoutingProfiles()
const pendingSwitch = await readController()
const switchOperationId: string = pendingSwitch.state.operationId
assert.ok(switchOperationId, String(pendingSwitch.state.pausedReason))
assert.equal(await waitForOperation(switchOperationId), "applied")
await tickRoutingProfiles()
const switched = await readController()
assert.equal(switched.enabled, true, String(switched.state.pausedReason))
assert.equal(switched.state.planId, undefined)
assert.equal(switched.state.expectedCandidateId, "b")
assert.equal(switched.state.baseline.packet.candidateId, "b")
assert.equal(switched.state.baseline.packet.previousProof, enrolled.state.baseline.proof)
assert.equal(switched.state.baseline.packet.transition.operationId, switchOperationId)
assert.equal(switched.state.baseline.packet.transition.planId, switchPlanId)
assert.equal(writes - beforeControllerWrites, 1)
await disablePersistent(activeProfileId)
const boundedReserveFailover = await acceptControllerFailover()
assert.deepEqual((await pool.query("SELECT * FROM filter_rules WHERE server_id=$1 ORDER BY id", [sid])).rows, dbBefore) assert.deepEqual((await pool.query("SELECT * FROM filter_rules WHERE server_id=$1 ORDER BY id", [sid])).rows, dbBefore)
const result = { accepted: true, cases, publicManualRevision2, persistentAuthorityCases: 0, realMcpDispatcher: true, realSql: true, routerOsTransport: "declared-synthetic-localhost-http", const result = { accepted: true, cases, publicManualRevision2, boundedReserveFailover, persistentAuthorityCases: 0, realMcpDispatcher: true, realSql: true, routerOsTransport: "declared-synthetic-localhost-http",
genuineApprovedRunningOrigin: true, actualConfigurationCheckpoint: true, signedEffectTamperRejected: true, genuineApprovedRunningOrigin: true, actualConfigurationCheckpoint: true, signedEffectTamperRejected: true,
originalAutoRecoveryEnabled: false, fullRoutingAccepted: false, scopedControllerEnabled: false, nativeChrAccepted: false, forwardingVerified: false } originalAutoRecoveryEnabled: false, fullRoutingAccepted: false, scopedControllerEnabled: true, publicScopedControllerLifecycle: true, nativeChrAccepted: false, forwardingVerified: false }
console.log("Original effect SQL/runtime component:", JSON.stringify(result)) console.log("Original effect SQL/runtime component:", JSON.stringify(result))
return result return result
} }
@@ -423,8 +683,9 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
await assertOriginalCredentialRevocation() await assertOriginalCredentialRevocation()
return {accepted:true,originalCredentialRevocation:true,unauthorized401:true,fullAdmissionRollback:true,nativeReads:0,nativeWrites:0,realSql:true,allWriterLedgerComplete:false} return {accepted:true,originalCredentialRevocation:true,unauthorized401:true,fullAdmissionRollback:true,nativeReads:0,nativeWrites:0,realSql:true,allWriterLedgerComplete:false}
} }
if (options.manualInverseChargeOnly) { if (options.manualInverseChargeOnly || options.lateInverseAuthorityChange) {
const chargeCase = options.manualInverseChargeCase ?? "positive" const chargeCase = options.manualInverseChargeCase ?? "positive"
if (options.lateInverseAuthorityChange) assert.equal(chargeCase, "positive")
const persistent = chargeCase !== "nonpersistent" const persistent = chargeCase !== "nonpersistent"
// Same genuine fixed-reader baseline/CAS used by the existing persistent // Same genuine fixed-reader baseline/CAS used by the existing persistent
// acceptance below. No hand-written baseline, sealed plan or authority. // acceptance below. No hand-written baseline, sealed plan or authority.
@@ -520,10 +781,20 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
const recovery = await invokeMcpTool(principal, "mm_operation_recovery_plan", { operationId: source.id }) as { planId: string; state: string } const recovery = await invokeMcpTool(principal, "mm_operation_recovery_plan", { operationId: source.id }) as { planId: string; state: string }
assert.equal(recovery.state, "awaiting-approval") assert.equal(recovery.state, "awaiting-approval")
assert.equal((await loadPlan(recovery.planId)).operation_id, "operations.recover") assert.equal((await loadPlan(recovery.planId)).operation_id, "operations.recover")
assert.equal((await loadPlan(recovery.planId)).operation_version, 7) assert.equal((await loadPlan(recovery.planId)).operation_version, 8)
await assert.rejects(applyPlan(principal, recovery.planId, randomUUID())) await assert.rejects(applyPlan(principal, recovery.planId, randomUUID()))
assert.deepEqual(await snapshot(), afterForward, "No approval means no debit/charge/native write") assert.deepEqual(await snapshot(), afterForward, "No approval means no debit/charge/native write")
await approvePlan(recovery.planId, writer, true) await approvePlan(recovery.planId, writer, true)
if (chargeCase === "positive" && !options.lateInverseAuthorityChange) {
await pool.query("UPDATE mcp_plans SET operation_version=7 WHERE id=$1", [recovery.planId])
try {
await assert.rejects(applyPlan(principal, recovery.planId, randomUUID()), { code: "PLAN_EXPIRED" })
assert.equal((await pool.query("SELECT id FROM mcp_operations WHERE plan_id=$1", [recovery.planId])).rowCount, 0)
assert.deepEqual(await snapshot(), afterForward, "Superseded approved inverse cannot create an operation, debit or native write")
} finally {
await pool.query("UPDATE mcp_plans SET operation_version=8 WHERE id=$1", [recovery.planId])
}
}
if (chargeCase === "mixed-forward") { if (chargeCase === "mixed-forward") {
assert(competingForward) assert(competingForward)
const attempts = [ const attempts = [
@@ -573,6 +844,48 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
realSql: true, allWriterLedgerComplete: false, reservedDispatchEnabled: false, autoRecoveryEnabled: false } realSql: true, allWriterLedgerComplete: false, reservedDispatchEnabled: false, autoRecoveryEnabled: false }
} }
console.log(JSON.stringify({ stage: "manual-inverse:human-recovery-apply" })) console.log(JSON.stringify({ stage: "manual-inverse:human-recovery-apply" }))
const lateChange = options.lateInverseAuthorityChange
if (lateChange) {
const priorIntent = (await pool.query("SELECT coalesce(max(id),0)::text AS id FROM mcp_audit WHERE connection_id=$1 AND action='configuration.intent'", [connectionId])).rows[0].id
let writeId: string | undefined
afterIntent = async () => {
const intents = (await pool.query(`SELECT payload FROM mcp_audit WHERE connection_id=$1 AND id>$2::bigint
AND action='configuration.intent' AND payload->>'path'=$3`,
[connectionId, priorIntent, "/routing/filter/rule/" + loaded.configuration.packet.nativeId])).rows
assert.equal(intents.length, 1)
assert.equal(intents[0].payload.method, "PATCH")
assert.equal(intents[0].payload.serverId, sid)
writeId = intents[0].payload.writeId
assert.match(writeId ?? "", /^[0-9a-f-]{36}$/)
const sql = {
edit: "UPDATE mcp_routing_profiles SET version=version+1,config=jsonb_set(config,'{name}','\"late inverse edit\"') WHERE id=$1",
disable: "UPDATE mcp_routing_profiles SET enabled=false WHERE id=$1",
grant: "UPDATE mcp_routing_profiles SET grant_expires_at=clock_timestamp()-interval '1 second' WHERE id=$1",
}[lateChange]
assert.equal((await pool.query(sql, [id])).rowCount, 1)
}
const inverse = await execute(recovery.planId)
assert.equal(afterIntent, undefined)
assert.ok(writeId)
assert.notEqual(inverse.state, "applied")
assert.equal(writes, afterForward.writes)
assert.equal(inversePostChecks, 0)
const auditRows = (await pool.query(`SELECT action FROM mcp_audit WHERE connection_id=$1 AND payload->>'writeId'=$2
ORDER BY id`, [connectionId, writeId])).rows
assert.deepEqual(auditRows.map(row => row.action), ["configuration.intent", "configuration.unconfirmed"])
const refusal = (await pool.query(`SELECT payload->>'phase' AS phase, payload->>'code' AS code
FROM mcp_audit WHERE connection_id=$1 AND action='routing.scoped.recovery-unconfirmed'`, [connectionId])).rows
assert.deepEqual(refusal, [{ phase: "before-dispatch-context", code: "STATE_CONFLICT" }])
assert.deepEqual((await pool.query(`SELECT state,verified_sealed FROM mcp_scoped_recovery_intents
WHERE source_operation_id=$1 AND recovery_operation_id=$2`, [source.id, inverse.id])).rows,
[{ state: "unknown", verified_sealed: null }])
assert.equal((await pool.query(`SELECT count(*)::int AS n FROM mcp_operation_checkpoints
WHERE operation_id=$1 AND configuration_sealed IS NOT NULL`, [inverse.id])).rows[0].n, 0)
assert.equal((await pool.query("SELECT error_code FROM mcp_operations WHERE id=$1", [inverse.id])).rows[0].error_code,
"MUTATION_OUTCOME_UNKNOWN")
return { accepted: true, lateInverseAuthorityChange: lateChange, durableIntents: 1, inverseNativeWrites: 0,
realSql: true, realMcpDispatcher: true, fullInverseLifecycleRepeated: false }
}
if (chargeCase === "quota" || chargeCase === "revoked") { if (chargeCase === "quota" || chargeCase === "revoked") {
if (chargeCase === "revoked") await pool.query("UPDATE mcp_routing_profiles SET enabled=false WHERE id=$1", [id]) if (chargeCase === "revoked") await pool.query("UPDATE mcp_routing_profiles SET enabled=false WHERE id=$1", [id])
const beforeOperations = (await pool.query("SELECT count(*)::int AS n FROM mcp_operations WHERE connection_id=$1", [connectionId])).rows[0].n const beforeOperations = (await pool.query("SELECT count(*)::int AS n FROM mcp_operations WHERE connection_id=$1", [connectionId])).rows[0].n
@@ -605,9 +918,14 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
assert.equal(afterInverse.forward, persistent ? 1 : 0); assert.equal(afterInverse.manualInverse, persistent ? 1 : 0); assert.equal(afterInverse.total, persistent ? 2 : 0) assert.equal(afterInverse.forward, persistent ? 1 : 0); assert.equal(afterInverse.manualInverse, persistent ? 1 : 0); assert.equal(afterInverse.total, persistent ? 2 : 0)
assert.equal(afterInverse.reserved, 0); assert.equal(afterInverse.legacy, 0) assert.equal(afterInverse.reserved, 0); assert.equal(afterInverse.legacy, 0)
assert.equal(afterInverse.writes, 2); if (chargeCase !== "unknown") assert.equal(inversePostChecks, 1) assert.equal(afterInverse.writes, 2); if (chargeCase !== "unknown") assert.equal(inversePostChecks, 1)
const charges = (await pool.query("SELECT recovery_operation_id,recovery_plan_id,source_operation_id,profile_id,charge_units FROM mcp_routing_manual_inverse_charges WHERE source_operation_id=$1", [source.id])).rows const charges = (await pool.query("SELECT recovery_operation_id,recovery_plan_id,source_operation_id,profile_id,charge_units,binding_sealed FROM mcp_routing_manual_inverse_charges WHERE source_operation_id=$1", [source.id])).rows
assert.equal(charges.length, persistent ? 1 : 0); assert.equal(charges.length, persistent ? 1 : 0);
if (persistent) { assert.equal(charges[0].recovery_operation_id, inverse.id); assert.equal(charges[0].recovery_plan_id, inverse.plan_id); assert.equal(charges[0].profile_id,id); assert.equal(charges[0].charge_units,1) } if (persistent) {
assert.equal(charges[0].recovery_operation_id, inverse.id); assert.equal(charges[0].recovery_plan_id, inverse.plan_id); assert.equal(charges[0].profile_id,id); assert.equal(charges[0].charge_units,1)
const binding = JSON.parse(await decryptSecret(charges[0].binding_sealed))
assert.equal(binding.operationVersion, 8)
assert.equal(binding.sourceVersion, 7)
}
const intent = (await pool.query("SELECT state,verified_sealed,recovery_operation_id,authority_profile_id,authority_profile_version,reservation_id FROM mcp_scoped_recovery_intents WHERE source_operation_id=$1 AND source_step_index=0", [source.id])).rows const intent = (await pool.query("SELECT state,verified_sealed,recovery_operation_id,authority_profile_id,authority_profile_version,reservation_id FROM mcp_scoped_recovery_intents WHERE source_operation_id=$1 AND source_step_index=0", [source.id])).rows
assert.equal(intent.length, 1); assert.equal(intent[0].state, chargeCase === "unknown" ? "unknown" : "verified"); assert.equal(intent[0].recovery_operation_id, inverse.id) assert.equal(intent.length, 1); assert.equal(intent[0].state, chargeCase === "unknown" ? "unknown" : "verified"); assert.equal(intent[0].recovery_operation_id, inverse.id)
assert.equal(intent[0].authority_profile_id, null); assert.equal(intent[0].authority_profile_version, persistent ? 1 : null); assert.equal(intent[0].reservation_id, null) assert.equal(intent[0].authority_profile_id, null); assert.equal(intent[0].authority_profile_version, persistent ? 1 : null); assert.equal(intent[0].reservation_id, null)
@@ -659,156 +977,59 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
if (options.forwardRoundsOnly) { if (options.forwardRoundsOnly) {
const accepted: string[] = [] const accepted: string[] = []
for (const scenario of ["two-rounds", "native-drift", "checkpoint-tamper", "late-revoke", "row-budget"] as const) { for (const scenario of ["two-rounds", "native-drift", "checkpoint-tamper", "late-revoke", "row-budget"] as const) {
console.log("Routing forward rounds stage:", scenario, "prepare") reset(); await refresh()
filters.forEach(row => { row.rule = initial; row.opaque = "NEVER_SCOPED_OPAQUE" }) const persistent = await persistentFixture(), { plan, input } = await prepare("a", persistent)
pendingPostCheckpoint = false; pendingInversePost = false; forwardRoundBulk = 0 const beforeWrites = writes, wireStart = originalEffectWire.length
await pool.query("UPDATE mcp_routing_observations SET ping_at=now(),bandwidth_at=now(),loss_pct=0 WHERE src_server_id=$1", [sid]) let faultReached = false
for (const row of (await pool.query("SELECT id FROM mcp_routing_observations WHERE src_server_id=$1", [sid])).rows) await attestRoutingFixture(row.id)
// Same synthetic persistent authority fixture used by the original
// acceptance. CONFIG_APPLIED itself is produced ONLY by real runtime.
const profileId = randomUUID(), current = await effectiveMcpConnection(principal)
const expiry = new Date(Math.min(Date.now() + 600000, current.owner_valid_until.getTime(), Date.parse(principal.credentialExpiresAt), Date.parse(current.policy.expiresAt)))
// Explicit trusted test-host enrollment only. Public nonmain activation
// stays disabled. Real human-principal fixed native issuer and normal CAS
// supply the private baseline; no MAC/checkpoint is manufactured here.
await withApplicationMutationLock(() => withConfigMutationLock([sid], async () => {
const baseline=await prepareRoutingControllerBaseline(profileId,1,profile("a"),expiry.toISOString(),{principal,connection:current})
const saved=await saveControllerProfileCas(pool,{profileId,connectionId,expectedVersion:null,profile:profile("a"),enabled:true,expiresAt:expiry.toISOString(),credentialId:principal.credentialId,generation:await accessGeneration(),policyVersion:current.policy.version,baseline})
assert.equal(saved?.version,1)
}))
persistentProfileIds.push(profileId)
const { plan } = await prepare("a", { id: profileId, version: 1 }), beforeWrites = writes
let checked = false, callbackStage = "not-entered"
const startedAt = Date.now()
const sourceDiagnostic = async (operationId: string, stage: string) => {
const row = (await pool.query(`SELECT o.state,o.error_code,s.state AS step_state,s.result IS NULL AS result_null,
p.operation_id,p.operation_version,p.approved_by IS NOT NULL AS approved,
o.created_at,p.expires_at FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id
JOIN mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=0 WHERE o.id=$1`, [operationId])).rows[0]
console.log("Routing forward rounds source:", JSON.stringify({ scenario, stage, elapsedMs: Date.now() - startedAt, ...row }))
return row
}
const observeConfiguration = async (operationId: string) => {
callbackStage = "outside-http-before-constructor"
const source = await sourceDiagnostic(operationId, callbackStage)
const action = { principal, connection: await effectiveMcpConnection(principal) }
const session = await createRoutingNetworkNativeSession({ sourceOperationId: operationId, context: action,
authorize: async () => {}, deadline: Math.min(source.created_at.getTime() + 115000, Date.now() + 90000), manifest: { servers: [sid], probes: [] } })
await sourceDiagnostic(operationId, "after-constructor")
let configuration: Awaited<ReturnType<typeof resolveRoutingNetworkForwardRoundConfigurationContext>> | undefined
const saved = (await pool.query("SELECT configuration_proof FROM mcp_operation_checkpoints WHERE operation_id=$1", [operationId])).rows[0]
const originalRule = filters[0].rule
try { try {
callbackStage = "baseline-snapshot"
await session.readSnapshot("before")
callbackStage = "resolve-configuration-capability"
configuration = await resolveRoutingNetworkForwardRoundConfigurationContext(session.sharedContext, action)
await assert.rejects(resolveRoutingNetworkForwardRoundConfigurationContext(session.sharedContext, { ...action, principal: { ...principal, connectionId: randomUUID() } }))
// The previous mismatched-principal admission does not own/close
// this session; the genuine capability is still the sole owner.
if (scenario === "row-budget") forwardRoundBulk = 1000 if (scenario === "row-budget") forwardRoundBulk = 1000
callbackStage = "first-bound-round" const operation = await executeWithCheckpointObservation(plan.planId, async operationId => {
const first = await session.readConfigurationBoundForwardSnapshot(configuration) const reads = requests, connection = await effectiveMcpConnection(principal)
assert.equal(first.round, 1); assert.equal(first.securityProof, false); assert.equal(first.forwardingVerified, false) await assert.rejects(resolveRoutingNetworkConfigurationContext(operationId, {
assert.equal(first.logCoverage, "unknown"); assert.equal(first.configurationReadbacks.before.operationId, operationId) principal: { ...principal, connectionId: randomUUID() }, connection,
assert.equal(first.configurationReadbacks.after.sharedNetworkBudgetVerified, true) }))
assert.equal(first.budget.calls, session.readBudget().calls); assert.equal(first.budget.rows, session.readBudget().rows) assert.equal(requests, reads, "A mismatched principal cannot read native state")
if (["native-drift", "checkpoint-tamper", "late-revoke"].includes(scenario)) afterRoundLogRead = async () => { if (scenario !== "two-rounds") afterRoundLogRead = async () => {
faultReached = true
if (scenario === "native-drift") filters[0].rule = "reject;" if (scenario === "native-drift") filters[0].rule = "reject;"
if (scenario === "checkpoint-tamper") await pool.query("UPDATE mcp_operation_checkpoints SET configuration_proof=$2 WHERE operation_id=$1", [operationId, "0".repeat(64)]) if (scenario === "checkpoint-tamper") await pool.query("UPDATE mcp_operation_checkpoints SET configuration_proof=$2 WHERE operation_id=$1", [operationId, "0".repeat(64)])
if (scenario === "late-revoke") await pool.query("UPDATE mcp_routing_profiles SET enabled=false WHERE id=$1", [profileId]) if (scenario === "late-revoke") await disablePersistent(persistent.id)
} }
})
assert.equal(faultReached, scenario !== "two-rounds")
const wire = originalEffectWire.slice(wireStart)
const originalRib = wire.filter(row => row.method === "GET" && row.path === "/routing/route" && !row.afterPatch)
const forwardRib = wire.filter(row => row.method === "GET" && row.path === "/routing/route" && row.afterPatch)
assert.equal(originalRib.length, 1); assert.equal(writes, beforeWrites + 1)
const { rows: [terminal] } = await pool.query("SELECT o.error_code,s.result FROM mcp_operations o JOIN mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=0 WHERE o.id=$1", [operation.id])
if (scenario === "two-rounds" || scenario === "row-budget") { if (scenario === "two-rounds" || scenario === "row-budget") {
callbackStage = "second-bound-round" assert.equal(operation.state, "applied"); assert.equal(forwardRib.length, 2)
const second = await session.readConfigurationBoundForwardSnapshot(configuration) const report = originalEffectReportSchema.parse(terminal.result.originalEffects)
assert.equal(second.round, 2); assert(second.budget.calls > first.budget.calls) const network = await loadScopedNetworkCheckpoint(input, await assertScopedCommunityRequest(input))
assert.equal(second.budget.calls, session.readBudget().calls); assert.equal(second.budget.rows, session.readBudget().rows) assert(network); assert.equal(network.status, "passed"); assert.deepEqual(network.originalEffects, report)
if (scenario === "row-budget") { assert.equal(report.semanticProof, false); assert.equal(report.forwardingVerified, false); assert.equal(report.logCoverage, "unknown")
await assert.rejects(session.readConfigurationBoundForwardSnapshot(configuration), (error: unknown) => (error as { code?: string }).code === "LIMIT_EXCEEDED") if (scenario === "two-rounds") {
assert(session.readBudget().rows > 10000) assert.equal(report.status, "observed-expected"); assert.equal(report.rounds.length, 2)
assert(report.budget && report.budget.calls > 0 && report.budget.calls <= 256 && report.budget.rows <= 10000)
} else {
assert.equal(report.status, "unknown"); assert.equal(report.rounds.length, 0)
assert.equal(report.reason, "original-effect-rounds-unavailable:LIMIT_EXCEEDED")
assert.equal(report.budget, undefined)
} }
} else await assert.rejects(session.readConfigurationBoundForwardSnapshot(configuration)) } else {
if (scenario !== "two-rounds") { assert.equal(operation.state, "unknown"); assert.equal(forwardRib.length, 1)
assert.equal(session.readBudget().closed, true) assert.equal(terminal.error_code, "MUTATION_OUTCOME_UNKNOWN")
const stopped = requests
await assert.rejects(session.readSnapshot("after-forward")); assert.equal(requests, stopped)
} }
assert.equal(writes, beforeWrites + 1); checked = true; callbackStage = "checked" const readsBeforeReplay = requests
const replay = await applyPlan(principal, plan.planId, randomUUID())
assert.equal(replay.id, operation.id); assert.equal(requests, readsBeforeReplay); assert.equal(writes, beforeWrites + 1)
accepted.push(scenario)
console.log(JSON.stringify({ scenario, state: operation.state, errorCode: terminal.error_code, forwardRibReads: forwardRib.length, nativeWrites: 1 }))
} finally { } finally {
afterRoundLogRead = undefined; forwardRoundBulk = 0 afterRoundLogRead = undefined; forwardRoundBulk = 0
filters[0].rule = originalRule await disablePersistent(persistent.id)
await pool.query("UPDATE mcp_operation_checkpoints SET configuration_proof=$2 WHERE operation_id=$1", [operationId, saved.configuration_proof])
await pool.query("UPDATE mcp_routing_profiles SET enabled=true WHERE id=$1", [profileId])
if (configuration) closeRoutingNetworkForwardRoundConfigurationContext(configuration)
session.close()
} }
} }
// Capture only a real row lock inside the post-CONFIG_APPLIED HTTP
// callback, then return its response immediately. The long acquisition
// runs outside HTTP while the genuine generic runtime UPDATE waits.
let blocker: PoolClient | undefined, blockerPid: number | undefined, capturedOperationId: string | undefined, admissionError: unknown
afterConfigurationCheckpoint = async operationId => {
try {
blocker = await pool.connect()
await blocker.query("BEGIN"); await blocker.query("SET LOCAL lock_timeout='3s'")
const held = await blocker.query(`SELECT s.operation_id FROM mcp_operation_steps s JOIN mcp_operations o ON o.id=s.operation_id
WHERE s.operation_id=$1 AND s.step_index=0 AND o.state='running' AND s.state='intent' AND s.result IS NULL FOR UPDATE OF s`, [operationId])
assert.equal(held.rowCount, 1, "Only the genuine running intent with null result can be held")
blockerPid = Number((await blocker.query("SELECT pg_backend_pid() AS pid")).rows[0].pid)
capturedOperationId = operationId; callbackStage = "row-lock-captured-http-returning"
console.log("Routing forward rounds stage:", scenario, callbackStage, "elapsedMs", Date.now() - startedAt)
} catch (error) { admissionError = error }
}
console.log("Routing forward rounds stage:", scenario, "apply")
let operationOutcome: { ok: true; value: Awaited<ReturnType<typeof execute>> } | { ok: false; error: unknown } | undefined
const execution = execute(plan.planId).then(value => ({ ok: true as const, value }), error => ({ ok: false as const, error }))
let settled = false
void execution.then(() => { settled = true })
try {
const admissionDeadline = Date.now() + 60000
while (true) {
if (admissionError) throw admissionError
if (settled) {
const terminal = await execution
if (!terminal.ok) throw terminal.error
await sourceDiagnostic(terminal.value.id, "terminated-before-row-lock-observation")
assert.fail("Original operation ended before its genuine running intent could be observed")
}
if (capturedOperationId && blockerPid !== undefined) {
const waiters = await pool.query(`SELECT pid FROM pg_stat_activity WHERE datname=current_database() AND usename='mcp_test'
AND state='active' AND wait_event_type='Lock' AND query ~* '^UPDATE mcp_operation_steps SET state='
AND $1::int=ANY(pg_blocking_pids(pid))`, [blockerPid])
if (waiters.rowCount) break
}
assert(Date.now() < admissionDeadline, "Generic runtime did not reach its real source-step UPDATE behind the owned row lock")
await delay(20)
}
assert(capturedOperationId)
await sourceDiagnostic(capturedOperationId, "generic-step-update-blocked-no-http-pending")
await observeConfiguration(capturedOperationId)
} finally {
afterConfigurationCheckpoint = undefined
// A row lock is only an observation fixture. It never changes a
// state/result or renews authority, deadline, intent or checkpoint.
try {
if (blocker) { try { await blocker.query("ROLLBACK") } finally { blocker.release() } }
} finally { operationOutcome = await execution }
}
assert(operationOutcome)
if (!operationOutcome.ok) throw operationOutcome.error
const operation = operationOutcome.value
const diagnostic = (await pool.query(`SELECT o.state,o.error_code,o.verification,
EXISTS(SELECT 1 FROM mcp_operation_checkpoints c WHERE c.operation_id=o.id AND c.configuration_sealed IS NOT NULL) AS configuration_checkpoint,
ARRAY(SELECT s.state FROM mcp_operation_steps s WHERE s.operation_id=o.id ORDER BY s.step_index) AS step_states
FROM mcp_operations o WHERE o.id=$1`, [operation.id])).rows[0]
// Fixed stage labels, counters and generic persisted status only. Never
// log native bodies, endpoint credentials or encrypted plan contents.
console.log("Routing forward rounds terminal:", JSON.stringify({ scenario, callbackStage, checked,
writesSinceApply: writes - beforeWrites, ...diagnostic }))
assert.equal(checked, true); assert.equal(operation.state, "applied"); assert.equal(writes, beforeWrites + 1)
await pool.query("UPDATE mcp_routing_profiles SET enabled=false WHERE id=$1", [profileId])
accepted.push(scenario)
}
console.log(JSON.stringify({ forwardRoundCases: accepted, actualGenuineConfigApplied: true, sharedBudget: true, console.log(JSON.stringify({ forwardRoundCases: accepted, actualGenuineConfigApplied: true, sharedBudget: true,
securityProof: false, logCoverage: "unknown", forwardingVerified: false })) securityProof: false, logCoverage: "unknown", forwardingVerified: false }))
return { accepted: true, forwardRoundCases: accepted, securityProof: false, forwardingVerified: false } return { accepted: true, forwardRoundCases: accepted, securityProof: false, forwardingVerified: false }
@@ -1048,45 +1269,60 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
reset(); await refresh() reset(); await refresh()
{ {
const persistent = await persistentFixture(), { plan, input } = await prepare("a", persistent), before = writes const persistent = await persistentFixture(), { plan, input } = await prepare("a", persistent), before = writes
const op = await execute(plan.planId); assert.equal(op.state, "applied"); assert.equal(writes, before + 1) const checkBridge = async (operationId: string) => {
const action = { principal, connection: await effectiveMcpConnection(principal) }
const bridge = await resolveRoutingNetworkConfigurationContext(operationId, action)
const current = await consumeRoutingNetworkConfigurationContext(bridge)
assert.equal(current.operationId, operationId); assert.equal(current.securityProof, false); assert.equal(current.sharedNetworkBudgetVerified, false)
assert.equal(current.configuration.nativeId, input.nativeId); assert.equal(current.configuration.dependencyIdentity, input.dependencyProof)
const afterReads = requests
await assert.rejects(consumeRoutingNetworkConfigurationContext(bridge))
await assert.rejects(consumeRoutingNetworkConfigurationContext(JSON.parse(JSON.stringify(bridge))))
await assert.rejects(resolveRoutingNetworkConfigurationContext(operationId, action), /source already issued/)
assert.equal(requests, afterReads); assert.equal(writes, before + 1)
}
// The production forward rounds use the same one-time bridge issuance.
// Exercise the independent bridge at the committed checkpoint first.
const op = await executeWithCheckpointObservation(plan.planId, checkBridge)
assert.equal(op.state, "applied"); assert.equal(writes, before + 1)
const row = (await pool.query("SELECT c.configuration_sealed,s.result FROM mcp_operation_checkpoints c JOIN mcp_operation_steps s USING(operation_id,step_index) WHERE c.operation_id=$1", [op.id])).rows[0] const row = (await pool.query("SELECT c.configuration_sealed,s.result FROM mcp_operation_checkpoints c JOIN mcp_operation_steps s USING(operation_id,step_index) WHERE c.operation_id=$1", [op.id])).rows[0]
const packet = JSON.parse(await decryptSecret(row.configuration_sealed)) const packet = JSON.parse(await decryptSecret(row.configuration_sealed))
assert.equal(packet.revision, 2); assert.equal(row.result.proofRevision, 3) assert.equal(packet.revision, 2); assert.equal(row.result.proofRevision, 3)
assert.equal(fingerprint(packet.persistentProfileAuthority), fingerprint(input.persistentProfileAuthority)) assert.equal(fingerprint(packet.persistentProfileAuthority), fingerprint(input.persistentProfileAuthority))
assert.equal(packet.persistentProfileAuthority.packet.credentialId, principal.credentialId) assert.equal(packet.persistentProfileAuthority.packet.credentialId, principal.credentialId)
assert.equal((await reconcile(op.id)).matchesDesiredState, true) assert.equal((await reconcile(op.id)).matchesDesiredState, true)
const bridge = await resolveRoutingNetworkConfigurationContext(op.id, { principal, connection: await effectiveMcpConnection(principal) })
const current = await consumeRoutingNetworkConfigurationContext(bridge)
assert.equal(current.operationId, op.id); assert.equal(current.securityProof, false); assert.equal(current.sharedNetworkBudgetVerified, false)
assert.equal(current.configuration.nativeId, packet.nativeId); assert.equal(current.configuration.dependencyIdentity, packet.dependencyProof)
const afterReads = requests
await assert.rejects(consumeRoutingNetworkConfigurationContext(bridge))
await assert.rejects(consumeRoutingNetworkConfigurationContext(JSON.parse(JSON.stringify(bridge))))
assert.equal(requests, afterReads); assert.equal(writes, before + 1)
persistentCases.push("genuine-configuration-bridge-one-consume-no-clone-grant") persistentCases.push("genuine-configuration-bridge-one-consume-no-clone-grant")
await disablePersistent(persistent.id); persistentCases.push("persistent-forward-request3-checkpoint2") await disablePersistent(persistent.id); persistentCases.push("persistent-forward-request3-checkpoint2")
} }
for (const change of ["closed", "disabled", "checkpoint-tamper"] as const) { for (const change of ["closed", "disabled", "checkpoint-tamper"] as const) {
reset(); await refresh() reset(); await refresh()
const persistent = await persistentFixture(), { plan } = await prepare("a", persistent), before = writes const persistent = await persistentFixture(), { plan } = await prepare("a", persistent), before = writes
const op = await execute(plan.planId); assert.equal(op.state, "applied"); assert.equal(writes, before + 1) const op = await executeWithCheckpointObservation(plan.planId, async operationId => {
const bridge = await resolveRoutingNetworkConfigurationContext(op.id, { principal, connection: await effectiveMcpConnection(principal) }) const bridge = await resolveRoutingNetworkConfigurationContext(operationId, { principal, connection: await effectiveMcpConnection(principal) })
const { rows: [checkpoint] } = await pool.query("SELECT configuration_proof FROM mcp_operation_checkpoints WHERE operation_id=$1", [operationId])
try {
if (change === "closed") closeRoutingNetworkConfigurationContext(bridge) if (change === "closed") closeRoutingNetworkConfigurationContext(bridge)
if (change === "disabled") await disablePersistent(persistent.id) if (change === "disabled") await disablePersistent(persistent.id)
if (change === "checkpoint-tamper") await pool.query("UPDATE mcp_operation_checkpoints SET configuration_proof=$2 WHERE operation_id=$1", [op.id, "f".repeat(64)]) if (change === "checkpoint-tamper") await pool.query("UPDATE mcp_operation_checkpoints SET configuration_proof=$2 WHERE operation_id=$1", [operationId, "f".repeat(64)])
const beforeReads = requests const beforeReads = requests
await assert.rejects(consumeRoutingNetworkConfigurationContext(bridge)) await assert.rejects(consumeRoutingNetworkConfigurationContext(bridge))
assert.equal(requests, beforeReads); assert.equal(writes, before + 1) assert.equal(requests, beforeReads); assert.equal(writes, before + 1)
} finally {
if (change === "disabled") await pool.query("UPDATE mcp_routing_profiles SET enabled=true WHERE id=$1", [persistent.id])
if (change === "checkpoint-tamper") await pool.query("UPDATE mcp_operation_checkpoints SET configuration_proof=$2 WHERE operation_id=$1", [operationId, checkpoint.configuration_proof])
closeRoutingNetworkConfigurationContext(bridge)
}
})
assert.equal(op.state, "applied"); assert.equal(writes, before + 1)
await disablePersistent(persistent.id) await disablePersistent(persistent.id)
persistentCases.push("genuine-configuration-bridge-denies:" + change) persistentCases.push("genuine-configuration-bridge-denies:" + change)
} }
{ {
reset(); await refresh() reset(); await refresh()
const persistent = await persistentFixture(), { plan } = await prepare("a", persistent), before = writes const persistent = await persistentFixture(), { plan } = await prepare("a", persistent), before = writes
let sharedChecks = 0 // The production effect reader already owns this operation's native
// This is an out-of-band running-checkpoint check, not RouterOS ping work. // session. A second reader must fail before any native request.
// Do not put two full SQL/native reads inside the timed HTTP response. const op = await executeWithCheckpointObservation(plan.planId, async operationId => {
const checkpointObserver = observeCommittedScopedCheckpoint(pool, async operationId => {
assert.equal((await pool.query("SELECT plan_id FROM mcp_operations WHERE id=$1", [operationId])).rows[0]?.plan_id, plan.planId) assert.equal((await pool.query("SELECT plan_id FROM mcp_operations WHERE id=$1", [operationId])).rows[0]?.plan_id, plan.planId)
if(process.env.MMAPP_OWN_ROUTING_WORKER_REPRO==="true") { if(process.env.MMAPP_OWN_ROUTING_WORKER_REPRO==="true") {
await (await import("./workers.js")).tickMcpWorkers() await (await import("./workers.js")).tickMcpWorkers()
@@ -1095,23 +1331,22 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
console.log("Worker exclusion preserved exact running CONFIG_APPLIED profile") console.log("Worker exclusion preserved exact running CONFIG_APPLIED profile")
} }
const action = { principal, connection: await effectiveMcpConnection(principal) } const action = { principal, connection: await effectiveMcpConnection(principal) }
const session = await createRoutingNetworkNativeSession({ sourceOperationId: operationId, context: action, // Reuse the approved plan's original deadline: now +90s would extend
authorize: async () => {}, deadline: Date.now() + 90000, manifest: { servers: [sid], probes: [] } }) // its preparation-time probe horizon and mask the duplicate-owner guard.
try { const deadline = (await loadPlan(plan.planId)).expires_at.getTime()
const context = await resolveRoutingNetworkConfigurationContextFromSharedContext(session.sharedContext, action) const reads = requests
const result = await consumeRoutingNetworkConfigurationContext(context) await assert.rejects(createRoutingNetworkNativeSession({ sourceOperationId: operationId, context: action,
assert.equal(result.operationId, operationId); assert.equal(result.sharedNetworkBudgetVerified, true); assert.equal(result.securityProof, false) authorize: async () => {}, deadline, manifest: { servers: [sid], probes: [] } }), /Source native session уже создавалась/)
const budget = session.readBudget() assert.equal(requests, reads); assert.equal(writes, before + 1)
assert(budget.calls > 0 && budget.calls <= 256); assert(budget.bytes > 0 && budget.rows > 0) })
assert.equal(budget.closed, false); assert.equal(writes, before + 1) assert.equal(op.state, "applied"); assert.equal(writes, before + 1)
sharedChecks++ const { rows: [step] } = await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1 AND step_index=0", [op.id])
} finally { session.close() } const effects = originalEffectReportSchema.parse(step.result.originalEffects)
}, { databaseUrl: process.env.DATABASE_URL ?? "", stateDir: process.env.MCP_STATE_DIR ?? "" }) assert.equal(effects.status, "observed-expected")
try { assert.equal(effects.rounds.length, 2)
const op = await execute(plan.planId) assert.equal(effects.semanticProof, false)
checkpointObserver.assertCompleted() assert(effects.budget && effects.budget.calls > 0 && effects.budget.calls <= 256)
assert.equal(op.state, "applied"); assert.equal(sharedChecks, 1); assert.equal(writes, before + 1) assert(effects.budget.bytes > 0 && effects.budget.rows > 0)
} finally { checkpointObserver.close() }
await disablePersistent(persistent.id) await disablePersistent(persistent.id)
persistentCases.push("genuine-running-config-bridge-uses-one-shared-native-owner") persistentCases.push("genuine-running-config-bridge-uses-one-shared-native-owner")
} }
@@ -257,6 +257,7 @@ async function checkpoint(value: Session, intent: Intent, phase: Checkpoint["pha
export async function executeReservedScopedRecovery(allocation: RoutingReservedInverseAllocation, expected: { sourceOperationId: string; principal: McpPrincipal }) { export async function executeReservedScopedRecovery(allocation: RoutingReservedInverseAllocation, expected: { sourceOperationId: string; principal: McpPrincipal }) {
const store = createRoutingReservationStore(), spent = await store.beginInverseAttempt(allocation) const store = createRoutingReservationStore(), spent = await store.beginInverseAttempt(allocation)
const execution = spent.execution const execution = spent.execution
try {
const binding = await store.inspectInverseExecution(execution) const binding = await store.inspectInverseExecution(execution)
if (binding.sourceOperationId !== expected.sourceOperationId || fingerprint(binding.principal) !== fingerprint(expected.principal)) throw fail("Reserved runner identity differs") if (binding.sourceOperationId !== expected.sourceOperationId || fingerprint(binding.principal) !== fingerprint(expected.principal)) throw fail("Reserved runner identity differs")
const current = await effectiveMcpConnection(binding.principal) const current = await effectiveMcpConnection(binding.principal)
@@ -273,8 +274,13 @@ export async function executeReservedScopedRecovery(allocation: RoutingReservedI
input.requestProof = await sealedProof(input, "request") input.requestProof = await sealedProof(input, "request")
reservedExecutions.set(input, execution) reservedExecutions.set(input, execution)
try { return await executeScopedRecovery(input) } try { return await executeScopedRecovery(input) }
catch (error) { await store.markUnknown(binding.reservationId, "inverse").catch(() => {}); throw error }
finally { reservedExecutions.delete(input) } finally { reservedExecutions.delete(input) }
} catch (error) {
// Acknowledged spend owns this exact slot even when its authority can no
// longer be inspected. A refused or uncertain spend never enters here.
await store.markUnknown(spent.reservationId, "inverse").catch(() => {})
throw error
}
} }
/** Fixed-domain receipt verifier used by the reservation store. It recognizes /** Fixed-domain receipt verifier used by the reservation store. It recognizes
@@ -1,5 +1,18 @@
import type { PoolClient } from "pg"
import { COMMUNITY_PATH_MAX_AGE_MS } from "./path-proof-purpose.js"
import { fingerprint, McpError, privateSecretFingerprint } from "./security.js" import { fingerprint, McpError, privateSecretFingerprint } from "./security.js"
export async function readScopedPathCheckpoint(db: Pick<PoolClient, "query">, id: string, connectionId: string, phase: "admission" | "configuration" = "admission") {
const { rows: [row] } = await db.query("SELECT id,connection_id,profile_hash,candidate_id,status,created_at,evidence FROM mcp_path_checks WHERE id=$1 AND connection_id=$2", [id, connectionId])
const now = Date.now(), createdAt = row?.created_at.getTime(), observedAt = Date.parse(row?.evidence?.observedAt)
const validUntil = Math.min(createdAt, observedAt) + COMMUNITY_PATH_MAX_AGE_MS
if (!row || row.status !== "passed" || createdAt > now || phase === "admission" &&
(!Number.isFinite(validUntil) || observedAt > now || validUntil <= now)) {
throw new McpError("MEASUREMENT_UNKNOWN", "Контрольное подтверждение пути устарело", 409)
}
return { proof: await privateSecretFingerprint(fingerprint(row), "route-ai-scoped-path-checkpoint-v1"), validUntil }
}
/** One request domain for forward execution and trusted recovery resolution. */ /** One request domain for forward execution and trusted recovery resolution. */
export async function assertScopedCommunityRequest(input: Record<string, unknown>) { export async function assertScopedCommunityRequest(input: Record<string, unknown>) {
const { requestProof: _proof, ...request } = input const { requestProof: _proof, ...request } = input
@@ -1,7 +1,7 @@
import { createRoutingNetworkSessionForCurrentExecution } from "./routing-network-execution-session.js" import { createRoutingNetworkSessionForCurrentExecution } from "./routing-network-execution-session.js"
import { readRoutingNetworkScopedCommunityContext } from "./routing-network-scoped-context.js" import { readRoutingNetworkScopedCommunityContext } from "./routing-network-scoped-context.js"
import { resolveRoutingNetworkForwardRoundConfigurationContext, closeRoutingNetworkForwardRoundConfigurationContext } from "./routing-network-configuration-bridge.js" import { resolveRoutingNetworkForwardRoundConfigurationContext, closeRoutingNetworkForwardRoundConfigurationContext } from "./routing-network-configuration-bridge.js"
import { originalEffectSelectionSchema, unknownOriginalEffects, originalEffectBaselineMatches, inspectOriginalEffectRounds, type OriginalEffectReport } from "./routing-original-effects.js" import { originalEffectReportSchema, originalEffectSelectionSchema, unknownOriginalEffects, originalEffectBaselineMatches, inspectOriginalEffectRounds, type OriginalEffectReport } from "./routing-original-effects.js"
import { randomUUID } from "node:crypto" import { randomUUID } from "node:crypto"
import { z } from "zod" import { z } from "zod"
import { routeAiProfileSchema } from "@mmapp/contracts/mcp-routing" import { routeAiProfileSchema } from "@mmapp/contracts/mcp-routing"
@@ -12,7 +12,7 @@ import { withApplicationMutationLock, withConfigMutationLock } from "../../servi
import { effectiveMcpConnection, audit } from "./store.js" import { effectiveMcpConnection, audit } from "./store.js"
import { accessGeneration, decryptSecret, encryptSecret, fingerprint, McpError, privateSecretFingerprint, redact } from "./security.js" import { accessGeneration, decryptSecret, encryptSecret, fingerprint, McpError, privateSecretFingerprint, redact } from "./security.js"
import { saveScopedConfigurationCheckpoint, loadScopedConfigurationCheckpoint, saveScopedNetworkCheckpoint, loadScopedNetworkCheckpoint } from "./routing-community-scoped-checkpoint.js" import { saveScopedConfigurationCheckpoint, loadScopedConfigurationCheckpoint, saveScopedNetworkCheckpoint, loadScopedNetworkCheckpoint } from "./routing-community-scoped-checkpoint.js"
import { assertScopedCommunityRequest as sealedInput } from "./routing-community-scoped-request.js" import { assertScopedCommunityRequest as sealedInput, readScopedPathCheckpoint } from "./routing-community-scoped-request.js"
import { prepareManagementTransportBinding } from "./management-transport.js" import { prepareManagementTransportBinding } from "./management-transport.js"
import { readRouteCommunityContext } from "./routing-community-context.js" import { readRouteCommunityContext } from "./routing-community-context.js"
import { candidateMatchesGateway } from "./routing-community-context-model.js" import { candidateMatchesGateway } from "./routing-community-context-model.js"
@@ -37,12 +37,6 @@ const effectReadFailure = (stage: "original-effect-rounds-unavailable" | "effect
export const scopedProfileIds = (profile: z.infer<typeof routeAiProfileSchema>) => [...new Set([profile.serverId, ...profile.candidates.flatMap(candidate => [candidate.jumpHostId, candidate.exitNodeId])])].sort((a, b) => a - b) export const scopedProfileIds = (profile: z.infer<typeof routeAiProfileSchema>) => [...new Set([profile.serverId, ...profile.candidates.flatMap(candidate => [candidate.jumpHostId, candidate.exitNodeId])])].sort((a, b) => a - b)
const publicRule = (rule: { community: string; gateway: string; outIface: string }) => ({ community: rule.community, action: "route", gateway: rule.gateway, gatewayTunnelId: rule.outIface }) const publicRule = (rule: { community: string; gateway: string; outIface: string }) => ({ community: rule.community, action: "route", gateway: rule.gateway, gatewayTunnelId: rule.outIface })
type ObservationPhase = "admission" | "configuration" type ObservationPhase = "admission" | "configuration"
async function pathCheckpoint(id: string, connectionId: string, phase: ObservationPhase = "admission") {
const row = (await pool.query("SELECT id,connection_id,profile_hash,candidate_id,status,created_at,evidence FROM mcp_path_checks WHERE id=$1 AND connection_id=$2", [id, connectionId])).rows[0]
if (!row || row.status !== "passed" || row.created_at.getTime() > Date.now() || phase === "admission" && Date.now() - row.created_at.getTime() > 90000) throw fail("Контрольное подтверждение пути устарело", "MEASUREMENT_UNKNOWN")
return privateSecretFingerprint(fingerprint(row), "route-ai-scoped-path-checkpoint-v1")
}
async function access(input: Record<string, unknown>, signal?: AbortSignal, verified = false, phase: ObservationPhase = "admission") { async function access(input: Record<string, unknown>, signal?: AbortSignal, verified = false, phase: ObservationPhase = "admission") {
if (signal?.aborted) throw fail("Операция отменена", "READ_CANCELLED") if (signal?.aborted) throw fail("Операция отменена", "READ_CANCELLED")
if (verified) await assertVerifiedConfigurationAccess() if (verified) await assertVerifiedConfigurationAccess()
@@ -58,7 +52,7 @@ async function access(input: Record<string, unknown>, signal?: AbortSignal, veri
const ranked = rankRoutingCandidates(profile, await readRoutingObservations(ids)) const ranked = rankRoutingCandidates(profile, await readRoutingObservations(ids))
if (!ranked.find(row => row.candidate.id === input.candidateId)?.eligible) throw fail("Нет свежего SLA выбранного пути", "MEASUREMENT_UNKNOWN") if (!ranked.find(row => row.candidate.id === input.candidateId)?.eligible) throw fail("Нет свежего SLA выбранного пути", "MEASUREMENT_UNKNOWN")
} }
if (input.pathCheckId !== undefined && await pathCheckpoint(String(input.pathCheckId), principal.connectionId, phase) !== input.pathCheckpointProof) throw fail("Подтверждение пути изменилось", "MEASUREMENT_UNKNOWN") if (input.pathCheckId !== undefined && (await readScopedPathCheckpoint(pool, String(input.pathCheckId), principal.connectionId, phase)).proof !== input.pathCheckpointProof) throw fail("Подтверждение пути изменилось", "MEASUREMENT_UNKNOWN")
return { principal, current, profile, ids } return { principal, current, profile, ids }
} }
@@ -117,7 +111,8 @@ export async function prepareScopedCommunitySwitch(input: Record<string, unknown
await access(request, action.signal) await access(request, action.signal)
} }
const pathCheckId = await requireRecentPath(profile, String(input.candidateId), action.connection.id, { authorize: async () => { await access(request, action.signal) }, signal: action.signal }) const pathCheckId = await requireRecentPath(profile, String(input.candidateId), action.connection.id, { authorize: async () => { await access(request, action.signal) }, signal: action.signal })
const withPath = { ...request, pathCheckId, pathCheckpointProof: await pathCheckpoint(pathCheckId, action.connection.id) } const checkpoint = await readScopedPathCheckpoint(pool, pathCheckId, action.connection.id)
const withPath = { ...request, pathCheckId, pathCheckpointProof: checkpoint.proof }
const value = await observe(withPath, action.signal), owned = value.context.ownedTarget! const value = await observe(withPath, action.signal), owned = value.context.ownedTarget!
if (!value.candidate.controlProbe) throw fail("Scoped переключение требует свежий control probe", "MEASUREMENT_UNKNOWN") if (!value.candidate.controlProbe) throw fail("Scoped переключение требует свежий control probe", "MEASUREMENT_UNKNOWN")
await requireRecentPath(value.profile, value.candidate.id, action.connection.id, { authorize: value.authorize, signal: action.signal }) await requireRecentPath(value.profile, value.candidate.id, action.connection.id, { authorize: value.authorize, signal: action.signal })
@@ -149,9 +144,12 @@ export async function prepareScopedCommunitySwitch(input: Record<string, unknown
sealed.requestProof = await privateSecretFingerprint(fingerprint(sealed), `route-ai-scoped-request-v${sealed.proofRevision}`) sealed.requestProof = await privateSecretFingerprint(fingerprint(sealed), `route-ai-scoped-request-v${sealed.proofRevision}`)
await value.context.fence() await value.context.fence()
await access(sealed, action.signal) await access(sealed, action.signal)
const authorityValidUntil = persistent ? routingProfileAuthorityBindingSchema.parse(sealed.persistentProfileAuthority).packet.validUntil : undefined
const validUntil = authorityValidUntil && !profile.autoRecovery
? new Date(Math.min(Date.parse(authorityValidUntil), checkpoint.validUntil)).toISOString() : authorityValidUntil
return { target: { scope: "servers", serverIds: scopedProfileIds(value.profile) }, objects: [{ kind: "routing_filter", serverId: value.profile.serverId, id: owned.nativeId }, ...scopedProfileIds(value.profile).map(serverId => ({ kind: "server" as const, serverId, id: String(serverId) }))], snapshots: [], before: sealed.beforeRules, return { target: { scope: "servers", serverIds: scopedProfileIds(value.profile) }, objects: [{ kind: "routing_filter", serverId: value.profile.serverId, id: owned.nativeId }, ...scopedProfileIds(value.profile).map(serverId => ({ kind: "server" as const, serverId, id: String(serverId) }))], snapshots: [], before: sealed.beforeRules,
steps: [{ kind: "service", service: "routing.community.scoped", input: sealed }], requiresHuman: true, mutationCost: 2, verificationScope: "configuration", steps: [{ kind: "service", service: "routing.community.scoped", input: sealed }], requiresHuman: true, mutationCost: 2, verificationScope: "configuration",
...(persistent ? { validUntil: routingProfileAuthorityBindingSchema.parse(sealed.persistentProfileAuthority).packet.validUntil } : {}), ...(validUntil ? { validUntil } : {}),
warnings: ["Одна запись owned фильтра; SQL метаданные фильтров сохраняются.", "SLA и свежая preflight-проба обязательны до отправки PATCH. После записи подтверждение конфигурации сохраняется независимо от новых SLA-измерений; контрольная проба и её результат проверяются отдельно.", "Подтверждается конфигурация и control probe; сквозной и обратный трафик требуют отдельной приёмки.", managementBindingCiphertext ? "Заранее закреплён независимый транспорт для отдельного обратного плана; автоматический inverse не включён." : "Независимый transport не закреплён; отдельный обратный план потребует доступного primary.", "Неизвестный результат не повторяется; сверка только читает состояние."] } warnings: ["Одна запись owned фильтра; SQL метаданные фильтров сохраняются.", "SLA и свежая preflight-проба обязательны до отправки PATCH. После записи подтверждение конфигурации сохраняется независимо от новых SLA-измерений; контрольная проба и её результат проверяются отдельно.", "Подтверждается конфигурация и control probe; сквозной и обратный трафик требуют отдельной приёмки.", managementBindingCiphertext ? "Заранее закреплён независимый транспорт для отдельного обратного плана; автоматический inverse не включён." : "Независимый transport не закреплён; отдельный обратный план потребует доступного primary.", "Неизвестный результат не повторяется; сверка только читает состояние."] }
} }
@@ -166,7 +164,9 @@ export function registerScopedCommunityService() {
} }
await original(input, true) await original(input, true)
if (await receiptExists(input, "routing.scoped.attempt")) throw fail("Повтор PATCH запрещён", "MUTATION_OUTCOME_UNKNOWN") if (await receiptExists(input, "routing.scoped.attempt")) throw fail("Повтор PATCH запрещён", "MUTATION_OUTCOME_UNKNOWN")
await receipt(input, "routing.scoped.attempt") const requiresEffectBaseline = input.proofRevision === 3 && !routeAiProfileSchema.parse(input.profile).autoRecovery
let attemptRecorded = !requiresEffectBaseline
if (attemptRecorded) await receipt(input, "routing.scoped.attempt")
const before = await original(input, true), owned = before.context.ownedTarget! const before = await original(input, true), owned = before.context.ownedTarget!
let effectSession: Awaited<ReturnType<typeof createRoutingNetworkSessionForCurrentExecution>> | undefined let effectSession: Awaited<ReturnType<typeof createRoutingNetworkSessionForCurrentExecution>> | undefined
let effectBaseline: Parameters<typeof originalEffectBaselineMatches>[1] | undefined let effectBaseline: Parameters<typeof originalEffectBaselineMatches>[1] | undefined
@@ -174,8 +174,8 @@ export function registerScopedCommunityService() {
const effectSelection = originalEffectSelectionSchema.safeParse(input.originalEffectSelection) const effectSelection = originalEffectSelectionSchema.safeParse(input.originalEffectSelection)
if (effectSelection.success) originalEffects = unknownOriginalEffects(effectSelection.data.status === "unknown" ? effectSelection.data.reason : "effect-acquisition-unavailable") if (effectSelection.success) originalEffects = unknownOriginalEffects(effectSelection.data.status === "unknown" ? effectSelection.data.reason : "effect-acquisition-unavailable")
try { try {
// Optional original-effect vector is never an extra write/recovery grant. // The persistent controller needs a baseline it can advance after the
// Old manual configuration+probe acceptance remains authoritative. // write. Manual plans still treat the effect vector as observational.
if (context?.phase === "forward" && (input.proofRevision === 3 || input.proofRevision === 2 && input.manualEffectPolicy === "observational-v1") && effectSelection.success && effectSelection.data.status === "prepared") { if (context?.phase === "forward" && (input.proofRevision === 3 || input.proofRevision === 2 && input.manualEffectPolicy === "observational-v1") && effectSelection.success && effectSelection.data.status === "prepared") {
try { try {
const selected = effectSelection.data const selected = effectSelection.data
@@ -188,6 +188,11 @@ export function registerScopedCommunityService() {
} }
} catch { effectSession?.close(); effectSession = undefined } } catch { effectSession?.close(); effectSession = undefined }
} }
if (requiresEffectBaseline) {
if (!effectSession) throw fail("Исходное состояние изменяемого префикса не подтверждено", "MEASUREMENT_UNKNOWN")
await receipt(input, "routing.scoped.attempt")
attemptRecorded = true
}
await executeGuardedConfigurationWrite(before.profile.serverId, "PATCH", "/routing/filter/rule/" + owned.nativeId, async () => { await executeGuardedConfigurationWrite(before.profile.serverId, "PATCH", "/routing/filter/rule/" + owned.nativeId, async () => {
// This runs AFTER the durable configuration.intent, immediately before transport. // This runs AFTER the durable configuration.intent, immediately before transport.
const fresh = await original(input, true); await before.context.fence() const fresh = await original(input, true); await before.context.fence()
@@ -208,8 +213,7 @@ export function registerScopedCommunityService() {
roundContext = await resolveRoutingNetworkForwardRoundConfigurationContext(effectSession.sharedContext, action) roundContext = await resolveRoutingNetworkForwardRoundConfigurationContext(effectSession.sharedContext, action)
const first = await effectSession.readConfigurationBoundForwardSnapshot(roundContext) const first = await effectSession.readConfigurationBoundForwardSnapshot(roundContext)
const second = await effectSession.readConfigurationBoundForwardSnapshot(roundContext) const second = await effectSession.readConfigurationBoundForwardSnapshot(roundContext)
const observed = await inspectOriginalEffectRounds(effectSelection.data, [first, second], effectBaseline), budget = effectSession.readBudget() originalEffects = await inspectOriginalEffectRounds(effectSelection.data, [first, second], effectBaseline)
originalEffects = { ...observed, budget: { deadline: budget.deadline, calls: budget.calls, bytes: budget.bytes, rows: budget.rows } }
} catch (error) { originalEffects = effectReadFailure("original-effect-rounds-unavailable", error) } } catch (error) { originalEffects = effectReadFailure("original-effect-rounds-unavailable", error) }
finally { if (roundContext) closeRoutingNetworkForwardRoundConfigurationContext(roundContext) } finally { if (roundContext) closeRoutingNetworkForwardRoundConfigurationContext(roundContext) }
} }
@@ -221,8 +225,6 @@ export function registerScopedCommunityService() {
const published = await readRoutingNetworkScopedCommunityContext(effectSession.sharedContext) const published = await readRoutingNetworkScopedCommunityContext(effectSession.sharedContext)
if (!matches(input, published).configurationMatchesDesired) throw fail("Состояние изменилось после effect-check") if (!matches(input, published).configurationMatchesDesired) throw fail("Состояние изменилось после effect-check")
await published.fence() await published.fence()
const budget = effectSession.readBudget()
originalEffects = { ...originalEffects!, budget: { deadline: budget.deadline, calls: budget.calls, bytes: budget.bytes, rows: budget.rows } }
sharedPublication = true sharedPublication = true
} catch (error) { originalEffects = effectReadFailure("effect-publication-revalidation-unavailable", error) } } catch (error) { originalEffects = effectReadFailure("effect-publication-revalidation-unavailable", error) }
} }
@@ -235,12 +237,16 @@ export function registerScopedCommunityService() {
await final.context.fence() await final.context.fence()
} }
if (effectSession && originalEffects) { if (effectSession && originalEffects) {
const budget = effectSession.readBudget() const { deadline, calls, bytes, rows } = effectSession.readBudget()
originalEffects = { ...originalEffects, budget: { deadline: budget.deadline, calls: budget.calls, bytes: budget.bytes, rows: budget.rows } } const budget = originalEffectReportSchema.shape.budget.safeParse({ deadline, calls, bytes, rows })
if (budget.success) originalEffects = { ...originalEffects, budget: budget.data }
} }
await saveScopedNetworkCheckpoint(input, await sealedInput(input), { ...check, status: check.status }, originalEffects) await saveScopedNetworkCheckpoint(input, await sealedInput(input), { ...check, status: check.status }, originalEffects)
return verifiedResult(input) return verifiedResult(input)
} catch { throw fail("Результат переключения не подтверждён; повтор запрещён, требуется сверка", "MUTATION_OUTCOME_UNKNOWN") } } catch (error) {
if (!attemptRecorded) throw error
throw fail("Результат переключения не подтверждён; повтор запрещён, требуется сверка", "MUTATION_OUTCOME_UNKNOWN")
}
finally { effectSession?.close() } finally { effectSession?.close() }
})), })),
verify: async (input, result) => { verify: async (input, result) => {
@@ -4,7 +4,7 @@ import { readFile } from "node:fs/promises"
import { randomUUID } from "node:crypto" import { randomUUID } from "node:crypto"
import ts from "typescript" import ts from "typescript"
import { z } from "zod" import { z } from "zod"
import { routeAiProfileSchema } from "@mmapp/contracts/mcp-routing" import { routeAiControllerBlockReason, routeAiProfileSchema, usesScopedRouteAiContext } from "@mmapp/contracts/mcp-routing"
import { fingerprint, McpError } from "./security.js" import { fingerprint, McpError } from "./security.js"
type Handler = { prepare?: (input: Record<string, unknown>, context: unknown) => Promise<Record<string, unknown>>; execute?: (input: Record<string, unknown>) => Promise<unknown> } type Handler = { prepare?: (input: Record<string, unknown>, context: unknown) => Promise<Record<string, unknown>>; execute?: (input: Record<string, unknown>) => Promise<unknown> }
@@ -32,10 +32,10 @@ async function fixture() {
const profileModule = await load("./routing-profiles.ts", { const profileModule = await load("./routing-profiles.ts", {
"./routing-controller-overlap.js": overlap, "./security.js": { fingerprint, McpError, accessGeneration: async () => "g" }, "./routing-controller-overlap.js": overlap, "./security.js": { fingerprint, McpError, accessGeneration: async () => "g" },
"./routing-controller-baseline.js": { prepareRoutingControllerBaseline: async () => { baselinePreparations++; return { packet: {}, proof: "inert-baseline" } }, assertRoutingControllerBaseline: async () => {}, routingControllerBaselinePrincipal: async () => principal, advanceRoutingControllerBaseline: async () => {} }, "./routing-controller-baseline.js": { prepareRoutingControllerBaseline: async () => { baselinePreparations++; return { packet: {}, proof: "inert-baseline" } }, assertRoutingControllerBaseline: async () => {}, routingControllerBaselinePrincipal: async () => principal, advanceRoutingControllerBaseline: async () => {} },
"./routing-community-public.js": { publicRoutingControllerState: () => ({}) }, "node:crypto": { randomUUID }, zod: { z }, "@mmapp/contracts/mcp-routing": { routeAiProfileSchema }, "../../db/index.js": { pool }, "./routing-community-public.js": { publicRoutingControllerState: () => ({}) }, "node:crypto": { randomUUID }, zod: { z }, "@mmapp/contracts/mcp-routing": { routeAiControllerBlockReason, routeAiProfileSchema, usesScopedRouteAiContext }, "../../db/index.js": { pool },
"./service-registry.js": { registerMcpService: (name: string, handler: Handler) => services.set(name, handler) }, "./service-registry.js": { registerMcpService: (name: string, handler: Handler) => services.set(name, handler) },
"./store.js": { audit: async () => {}, validatePrincipal: async () => connection }, "./routing-model.js": { decideRoutingSwitch: () => ({}) }, "./store.js": { audit: async () => {}, validatePrincipal: async () => connection }, "./routing-model.js": { decideRoutingSwitch: () => ({}) },
"./routing-community-context-model.js": { routeContextExecutionReason: () => null }, "./path-proof-purpose.js": { COMMUNITY_PATH_MAX_AGE_MS: 90000 },
"./routing-controller-fence.js": { saveControllerProfileCas: async () => { casWrites++; return { id: "saved", version: 1 } } }, "./routing-controller-fence.js": { saveControllerProfileCas: async () => { casWrites++; return { id: "saved", version: 1 } } },
"./routing-reservation-store.js": { createRoutingReservationStore: () => { throw new Error("Overlap-only fixture must not read or mutate routing charges") } }, "./routing-reservation-store.js": { createRoutingReservationStore: () => { throw new Error("Overlap-only fixture must not read or mutate routing charges") } },
}) })
+1 -1
View File
@@ -243,7 +243,7 @@ export function registerRoutingControls(register: typeof add) {
}, },
}) })
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(), { 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(), {
version: 3, version: 4,
prepare: async (input, context) => { prepare: async (input, context) => {
const sid = Number(input.serverId), { client } = await routerClient(sid), snapshot = await readRosSnapshot(sid, "vrf"), current = snapshot.rows.find(row => row[".id"] === input.id) const sid = Number(input.serverId), { client } = await routerClient(sid), snapshot = await readRosSnapshot(sid, "vrf"), current = snapshot.rows.find(row => row[".id"] === input.id)
if (!current || current.name === "main" || !hasManagedCommentPrefix(String(current.comment ?? ""))) throw new McpError("PROTECTED_OBJECT", "Изменять состав можно только у существующего VRF приложения") if (!current || current.name === "main" || !hasManagedCommentPrefix(String(current.comment ?? ""))) throw new McpError("PROTECTED_OBJECT", "Изменять состав можно только у существующего VRF приложения")
@@ -37,7 +37,7 @@ function fixture(service='routing.community') {
const principal={connectionId:id(3), credentialId:id(4), credentialExpiresAt:'2026-11-02T13:00:00.000Z'}; const principal={connectionId:id(3), credentialId:id(4), credentialExpiresAt:'2026-11-02T13:00:00.000Z'};
const authority={packet:{profileId:id(5),profileVersion:7,connectionId:id(3),credentialId:id(4),ownerId:'owner'}, proof:'a'.repeat(64)}; const authority={packet:{profileId:id(5),profileVersion:7,connectionId:id(3),credentialId:id(4),ownerId:'owner'}, proof:'a'.repeat(64)};
const metadata={operationId:id(1),planId:id(2),operationName:'routing.community.switch',operationVersion:7,ownerId:'owner',phase:'forward',stepIndex:0,serviceName:service,principal,policyVersion:2,generation:'generation',originalExpiresAt:now.getTime()+60000,sealedEvidenceProof:sha('evidence'),sealedPayloadProof:sha('payload'),serviceInput:{profile:{autoRecovery:false},persistentProfileAuthority:authority}}; const metadata={operationId:id(1),planId:id(2),operationName:'routing.community.switch',operationVersion:7,ownerId:'owner',phase:'forward',stepIndex:0,serviceName:service,principal,policyVersion:2,generation:'generation',originalExpiresAt:now.getTime()+60000,sealedEvidenceProof:sha('evidence'),sealedPayloadProof:sha('payload'),serviceInput:{profile:{autoRecovery:false},persistentProfileAuthority:authority}};
const source={operation_state:'running',plan_state:'executing',policy_version:2,generation:'generation',approved_by:'human',observed_at:now,evidence_sealed:'evidence',payload_sealed:'payload',step_state:'intent',intended_state:redact({kind:'service',service,input:metadata.serviceInput})}; const source={operation_state:'running',plan_state:'executing',policy_version:2,generation:'generation',approved_by:service==='routing.community.scoped'?'owner':'human',observed_at:now,evidence_sealed:'evidence',payload_sealed:'payload',step_state:'intent',intended_state:redact({kind:'service',service,input:metadata.serviceInput})};
const binding={revision:1,operationId:id(1),planId:id(2),stepIndex:0,profileId:id(5),profileVersion:7,connectionId:id(3),ownerId:'owner',principal,service,operationVersion:7,authorityProof:authority.proof,evidenceProof:fingerprint('evidence'),payloadProof:fingerprint('payload'),scope:[1],createdAt:now.toISOString(),chargeUnits:1}; const binding={revision:1,operationId:id(1),planId:id(2),stepIndex:0,profileId:id(5),profileVersion:7,connectionId:id(3),ownerId:'owner',principal,service,operationVersion:7,authorityProof:authority.proof,evidenceProof:fingerprint('evidence'),payloadProof:fingerprint('payload'),scope:[1],createdAt:now.toISOString(),chargeUnits:1};
const row={source_operation_id:id(1),source_plan_id:id(2),source_step_index:0,profile_id:id(5),connection_id:id(3),charge_units:1,binding_sealed:null,binding_proof:null,created_at:now}; const row={source_operation_id:id(1),source_plan_id:id(2),source_step_index:0,profile_id:id(5),connection_id:id(3),charge_units:1,binding_sealed:null,binding_proof:null,created_at:now};
let chargeRows=[row], sourceRows=[source], reservedRows=[], decryptCalls=0, nativeWrites=0, keyUnavailable=false; let chargeRows=[row], sourceRows=[source], reservedRows=[], decryptCalls=0, nativeWrites=0, keyUnavailable=false;
@@ -59,6 +59,7 @@ function fixture(service='routing.community') {
run:async()=>{await verifier(db,metadata,'owner');nativeWrites++;},writes:()=>nativeWrites,decryptCalls:()=>decryptCalls}; run:async()=>{await verifier(db,metadata,'owner');nativeWrites++;},writes:()=>nativeWrites,decryptCalls:()=>decryptCalls};
} }
for(const service of ['routing.community','routing.community.scoped'])test(`exact ${service} running charge passes without mutation`,async()=>{const f=fixture(service);await f.run();assert.equal(f.writes(),1);assert.equal(f.queries.length,3);assert.ok(f.queries.every(({sql})=>sql.startsWith('SELECT ')));assert.ok(f.queries[0].sql.includes('LIMIT 2'));}); for(const service of ['routing.community','routing.community.scoped'])test(`exact ${service} running charge passes without mutation`,async()=>{const f=fixture(service);await f.run();assert.equal(f.writes(),1);assert.equal(f.queries.length,3);assert.ok(f.queries.every(({sql})=>sql.startsWith('SELECT ')));assert.ok(f.queries[0].sql.includes('LIMIT 2'));});
test('scoped transport refuses delegated approval before reading the charge',async()=>{const f=fixture('routing.community.scoped');f.source.approved_by='delegated-admin';await assert.rejects(f.run);assert.equal(f.writes(),0);assert.equal(f.queries.length,1);});
const mutations=[ const mutations=[
['missing charge',f=>f.noCharge()],['duplicate charge',f=>f.duplicateCharge()],['reserved overlap',f=>f.reserved()],['ambiguous source',f=>f.ambiguousSource()], ['missing charge',f=>f.noCharge()],['duplicate charge',f=>f.duplicateCharge()],['reserved overlap',f=>f.reserved()],['ambiguous source',f=>f.ambiguousSource()],
['pending operation',f=>f.source.operation_state='pending'],['applied operation',f=>f.source.operation_state='applied'],['terminal step',f=>f.source.step_state='verified'],['draft plan',f=>f.source.plan_state='draft'],['missing approval',f=>f.source.approved_by=null],['policy drift',f=>f.source.policy_version++],['generation drift',f=>f.source.generation='other'], ['pending operation',f=>f.source.operation_state='pending'],['applied operation',f=>f.source.operation_state='applied'],['terminal step',f=>f.source.step_state='verified'],['draft plan',f=>f.source.plan_state='draft'],['missing approval',f=>f.source.approved_by=null],['policy drift',f=>f.source.policy_version++],['generation drift',f=>f.source.generation='other'],
@@ -57,7 +57,7 @@ function cursor(rows:unknown[],onFetch?:()=>void) {
test("historical cursor authenticates more than two pages of genuine nonpersistent classifications",async()=>{ test("historical cursor authenticates more than two pages of genuine nonpersistent classifications",async()=>{
const dir=await mkdtemp(join(tmpdir(),"mm-manual-history-unit-")),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir const dir=await mkdtemp(join(tmpdir(),"mm-manual-history-unit-")),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir
try { try {
const rows=[];for(let n=0;n<25;n++)rows.push((await fixture(false,n%2?6:7)).row) const rows=[];for(let n=0;n<25;n++)rows.push((await fixture(false,6+n%3)).row)
const f=cursor(rows) const f=cursor(rows)
await assertLegacyManualHistoryWithinOwner(f.db,randomUUID(),at,()=>{}) await assertLegacyManualHistoryWithinOwner(f.db,randomUUID(),at,()=>{})
assert.equal(f.calls.filter(sql=>sql.startsWith("FETCH")).length,3) assert.equal(f.calls.filter(sql=>sql.startsWith("FETCH")).length,3)
@@ -76,8 +76,10 @@ test("historical authentication, active persistent refusal, future rejection and
await assertLegacyManualHistoryWithinOwner(cursor([f.row]).db,f.profileId,"2026-11-01T13:01:00.001Z",()=>{}) await assertLegacyManualHistoryWithinOwner(cursor([f.row]).db,f.profileId,"2026-11-01T13:01:00.001Z",()=>{})
const expiredTamper={...f.row,source_evidence:null} const expiredTamper={...f.row,source_evidence:null}
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([expiredTamper]).db,f.profileId,"2026-11-01T14:00:00.000Z",()=>{})) await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([expiredTamper]).db,f.profileId,"2026-11-01T14:00:00.000Z",()=>{}))
const current={...f.row,recovery_version:7} for (const version of [7,8]) {
const current={...f.row,recovery_version:version}
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([current]).db,f.profileId,"2026-11-01T14:00:00.000Z",()=>{}),/lacks durable charge/) await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([current]).db,f.profileId,"2026-11-01T14:00:00.000Z",()=>{}),/lacks durable charge/)
}
const stripped={...f.input};delete stripped.persistentProfileAuthority const stripped={...f.input};delete stripped.persistentProfileAuthority
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([{...f.row,source_evidence:await encryptSecret(JSON.stringify({steps:[{kind:"service",service:"routing.community.scoped",input:stripped}]}))}]).db,f.profileId,at,()=>{})) await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([{...f.row,source_evidence:await encryptSecret(JSON.stringify({steps:[{kind:"service",service:"routing.community.scoped",input:stripped}]}))}]).db,f.profileId,at,()=>{}))
const future={...f.intent,createdAt:"2026-11-01T12:00:00.001Z"} const future={...f.intent,createdAt:"2026-11-01T12:00:00.001Z"}
@@ -112,14 +114,14 @@ test("state transfer cap MODEL refuses oversized or NULL history as a whole, wit
}finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})} }finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})}
}) })
async function retentionFixture() { async function retentionFixture(version = 7) {
const f=await fixture(true,7), authority=f.input.persistentProfileAuthority as {packet:{profileVersion:number;scope:number[]};proof:string} const f=await fixture(true,version), authority=f.input.persistentProfileAuthority as {packet:{profileVersion:number;scope:number[]};proof:string}
const payload=await encryptSecret(JSON.stringify({MODEL:true})) const payload=await encryptSecret(JSON.stringify({MODEL:true}))
const row={...f.row,recovery_payload:payload,authority_profile_version:1} const row={...f.row,recovery_payload:payload,authority_profile_version:1}
const binding=manualInverseReceiptSchema.shape.binding.parse({revision:1,recoveryOperationId:f.intent.operationId,recoveryPlanId:row.recovery_plan_id, const binding=manualInverseReceiptSchema.shape.binding.parse({revision:1,recoveryOperationId:f.intent.operationId,recoveryPlanId:row.recovery_plan_id,
sourceOperationId:f.intent.sourceOperationId,sourcePlanId:row.source_plan_id,sourceStepIndex:0,profileId:f.profileId,profileVersion:1, sourceOperationId:f.intent.sourceOperationId,sourcePlanId:row.source_plan_id,sourceStepIndex:0,profileId:f.profileId,profileVersion:1,
connectionId:f.intent.connectionId,ownerId:f.intent.ownerId,originalPrincipal:f.input.principal,recoveryPrincipal:f.input.principal, connectionId:f.intent.connectionId,ownerId:f.intent.ownerId,originalPrincipal:f.input.principal,recoveryPrincipal:f.input.principal,
operationVersion:7,sourceVersion:7,authorityProof:authority.proof,sourceProof:f.intent.sourceProof,sourceRequestProof:f.input.requestProof, operationVersion:version,sourceVersion:7,authorityProof:authority.proof,sourceProof:f.intent.sourceProof,sourceRequestProof:f.input.requestProof,
recoveryRequestProof:f.intent.requestSeal,sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence), recoveryRequestProof:f.intent.requestSeal,sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence),
recoveryPayloadProof:fingerprint(payload),scope:authority.packet.scope,createdAt:at,chargeUnits:1}) recoveryPayloadProof:fingerprint(payload),scope:authority.packet.scope,createdAt:at,chargeUnits:1})
const receipt={binding,proof:await sign(binding,"routing-manual-inverse-charge-v1")} const receipt={binding,proof:await sign(binding,"routing-manual-inverse-charge-v1")}
@@ -153,7 +155,8 @@ function retentionOwner(f:Awaited<ReturnType<typeof retentionFixture>>,present=t
test('retention MODEL intent2 authenticates SQL clock after parent GC; unknown noReplay anchor survives',async()=>{ test('retention MODEL intent2 authenticates SQL clock after parent GC; unknown noReplay anchor survives',async()=>{
const dir=await mkdtemp(join(tmpdir(),'mm-intent2-retention-')),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir const dir=await mkdtemp(join(tmpdir(),'mm-intent2-retention-')),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir
try { try {
const f=await retentionFixture(),owner=retentionOwner(f),before=fingerprint(f.row) for (const version of [7,8]) {
const f=await retentionFixture(version),owner=retentionOwner(f),before=fingerprint(f.row)
// SQL aging and the transaction owner below are MODELS, never genuine SQL. // SQL aging and the transaction owner below are MODELS, never genuine SQL.
await assertLegacyManualHistoryWithinOwner(owner.db,f.profileId,'2026-11-03T12:00:00.000Z',()=>{}) 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,1),{deleted:1})
@@ -164,6 +167,7 @@ test('retention MODEL intent2 authenticates SQL clock after parent GC; unknown n
assert(candidate.indexOf("state IN('pending','running')")<candidate.indexOf('LIMIT 100')) assert(candidate.indexOf("state IN('pending','running')")<candidate.indexOf('LIMIT 100'))
assert(candidate.includes('r.authority_profile_version IS NOT NULL')) assert(candidate.includes('r.authority_profile_version IS NOT NULL'))
assert.equal(f.intent.revision,2);assert.equal(await decryptSecretExisting(f.row.intent_sealed),JSON.stringify(f.intent)) assert.equal(f.intent.revision,2);assert.equal(await decryptSecretExisting(f.row.intent_sealed),JSON.stringify(f.intent))
}
}finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})} }finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})}
}) })
@@ -182,6 +186,11 @@ test('retention MODEL refuses recent/future/malformed copy, present corrupt pare
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,1),/MAC/);assert.equal(t.deleted.length,0)
const legacy=await fixture(true,7) const legacy=await fixture(true,7)
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([legacy.row]).db,legacy.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/lacks durable/) 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/)
assert.equal(mismatched.deleted.length,0)
// Revision2 payload proof cannot be replaced by an authentic stamp from a different payload. // Revision2 payload proof cannot be replaced by an authentic stamp from a different payload.
const payload=await retentionFixture();payload.row.recovery_payload=await encryptSecret('{}') const payload=await retentionFixture();payload.row.recovery_payload=await encryptSecret('{}')
await assert.rejects(assertLegacyManualHistoryWithinOwner(retentionOwner(payload,false).db,payload.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/payload/) await assert.rejects(assertLegacyManualHistoryWithinOwner(retentionOwner(payload,false).db,payload.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/payload/)
@@ -40,7 +40,7 @@ export async function classifyManualInverseHistory(row:HistoricalRow,observedAt:
intent.receiptId!==row.recovery_receipt_id || intent.validUntil!==iso(row.deadline) || intent.receiptId!==row.recovery_receipt_id || intent.validUntil!==iso(row.deadline) ||
row.source_connection_id!==intent.connectionId || row.recovery_connection_id!==intent.connectionId || row.source_connection_id!==intent.connectionId || row.recovery_connection_id!==intent.connectionId ||
row.source_action!=="routing.community.switch" || row.source_version!==7 || row.source_action!=="routing.community.switch" || row.source_version!==7 ||
row.recovery_action!=="operations.recover" || ![6,7].includes(row.recovery_version??0) || row.recovery_action!=="operations.recover" || ![6,7,8].includes(row.recovery_version??0) ||
!["partial","applied"].includes(row.source_state??"")) throw fail("Historical manual intent/source identity differs") !["partial","applied"].includes(row.source_state??"")) throw fail("Historical manual intent/source identity differs")
const source=await sealedJson(row.source_evidence) as ChangeEvidence, recovery=await sealedJson(row.recovery_evidence) as ChangeEvidence const source=await sealedJson(row.source_evidence) as ChangeEvidence, recovery=await sealedJson(row.recovery_evidence) as ChangeEvidence
const step=source.steps?.[0], inverse=recovery.steps?.[0] const step=source.steps?.[0], inverse=recovery.steps?.[0]
@@ -141,7 +141,7 @@ export async function assertLegacyManualHistoryWithinOwner(db:PoolClient,profile
// Authenticate any durable inverse row before treating this old manual // Authenticate any durable inverse row before treating this old manual
// identity as represented by the normal profile ledger. // identity as represented by the normal profile ledger.
const {authenticateManualInverseHistoryWithinOwner}=await import("./routing-manual-inverse-store.js") const {authenticateManualInverseHistoryWithinOwner}=await import("./routing-manual-inverse-store.js")
if(await authenticateManualInverseHistoryWithinOwner(db,classified.intent,classified.authority,observedAt,{sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence),recoveryPlanId:row.recovery_plan_id,sourcePlanId:row.source_plan_id,sourceRequestProof:classified.sourceRequestProof,sourcePrincipal:classified.sourcePrincipal,recoveryPrincipal:classified.recoveryPrincipal})) continue if(await authenticateManualInverseHistoryWithinOwner(db,classified.intent,classified.authority,observedAt,{sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence),recoveryPlanId:row.recovery_plan_id,recoveryVersion:row.recovery_version,sourcePlanId:row.source_plan_id,sourceRequestProof:classified.sourceRequestProof,sourcePrincipal:classified.sourcePrincipal,recoveryPrincipal:classified.recoveryPrincipal})) continue
if(row.recovery_version!==6) throw fail("Current persistent manual inverse lacks durable charge") if(row.recovery_version!==6) throw fail("Current persistent manual inverse lacks durable charge")
if(classified.authority.packet.profileId!==profileId) continue if(classified.authority.packet.profileId!==profileId) continue
// Historical v6 sealed evidence.validUntil bounded admission; source and // Historical v6 sealed evidence.validUntil bounded admission; source and
@@ -7,6 +7,7 @@ import { effectiveMcpConnection } from "./store.js"
import { decryptSecretExisting, encryptSecret, fingerprint, McpError, privateFingerprinterExisting, privateSecretFingerprint } from "./security.js" import { decryptSecretExisting, encryptSecret, fingerprint, McpError, privateFingerprinterExisting, privateSecretFingerprint } from "./security.js"
import { routingProfileAuthorityBindingSchema } from "./routing-profile-authority.js" import { routingProfileAuthorityBindingSchema } from "./routing-profile-authority.js"
import type { RoutingSwitchLedgerEntry } from "./routing-switch-ledger-contract.js" import type { RoutingSwitchLedgerEntry } from "./routing-switch-ledger-contract.js"
import { findMcpOperation } from "./registry.js"
const fail = (message: string) => new McpError("STATE_CONFLICT", message, 409) const fail = (message: string) => new McpError("STATE_CONFLICT", message, 409)
const uuid = z.string().uuid(), proof = z.string().regex(/^[a-f0-9]{64}$/), stamp = z.string().datetime() const uuid = z.string().uuid(), proof = z.string().regex(/^[a-f0-9]{64}$/), stamp = z.string().datetime()
@@ -14,7 +15,7 @@ const principal = z.object({ connectionId: uuid, credentialId: uuid, credentialE
export const manualInverseBindingSchema = z.object({ revision: z.literal(1), recoveryOperationId: uuid, recoveryPlanId: uuid, export const manualInverseBindingSchema = z.object({ revision: z.literal(1), recoveryOperationId: uuid, recoveryPlanId: uuid,
sourceOperationId: uuid, sourcePlanId: uuid, sourceStepIndex: z.literal(0), profileId: uuid, profileVersion: z.number().int().positive(), sourceOperationId: uuid, sourcePlanId: uuid, sourceStepIndex: z.literal(0), profileId: uuid, profileVersion: z.number().int().positive(),
connectionId: uuid, ownerId: z.string().min(1).max(1000), originalPrincipal: principal, recoveryPrincipal: principal, connectionId: uuid, ownerId: z.string().min(1).max(1000), originalPrincipal: principal, recoveryPrincipal: principal,
operationVersion: z.literal(7), sourceVersion: z.literal(7), authorityProof: proof, sourceProof: proof, operationVersion: z.union([z.literal(7), z.literal(8)]), sourceVersion: z.literal(7), authorityProof: proof, sourceProof: proof,
sourceRequestProof: proof, recoveryRequestProof: proof, sourceEvidenceProof: proof, sourceRequestProof: proof, recoveryRequestProof: proof, sourceEvidenceProof: proof,
recoveryEvidenceProof: proof, recoveryPayloadProof: proof, recoveryEvidenceProof: proof, recoveryPayloadProof: proof,
scope: z.array(z.number().int().positive().safe()).min(1).max(1000), createdAt: stamp, chargeUnits: z.literal(1) }).strict() scope: z.array(z.number().int().positive().safe()).min(1).max(1000), createdAt: stamp, chargeUnits: z.literal(1) }).strict()
@@ -74,7 +75,8 @@ export async function selectManualInverseAdmissionWithinOwner(db: PoolClient, pl
if (plan.operation_id!=="operations.recover") return undefined if (plan.operation_id!=="operations.recover") return undefined
const step=evidence.steps[0] const step=evidence.steps[0]
if (evidence.steps.length!==1 || step.kind!=="service" || step.service!=="routing.community.scoped.recover") return undefined if (evidence.steps.length!==1 || step.kind!=="service" || step.service!=="routing.community.scoped.recover") return undefined
if (plan.operation_version!==7 || step.input.mode!=="manual") throw fail("Manual recovery action version/mode differs") if (plan.operation_version!==findMcpOperation(plan.operation_id)?.version) throw new McpError("PLAN_EXPIRED", "Версия операции изменилась; подготовьте новый план", 409)
if (step.input.mode!=="manual") throw fail("Manual recovery mode differs")
const { loadScopedManualRecoveryAdmissionWithinOwner } = await import("./routing-community-scoped-recovery.js") const { loadScopedManualRecoveryAdmissionWithinOwner } = await import("./routing-community-scoped-recovery.js")
const loaded = await loadScopedManualRecoveryAdmissionWithinOwner(step.input, actor, db) const loaded = await loadScopedManualRecoveryAdmissionWithinOwner(step.input, actor, db)
const { source } = loaded const { source } = loaded
@@ -92,7 +94,7 @@ function expectedBinding(selected: NonNullable<Awaited<ReturnType<typeof selectM
return manualInverseBindingSchema.parse({revision:1,recoveryOperationId:operationId,recoveryPlanId:plan.id, return manualInverseBindingSchema.parse({revision:1,recoveryOperationId:operationId,recoveryPlanId:plan.id,
sourceOperationId:selected.source.row.id,sourcePlanId:selected.source.row.source_plan_id,sourceStepIndex:0,profileId:authority.profileId,profileVersion:authority.profileVersion, sourceOperationId:selected.source.row.id,sourcePlanId:selected.source.row.source_plan_id,sourceStepIndex:0,profileId:authority.profileId,profileVersion:authority.profileVersion,
connectionId:actor.connectionId,ownerId:authority.ownerId,originalPrincipal:principal.parse(selected.input.principal),recoveryPrincipal:actor, connectionId:actor.connectionId,ownerId:authority.ownerId,originalPrincipal:principal.parse(selected.input.principal),recoveryPrincipal:actor,
operationVersion:7,sourceVersion:7,authorityProof:selected.authority.proof,sourceProof:selected.source.sourceProof, operationVersion:plan.operation_version,sourceVersion:selected.source.row.source_version,authorityProof:selected.authority.proof,sourceProof:selected.source.sourceProof,
sourceRequestProof:selected.input.requestProof,recoveryRequestProof:selected.recoveryInput.requestProof, sourceRequestProof:selected.input.requestProof,recoveryRequestProof:selected.recoveryInput.requestProof,
sourceEvidenceProof:selected.sourceEvidenceProof,recoveryEvidenceProof:fingerprint(plan.evidence.sealed), sourceEvidenceProof:selected.sourceEvidenceProof,recoveryEvidenceProof:fingerprint(plan.evidence.sealed),
recoveryPayloadProof:fingerprint(plan.payload.sealed),scope:authority.scope,createdAt:observedAt,chargeUnits:1}) recoveryPayloadProof:fingerprint(plan.payload.sealed),scope:authority.scope,createdAt:observedAt,chargeUnits:1})
@@ -134,7 +136,7 @@ export async function admitManualInverseWithinOwner(db: PoolClient, operationId:
/** A committed charge is required before the existing manual native intent. /** A committed charge is required before the existing manual native intent.
* This authenticates identity only; existing live/native authority still runs. */ * This authenticates identity only; existing live/native authority still runs. */
export async function assertManualInverseChargeWithinOwner(db:PoolClient,operationId:string,input:Record<string,unknown>,actor:McpPrincipal) { export async function assertManualInverseChargeWithinOwner(db:PoolClient,operationId:string,input:Record<string,unknown>,actor:McpPrincipal) {
const rows=(await db.query<StoredPlan>("SELECT p.* FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id WHERE o.id=$1 AND o.connection_id=$2 AND o.state='running' AND p.state='executing' AND p.operation_id='operations.recover' AND p.operation_version=7",[operationId,actor.connectionId])).rows const rows=(await db.query<StoredPlan>("SELECT p.* FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id WHERE o.id=$1 AND o.connection_id=$2 AND o.state='running' AND p.state='executing' AND p.operation_id='operations.recover' AND p.operation_version=$3",[operationId,actor.connectionId,findMcpOperation("operations.recover")?.version])).rows
if(rows.length!==1) throw fail("Manual native claim requires current running action version") if(rows.length!==1) throw fail("Manual native claim requires current running action version")
const plan=rows[0], evidence=JSON.parse(await decryptSecretExisting(plan.evidence.sealed)) as ChangeEvidence const plan=rows[0], evidence=JSON.parse(await decryptSecretExisting(plan.evidence.sealed)) as ChangeEvidence
const step=evidence.steps[0] const step=evidence.steps[0]
@@ -154,7 +156,7 @@ export async function assertManualInverseChargeWithinOwner(db:PoolClient,operati
export async function authenticateManualInverseHistoryWithinOwner(db:PoolClient, export async function authenticateManualInverseHistoryWithinOwner(db:PoolClient,
intent:{revision?:number;manualInverseCharge?:ManualInverseReceipt;operationId:string;sourceOperationId:string;sourceReceiptId:string;sourceProof:string;requestSeal:string;connectionId:string;ownerId:string}, intent:{revision?:number;manualInverseCharge?:ManualInverseReceipt;operationId:string;sourceOperationId:string;sourceReceiptId:string;sourceProof:string;requestSeal:string;connectionId:string;ownerId:string},
authority:z.infer<typeof routingProfileAuthorityBindingSchema>,observedAt:string, authority:z.infer<typeof routingProfileAuthorityBindingSchema>,observedAt:string,
identity:{sourceEvidenceProof:string;recoveryEvidenceProof:string;recoveryPlanId:string|null;sourcePlanId:string|null;sourceRequestProof:unknown;sourcePrincipal:unknown;recoveryPrincipal:unknown}) { identity:{sourceEvidenceProof:string;recoveryEvidenceProof:string;recoveryPlanId:string|null;recoveryVersion:number|null;sourcePlanId:string|null;sourceRequestProof:unknown;sourcePrincipal:unknown;recoveryPrincipal:unknown}) {
const rows=(await db.query<ChargeRow>(`SELECT ${columns} FROM mcp_routing_manual_inverse_charges WHERE recovery_operation_id=$1 OR source_operation_id=$2 LIMIT 3`,[intent.operationId,intent.sourceOperationId])).rows const rows=(await db.query<ChargeRow>(`SELECT ${columns} FROM mcp_routing_manual_inverse_charges WHERE recovery_operation_id=$1 OR source_operation_id=$2 LIMIT 3`,[intent.operationId,intent.sourceOperationId])).rows
const copy = intent.revision === 2 ? await authenticateCopy(intent.manualInverseCharge, observedAt) : undefined const copy = intent.revision === 2 ? await authenticateCopy(intent.manualInverseCharge, observedAt) : undefined
if (!rows.length && !copy) return false if (!rows.length && !copy) return false
@@ -166,7 +168,8 @@ export async function authenticateManualInverseHistoryWithinOwner(db:PoolClient,
value.profileId!==authority.packet.profileId || value.profileVersion!==authority.packet.profileVersion || value.authorityProof!==authority.proof || value.profileId!==authority.packet.profileId || value.profileVersion!==authority.packet.profileVersion || value.authorityProof!==authority.proof ||
value.connectionId!==intent.connectionId || value.ownerId!==intent.ownerId || value.sourceProof!==intent.sourceProof || value.connectionId!==intent.connectionId || value.ownerId!==intent.ownerId || value.sourceProof!==intent.sourceProof ||
value.recoveryRequestProof!==intent.requestSeal || value.sourceEvidenceProof!==identity.sourceEvidenceProof || value.recoveryRequestProof!==intent.requestSeal || value.sourceEvidenceProof!==identity.sourceEvidenceProof ||
value.recoveryEvidenceProof!==identity.recoveryEvidenceProof || value.recoveryPlanId!==identity.recoveryPlanId || value.sourcePlanId!==identity.sourcePlanId)throw fail("Historical inverse is not represented by its signed allocation") value.recoveryEvidenceProof!==identity.recoveryEvidenceProof || value.recoveryPlanId!==identity.recoveryPlanId ||
value.operationVersion!==identity.recoveryVersion || value.sourcePlanId!==identity.sourcePlanId)throw fail("Historical inverse is not represented by its signed allocation")
// Only revision2 needs this extra one-row bounded projection; preserve the // Only revision2 needs this extra one-row bounded projection; preserve the
// existing cursor page payload ceiling and legacy protocol untouched. // existing cursor page payload ceiling and legacy protocol untouched.
const payload = copy ? (await db.query<{sealed:string|null}>("SELECT CASE WHEN octet_length(payload::text)<=1048576 THEN payload->>'sealed' END AS sealed FROM mcp_plans WHERE id=$1 LIMIT 2",[identity.recoveryPlanId])).rows : [] const payload = copy ? (await db.query<{sealed:string|null}>("SELECT CASE WHEN octet_length(payload::text)<=1048576 THEN payload->>'sealed' END AS sealed FROM mcp_plans WHERE id=$1 LIMIT 2",[identity.recoveryPlanId])).rows : []
@@ -201,11 +204,11 @@ export async function retainManualInverseChargesWithinOwner(db: PoolClient, rete
" WHERE r.mode='manual' AND r.source_operation_id=$1 AND r.source_step_index=0 AND r.recovery_operation_id=$2",[candidate.source_operation_id,candidate.recovery_operation_id])).rows " WHERE r.mode='manual' AND r.source_operation_id=$1 AND r.source_step_index=0 AND r.recovery_operation_id=$2",[candidate.source_operation_id,candidate.recovery_operation_id])).rows
if(rows.length!==1)throw fail("Retention intent/source pair disappeared") if(rows.length!==1)throw fail("Retention intent/source pair disappeared")
const row=rows[0],classified=await classifyManualInverseHistory(row,observedAt) const row=rows[0],classified=await classifyManualInverseHistory(row,observedAt)
if(classified.kind!=="persistent" || classified.intent.revision!==2 || row.recovery_version!==7 || if(classified.kind!=="persistent" || classified.intent.revision!==2 || row.recovery_version!==classified.intent.manualInverseCharge.binding.operationVersion ||
row.authority_profile_version!==candidate.profile_version || row.authority_profile_version!==classified.intent.manualInverseCharge.binding.profileVersion) row.authority_profile_version!==candidate.profile_version || row.authority_profile_version!==classified.intent.manualInverseCharge.binding.profileVersion)
throw fail("Retention hint is not an authenticated persistent intent2") throw fail("Retention hint is not an authenticated persistent intent2")
if(!await authenticateManualInverseHistoryWithinOwner(db,classified.intent,classified.authority,observedAt,{ if(!await authenticateManualInverseHistoryWithinOwner(db,classified.intent,classified.authority,observedAt,{
sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence),recoveryPlanId:row.recovery_plan_id,sourcePlanId:row.source_plan_id,sourceRequestProof:classified.sourceRequestProof,sourcePrincipal:classified.sourcePrincipal,recoveryPrincipal:classified.recoveryPrincipal})) sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence),recoveryPlanId:row.recovery_plan_id,recoveryVersion:row.recovery_version,sourcePlanId:row.source_plan_id,sourceRequestProof:classified.sourceRequestProof,sourcePrincipal:classified.sourcePrincipal,recoveryPrincipal:classified.recoveryPrincipal}))
throw fail("Retention parent is not authenticated") throw fail("Retention parent is not authenticated")
const result=await db.query("DELETE FROM mcp_routing_manual_inverse_charges WHERE recovery_operation_id=$1 AND source_operation_id=$2",[candidate.recovery_operation_id,candidate.source_operation_id]) const result=await db.query("DELETE FROM mcp_routing_manual_inverse_charges WHERE recovery_operation_id=$1 AND source_operation_id=$2",[candidate.recovery_operation_id,candidate.source_operation_id])
if(result.rowCount!==1)throw fail("Retention parent delete was not confirmed") if(result.rowCount!==1)throw fail("Retention parent delete was not confirmed")
+116 -7
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict" import assert from "node:assert/strict"
import { test } from "node:test" import { test } from "node:test"
import { routeAiProfileSchema, type RouteObservation } from "@mmapp/contracts/mcp-routing" import { routeAiProfileSchema, type RouteObservation } from "@mmapp/contracts/mcp-routing"
import { rankRoutingCandidates, decideRoutingSwitch, type ControllerState, type PathHealth } from "./routing-model.js" import { rankRoutingCandidates, decideRoutingSwitch, selectFailoverReserveProbes, type ControllerState, type PathHealth } from "./routing-model.js"
const now = Date.parse("2026-10-17T10:00:00Z"), at = new Date(now).toISOString() const now = Date.parse("2026-10-17T10:00:00Z"), at = new Date(now).toISOString()
const profile = routeAiProfileSchema.parse({ name: "Test", serverId: 1, community: "65000:10", family: "ipv4", candidates: [ const profile = routeAiProfileSchema.parse({ name: "Test", serverId: 1, community: "65000:10", family: "ipv4", candidates: [
@@ -43,14 +43,63 @@ test("new unknown attempts block older successful data independently for each me
test("failover requires three distinct failures and a freshly verified eligible standby", () => { test("failover requires three distinct failures and a freshly verified eligible standby", () => {
const ranking = rankRoutingCandidates(profile, [...a, sample("b1", 1, 4, "wan-b"), sample("b2", 4, 3, "exit-b")], now) const ranking = rankRoutingCandidates(profile, [...a, sample("b1", 1, 4, "wan-b"), sample("b2", 4, 3, "exit-b")], now)
let state: ControllerState = {} let state: ControllerState = {}
const health = (id: string, status: PathHealth["status"] = "failed"): PathHealth[] => [{ id, candidateId: "a", status, observedAt: at }, { id: "reserve", candidateId: "b", status: "passed", observedAt: at }] const health = (id: string, time = now, status: PathHealth["status"] = "failed"): PathHealth[] => {
const observedAt = new Date(time).toISOString()
return [{ id, candidateId: "a", status, observedAt }, { id: "reserve", candidateId: "b", status: "passed", observedAt }]
}
for (let i = 0; i < 5; i++) { const next = decideRoutingSwitch(profile, state, ranking, "a", now, health("one")); state = next.state; assert.equal(next.switchTo, null) } for (let i = 0; i < 5; i++) { const next = decideRoutingSwitch(profile, state, ranking, "a", now, health("one")); state = next.state; assert.equal(next.switchTo, null) }
assert.equal(state.failureCount, 1) assert.equal(state.failureCount, 1)
const second = decideRoutingSwitch(profile, state, ranking, "a", now, health("two")); assert.equal(second.switchTo, null) const secondAt = now + 60000, thirdAt = now + 120000
assert.equal(decideRoutingSwitch(profile, second.state, ranking, "a", now, health("three")).switchTo, "b") const second = decideRoutingSwitch(profile, state, ranking, "a", secondAt, health("two", secondAt)); assert.equal(second.switchTo, null)
assert.equal(decideRoutingSwitch(profile, second.state, ranking, "a", now, health("three", "unknown")).switchTo, null) assert.equal(decideRoutingSwitch(profile, second.state, ranking, "a", thirdAt, health("three", thirdAt)).switchTo, "b")
assert.equal(decideRoutingSwitch(profile, { ...second.state, switches: [at, at, at] }, ranking, "a", now, health("three")).switchTo, null) const unknown = decideRoutingSwitch(profile, second.state, ranking, "a", thirdAt, health("three", thirdAt, "unknown"))
assert.equal(decideRoutingSwitch(profile, second.state, ranking, "a", now + 100000, health("three")).switchTo, null) assert.equal(unknown.switchTo, null)
assert.equal(unknown.state.failureObservedAt, undefined)
assert.equal(decideRoutingSwitch(profile, { ...second.state, switches: [at, at, at] }, ranking, "a", thirdAt, health("three", thirdAt)).switchTo, null)
assert.equal(decideRoutingSwitch(profile, second.state, ranking, "a", thirdAt + 100000, health("three", thirdAt)).switchTo, null)
})
test("failover starts a fresh series after downtime or legacy state and ignores reordered failures", () => {
const ranking = rankRoutingCandidates(profile, [...a, sample("b1", 1, 4, "wan-b"), sample("b2", 4, 3, "exit-b")], now)
const last = now + 60000
const persisted: ControllerState = JSON.parse(JSON.stringify({ failureCount: 2, failureKey: "two",
failureObservedAt: new Date(last).toISOString(), failingCandidateId: "a" }))
const check = (state: ControllerState, id: string, time: number, clock = time) => {
const observedAt = new Date(time).toISOString()
return decideRoutingSwitch(profile, state, ranking, "a", clock,
[{ id, candidateId: "a", status: "failed", observedAt }, { id: "reserve", candidateId: "b", status: "passed", observedAt }])
}
for (const result of [check(persisted, "two", last + 60000), check(persisted, "older", last - 1000, last),
check(persisted, "clock-rollback", last - 1000), check(persisted, "equal-time", last)]) {
assert.equal(result.switchTo, null)
assert.equal(result.state.failureCount, 2)
assert.equal(result.state.failureObservedAt, persisted.failureObservedAt)
}
for (const [state, time] of [[persisted, now + 4 * 3600000], [{ ...persisted, failureObservedAt: undefined }, last + 60000]] as const) {
const result = check(state, "next", time)
assert.equal(result.switchTo, null)
assert.equal(result.state.failureCount, 1)
assert.equal(result.state.failureObservedAt, new Date(time).toISOString())
}
})
test("bounded failover probing keeps the highest eligible reserve and eventually visits every other eligible probe", () => {
const controlProbe = { targetInterface: "exit-a", expectedHops: [{ serverId: 3, interface: "exit-a" }] }
const configured = routeAiProfileSchema.parse({ ...profile, excludedCandidateIds: ["05"],
candidates: Array.from({ length: 100 }, (_, index) => ({ ...profile.candidates[0], id: String(index).padStart(2, "0"),
controlProbe: index === 4 ? undefined : controlProbe })) })
const ranking = rankRoutingCandidates(configured, a, now)
const expected = ranking.filter(row => row.eligible && row.candidate.controlProbe && !["00", "01"].includes(row.candidate.id)).map(row => row.candidate.id)
let cursor: string | undefined
for (const alternate of expected) {
const probes = selectFailoverReserveProbes(ranking, "00", cursor)
assert.deepEqual(probes.candidateIds, ["01", alternate])
assert.equal(probes.cursor, alternate)
cursor = probes.cursor
}
assert.deepEqual(selectFailoverReserveProbes(ranking, "00", cursor).candidateIds, ["01", "02"])
assert.deepEqual(selectFailoverReserveProbes(ranking, "00", "05").candidateIds, ["01", "02"])
const pinned = rankRoutingCandidates({ ...configured, pinnedCandidateId: "09" }, a, now)
assert.deepEqual(selectFailoverReserveProbes(pinned, "00"), { candidateIds: ["09"], cursor: undefined })
assert.deepEqual(selectFailoverReserveProbes(pinned, "09"), { candidateIds: [], cursor: undefined })
}) })
test("routing combines RTT, independent losses and bottleneck speed, never claims an observed path", () => { test("routing combines RTT, independent losses and bottleneck speed, never claims an observed path", () => {
const ranked = rankRoutingCandidates(profile, a, now).find(row => row.candidate.id === "a")! const ranked = rankRoutingCandidates(profile, a, now).find(row => row.candidate.id === "a")!
@@ -92,3 +141,63 @@ test("controller does not count polling the same sample as consecutive confirmat
assert.equal(decideRoutingSwitch(profile, { ...state, switches: [at, at, at] }, ranks, "a", now).switchTo, null) assert.equal(decideRoutingSwitch(profile, { ...state, switches: [at, at, at] }, ranks, "a", now).switchTo, null)
assert.equal(decideRoutingSwitch(profile, state, ranks, null, now).switchTo, null) assert.equal(decideRoutingSwitch(profile, state, ranks, null, now).switchTo, null)
}) })
test("hysteresis retains pending confirmation without counting sub-threshold observations or releasing cooldown", () => {
const ranked = rankRoutingCandidates(profile, [...a, sample("b1", 1, 4, "wan-b"), sample("b2", 4, 3, "exit-b")], now)
const compare = (state: ControllerState, gain: number, observation: string, time = now) => {
const ranks = ["b", "a"].map(id => ({ ...ranked.find(row => row.candidate.id === id)!,
score: id === "a" ? 50 : 50 + gain, observationIds: [id + observation] }))
return decideRoutingSwitch(profile, state, ranks, "a", time)
}
const initial = compare({}, 15, "1").state
assert.equal(initial.consecutive, 1)
for (const [gain, observation] of [[6, "2"], [5, "3"]] as const) {
const result = compare(initial, gain, observation)
assert.equal(result.switchTo, null)
assert.deepEqual(result.state, initial)
}
const reset = compare(initial, 4, "4")
assert.equal(reset.state.pendingCandidateId, undefined)
assert.equal(reset.state.consecutive, 0)
const second = compare(initial, 15, "5")
assert.equal(second.switchTo, null)
const third = compare({ ...second.state, lastSwitchAt: at }, 15, "6")
assert.equal(third.state.consecutive, 3)
assert.equal(third.switchTo, null)
const afterCooldown = now + profile.cooldownSec * 1000
assert.equal(compare(third.state, 6, "7", afterCooldown).switchTo, null)
assert.equal(compare(third.state, 15, "8", afterCooldown).switchTo, "b")
})
test("a different pending candidate cannot borrow confirmations from a shared observation key", () => {
const ranks = rankRoutingCandidates(profile, [...a, sample("b1", 1, 4, "wan-b"), sample("b2", 4, 3, "exit-b")], now)
.map(row => ({ ...row, score: row.candidate.id === "a" ? 50 : 75 }))
.sort((left, right) => right.score - left.score)
const observationKey = ranks.flatMap(row => row.observationIds).sort().join(":")
const persisted: ControllerState = { pendingCandidateId: "previous-candidate", consecutive: 3, observationKey }
const selected = decideRoutingSwitch(profile, persisted, ranks, "a", now)
assert.equal(selected.switchTo, null)
assert.equal(selected.state.pendingCandidateId, "b")
assert.equal(selected.state.consecutive, 0)
assert.equal(selected.state.observationKey, observationKey)
const replay = decideRoutingSwitch(profile, selected.state, ranks, "a", now)
assert.equal(replay.switchTo, null)
assert.equal(replay.state.consecutive, 0)
const freshRanks = [{ ...ranks[0], observationIds: ["new-b-1", "new-b-2"] }, ranks[1]]
const fresh = decideRoutingSwitch(profile, replay.state, freshRanks, "a", now)
assert.equal(fresh.state.consecutive, 1)
assert.equal(fresh.switchTo, null)
const belowFloor = [{ ...freshRanks[0], score: 54 }, freshRanks[1]]
const reset = decideRoutingSwitch(profile, fresh.state, belowFloor, "a", now)
assert.equal(reset.state.pendingCandidateId, undefined)
assert.equal(reset.state.consecutive, 0)
const replayAfterReset = decideRoutingSwitch(profile, reset.state, freshRanks, "a", now)
assert.equal(replayAfterReset.switchTo, null)
assert.equal(replayAfterReset.state.consecutive, 0)
})
+31 -9
View File
@@ -51,36 +51,58 @@ export function rankRoutingCandidates(profile: RouteAiProfile, observations: Rou
} }
export interface PathHealth { id: string; candidateId: string; status: "passed" | "failed" | "unknown"; observedAt: string } export interface PathHealth { id: string; candidateId: string; status: "passed" | "failed" | "unknown"; observedAt: string }
export interface ControllerState { pendingCandidateId?: string; consecutive?: number; observationKey?: string; lastSwitchAt?: string; switches?: string[]; operationId?: string; pausedReason?: string; failureCount?: number; failureKey?: string; failingCandidateId?: string } export interface ControllerState { pendingCandidateId?: string; consecutive?: number; observationKey?: string; lastSwitchAt?: string; switches?: string[]; operationId?: string; pausedReason?: string; failureCount?: number; failureKey?: string; failureObservedAt?: string; failingCandidateId?: string }
export function selectFailoverReserveProbes(ranking: ReturnType<typeof rankRoutingCandidates>, currentCandidateId: string, previous?: string) {
const standbys = ranking.filter(({ candidate, eligible }) => eligible && candidate.id !== currentCandidateId && candidate.controlProbe)
.map(({ candidate }) => candidate.id)
const [preferred, ...remaining] = standbys
if (remaining.length === 0) return { candidateIds: standbys, cursor: undefined }
const index = remaining.indexOf(previous ?? "")
const cursor = remaining[(index + 1) % remaining.length]
return { candidateIds: [preferred, cursor], cursor }
}
const pathHealthFreshnessSec = 90
export function decideRoutingSwitch(profile: RouteAiProfile, state: ControllerState, ranking: ReturnType<typeof rankRoutingCandidates>, currentCandidateId: string | null, now = Date.now(), health: PathHealth[] = [], chargedSwitchesPerHour?: number) { export function decideRoutingSwitch(profile: RouteAiProfile, state: ControllerState, ranking: ReturnType<typeof rankRoutingCandidates>, currentCandidateId: string | null, now = Date.now(), health: PathHealth[] = [], chargedSwitchesPerHour?: number) {
const next: ControllerState = { ...state, switches: (state.switches ?? []).filter(at => now - Date.parse(at) < 3600000) } const next: ControllerState = { ...state, switches: (state.switches ?? []).filter(at => now - Date.parse(at) < 3600000) }
if (chargedSwitchesPerHour !== undefined && (!Number.isSafeInteger(chargedSwitchesPerHour) || chargedSwitchesPerHour < next.switches!.length || chargedSwitchesPerHour > 4096)) throw Error("Invalid common routing charge count") if (chargedSwitchesPerHour !== undefined && (!Number.isSafeInteger(chargedSwitchesPerHour) || chargedSwitchesPerHour < next.switches!.length || chargedSwitchesPerHour > 4096)) throw Error("Invalid common routing charge count")
const switchCount = chargedSwitchesPerHour ?? next.switches!.length const switchCount = chargedSwitchesPerHour ?? next.switches!.length
const best = ranking.find(candidate => candidate.eligible), current = ranking.find(candidate => candidate.candidate.id === currentCandidateId) const best = ranking.find(candidate => candidate.eligible), current = ranking.find(candidate => candidate.candidate.id === currentCandidateId)
const currentHealth = health.find(value => value.candidateId === currentCandidateId && fresh(value.observedAt, 90, now)) const currentHealth = health.find(value => value.candidateId === currentCandidateId && fresh(value.observedAt, pathHealthFreshnessSec, now))
if (currentHealth?.status === "failed" && currentCandidateId && !state.pausedReason) { if (currentHealth?.status === "failed" && currentCandidateId && !state.pausedReason) {
if (state.failureKey !== currentHealth.id) { const observed = Date.parse(currentHealth.observedAt), previous = Date.parse(state.failureObservedAt ?? "")
next.failureCount = state.failingCandidateId === currentCandidateId ? (state.failureCount ?? 0) + 1 : 1 const sameCandidate = state.failingCandidateId === currentCandidateId
next.failureKey = currentHealth.id; next.failingCandidateId = currentCandidateId if (state.failureKey === currentHealth.id) {
if (!sameCandidate || observed !== previous) return { state: next, switchTo: null, reason: "Ожидание новой проверки отказа" }
} else {
if (observed <= previous) return { state: next, switchTo: null, reason: "Ожидание новой проверки отказа" }
const continues = sameCandidate && fresh(state.failureObservedAt ?? null, pathHealthFreshnessSec, observed)
next.failureCount = continues ? (state.failureCount ?? 0) + 1 : 1
next.failureKey = currentHealth.id; next.failureObservedAt = currentHealth.observedAt; next.failingCandidateId = currentCandidateId
} }
const reserve = ranking.find(value => value.eligible && value.candidate.id !== currentCandidateId && health.some(check => check.candidateId === value.candidate.id && check.status === "passed" && fresh(check.observedAt, 90, now))) const reserve = ranking.find(value => value.eligible && value.candidate.id !== currentCandidateId && health.some(check => check.candidateId === value.candidate.id && check.status === "passed" && fresh(check.observedAt, pathHealthFreshnessSec, now)))
if ((next.failureCount ?? 0) < 3 || !reserve) return { state: next, switchTo: null, reason: "Failover ожидает три отдельных отказа и подтверждённый резерв" } if ((next.failureCount ?? 0) < 3 || !reserve) return { state: next, switchTo: null, reason: "Failover ожидает три отдельных отказа и подтверждённый резерв" }
if (switchCount >= profile.maxSwitchesPerHour) return { state: { ...next, pausedReason: "Лимит переключений" }, switchTo: null, reason: "Исчерпан часовой бюджет, включая failover" } if (switchCount >= profile.maxSwitchesPerHour) return { state: { ...next, pausedReason: "Лимит переключений" }, switchTo: null, reason: "Исчерпан часовой бюджет, включая failover" }
if (next.lastSwitchAt && now - Date.parse(next.lastSwitchAt) < profile.cooldownSec * 1000) return { state: next, switchTo: null, reason: "Failover ожидает окончания cooldown" } if (next.lastSwitchAt && now - Date.parse(next.lastSwitchAt) < profile.cooldownSec * 1000) return { state: next, switchTo: null, reason: "Failover ожидает окончания cooldown" }
return { state: { ...next, failureCount: 0 }, switchTo: reserve.candidate.id, reason: "Failover: три подтверждённых отказа и проверенный резерв" } return { state: { ...next, failureCount: 0 }, switchTo: reserve.candidate.id, reason: "Failover: три подтверждённых отказа и проверенный резерв" }
} }
next.failureCount = 0; next.failureKey = undefined; next.failingCandidateId = undefined next.failureCount = 0; next.failureKey = undefined; next.failureObservedAt = undefined; next.failingCandidateId = undefined
if (state.pausedReason || !currentCandidateId || !best || best.candidate.id === currentCandidateId) return { state: { ...next, pendingCandidateId: undefined, consecutive: 0 }, switchTo: null, reason: "Нет подтверждённого улучшения или текущего пути" } if (state.pausedReason || !currentCandidateId || !best || best.candidate.id === currentCandidateId) return { state: { ...next, pendingCandidateId: undefined, consecutive: 0 }, switchTo: null, reason: "Нет подтверждённого улучшения или текущего пути" }
if (!current?.eligible || current.score === null) return { state: next, switchTo: null, reason: "Состояние текущего пути неизвестно; failover требует отдельных подтверждённых проверок отказа" } if (!current?.eligible || current.score === null) return { state: next, switchTo: null, reason: "Состояние текущего пути неизвестно; failover требует отдельных подтверждённых проверок отказа" }
const gain = best.score! - current.score const gain = best.score! - current.score
if (gain < Math.max(0, profile.switchThreshold - profile.hysteresis)) return { state: { ...next, consecutive: 0, pendingCandidateId: undefined }, switchTo: null, reason: "Сработал гистерезис" } if (gain < Math.max(0, profile.switchThreshold - profile.hysteresis)) return { state: { ...next, consecutive: 0, pendingCandidateId: undefined }, switchTo: null, reason: "Сработал гистерезис" }
if (gain < profile.switchThreshold && next.pendingCandidateId !== best.candidate.id) return { state: next, switchTo: null, reason: "Порог улучшения не достигнут" } if (gain < profile.switchThreshold) return { state: next, switchTo: null, reason: "Порог улучшения не достигнут" }
const observationKey = [...best.observationIds, ...current.observationIds].sort().join(":") const observationKey = [...best.observationIds, ...current.observationIds].sort().join(":")
if (next.pendingCandidateId !== undefined && next.pendingCandidateId !== best.candidate.id) {
next.pendingCandidateId = best.candidate.id
next.consecutive = 0
}
if (observationKey !== next.observationKey) { if (observationKey !== next.observationKey) {
next.consecutive = next.pendingCandidateId === best.candidate.id ? (next.consecutive ?? 0) + 1 : 1 next.consecutive = next.pendingCandidateId === best.candidate.id ? (next.consecutive ?? 0) + 1 : 1
next.pendingCandidateId = best.candidate.id; next.observationKey = observationKey next.pendingCandidateId = best.candidate.id; next.observationKey = observationKey
} }
if ((next.consecutive ?? 0) < profile.confirmationSamples) return { state: next, switchTo: null, reason: "Ожидание новых измерений" } if (next.pendingCandidateId !== best.candidate.id || (next.consecutive ?? 0) < profile.confirmationSamples) return { state: next, switchTo: null, reason: "Ожидание новых измерений" }
if (next.lastSwitchAt && now - Date.parse(next.lastSwitchAt) < profile.cooldownSec * 1000) return { state: next, switchTo: null, reason: "Cooldown" } if (next.lastSwitchAt && now - Date.parse(next.lastSwitchAt) < profile.cooldownSec * 1000) return { state: next, switchTo: null, reason: "Cooldown" }
if (switchCount >= profile.maxSwitchesPerHour) return { state: { ...next, pausedReason: "Лимит переключений" }, switchTo: null, reason: "Исчерпан часовой бюджет переключений" } if (switchCount >= profile.maxSwitchesPerHour) return { state: { ...next, pausedReason: "Лимит переключений" }, switchTo: null, reason: "Исчерпан часовой бюджет переключений" }
return { state: next, switchTo: best.candidate.id, reason: "Улучшение подтверждено свежими наблюдениями" } return { state: next, switchTo: best.candidate.id, reason: "Улучшение подтверждено свежими наблюдениями" }
@@ -0,0 +1,28 @@
import { isIP } from "node:net"
export function parseRoutingEndpoint(value: unknown) {
if (typeof value !== "string" || !value.length || value.length > 256) throw Error("missing-or-unsupported-field")
const match = /^([^%@/,\s]+)(?:\/(\d+))?(?:%([^%@/,\s]+))?(?:@([^%@/,\s]+))?$/.exec(value)
if (!match) throw Error("unsupported-endpoint")
const [, address, prefix, interfaceName, vrf] = match
const family = isIP(address)
if (!family) throw Error("unsupported-address")
if (prefix !== undefined && prefix !== String(family === 4 ? 32 : 128)) throw Error("unsupported-peer-range")
return { address, interface: interfaceName, vrf }
}
export function matchesBgpSessionContext(
row: Record<string, unknown>,
expected: { table: string; vrf: string },
local: ReturnType<typeof parseRoutingEndpoint>,
remote: ReturnType<typeof parseRoutingEndpoint>,
) {
if (!Object.hasOwn(row, "routing-table") || row["routing-table"] !== expected.table) return false
if (Object.hasOwn(row, "vrf")) {
return row.vrf === expected.vrf && (local.vrf === undefined || local.vrf === expected.vrf) &&
(remote.vrf === undefined || remote.vrf === expected.vrf)
}
// RouterOS may report the transport VRF only in the two runtime addresses.
// The installation table alone cannot identify it.
return local.vrf === expected.vrf && remote.vrf === expected.vrf
}
@@ -16,6 +16,8 @@ assert(process.env.MCP_STATE_DIR && isAbsolute(process.env.MCP_STATE_DIR))
assert.equal(basename(resolve(process.env.MCP_STATE_DIR)), "state") assert.equal(basename(resolve(process.env.MCP_STATE_DIR)), "state")
assert(basename(dirname(resolve(process.env.MCP_STATE_DIR))).startsWith("mm-mcp-test-")) assert(basename(dirname(resolve(process.env.MCP_STATE_DIR))).startsWith("mm-mcp-test-"))
const originalEffects = process.env.MMAPP_OWN_ROUTING_ORIGINAL_EFFECTS === "true" const originalEffects = process.env.MMAPP_OWN_ROUTING_ORIGINAL_EFFECTS === "true"
const controllerFailoverOnly = process.env.MMAPP_OWN_ROUTING_CONTROLLER_FAILOVER_ONLY === "true"
assert(!controllerFailoverOnly || originalEffects, "Focused controller failover requires the owned original-effects fixture")
assert(!(originalEffects && process.env.MMAPP_OWN_ROUTING_WORKER_REPRO === "true"), "Owned effect acceptance cannot mix with the worker repro profile") assert(!(originalEffects && process.env.MMAPP_OWN_ROUTING_WORKER_REPRO === "true"), "Owned effect acceptance cannot mix with the worker repro profile")
process.env.AUTH_REQUIRED = "true" process.env.AUTH_REQUIRED = "true"
process.env.AUTH_JWT_SECRET = "owned-routing-forward-rounds-acceptance-secret" process.env.AUTH_JWT_SECRET = "owned-routing-forward-rounds-acceptance-secret"
@@ -35,7 +37,7 @@ try {
console.log("Worker fixture repro:",await (await import("./routing-worker-fixture-isolation.sql-helper.js")).acceptRoutingWorkerFixtureIsolation(actor)) console.log("Worker fixture repro:",await (await import("./routing-worker-fixture-isolation.sql-helper.js")).acceptRoutingWorkerFixtureIsolation(actor))
} }
const result = await (await import("./routing-community-scoped-acceptance.js")).acceptScopedRouteCommunitySwitch(actor, const result = await (await import("./routing-community-scoped-acceptance.js")).acceptScopedRouteCommunitySwitch(actor,
originalEffects ? { originalEffectsOnly: true } : process.env.MMAPP_OWN_ROUTING_WORKER_REPRO==="true"?{}:{ forwardRoundsOnly: true }) originalEffects ? { originalEffectsOnly: true, controllerFailoverOnly } : process.env.MMAPP_OWN_ROUTING_WORKER_REPRO==="true"?{}:{ forwardRoundsOnly: true })
assert.equal(result.accepted, true) assert.equal(result.accepted, true)
} finally { } finally {
await (await import("../../services/scheduler.js")).stopSchedulerForDatabaseOperation() await (await import("../../services/scheduler.js")).stopSchedulerForDatabaseOperation()
@@ -154,6 +154,25 @@ for (const mutation of ["duplicate-config", "wrong-config-id", "unresolved-templ
assert.equal(f.parser.parseBgp("peer1", raw).status, "unknown") assert.equal(f.parser.parseBgp("peer1", raw).status, "unknown")
}) })
for (const ipv6 of [false, true]) test("BGP runtime context can come from both exact qualified " + (ipv6 ? "IPv6" : "IPv4") + " endpoints", () => {
const f = fixture(ipv6), raw = structuredClone(f.bgp) as unknown as RawMutation, session = raw.payload[0]
const local = session["local.address"], remote = session["remote.address"]
delete session.vrf
session["local.address"] = `${local}@main`
session["remote.address"] = `${remote}@main`
const observation = f.parser.parseBgp("peer1", raw)
assert.equal(observation.status, "observed", JSON.stringify(observation))
assert.equal(observation.securityProof, false)
for (const mutation of [
{ "remote.address": remote }, { "remote.address": `${remote}@tenant` },
{ "remote.address": `${remote}@main@main` }, { "routing-table": "main" },
{ vrf: "tenant" }, { vrf: null },
]) {
assert.equal(f.parser.parseBgp("peer1", { ...raw, payload: [{ ...session, ...mutation }] }).status, "unknown")
}
assert.equal(f.parser.parseBgp("peer1", { ...raw, payload: [session, { ...session, ".id": "*B3" }] }).status, "unknown")
})
test("native cached non-established BGP cannot invent a model FSM state from an undocumented field", () => { test("native cached non-established BGP cannot invent a model FSM state from an undocumented field", () => {
const f = fixture(), raw = structuredClone(f.bgp) as unknown as RawMutation const f = fixture(), raw = structuredClone(f.bgp) as unknown as RawMutation
raw.payload[0].established = "false" raw.payload[0].established = "false"
@@ -2,6 +2,7 @@ import { isIP } from "node:net"
import { z } from "zod" import { z } from "zod"
import { canonicalBgpPolicyPrefix } from "../../services/bgp-community-policy-model.js" import { canonicalBgpPolicyPrefix } from "../../services/bgp-community-policy-model.js"
import { captureRoutingNetworkManifest, type RoutingNetworkRound } from "./routing-network-verifier-model.js" import { captureRoutingNetworkManifest, type RoutingNetworkRound } from "./routing-network-verifier-model.js"
import { matchesBgpSessionContext, parseRoutingEndpoint } from "./routing-native-endpoint.js"
/** Pure decoded-response parsers, NOT source authentication or completeness /** Pure decoded-response parsers, NOT source authentication or completeness
* proof. Only the explicit RouterOS7.20–7.26 field subset below is recognized. * proof. Only the explicit RouterOS7.20–7.26 field subset below is recognized.
@@ -32,11 +33,8 @@ function canonicalAddress(value: unknown): string {
return canonicalBgpPolicyPrefix(text + "/" + (family === 4 ? 32 : 128)).split("/")[0] return canonicalBgpPolicyPrefix(text + "/" + (family === 4 ? 32 : 128)).split("/")[0]
} }
function endpoint(value: unknown) { function endpoint(value: unknown) {
const text = literal(value), match = /^([^%@/,\s]+)(?:\/(\d+))?(?:%([^%@/,\s]+))?(?:@([^%@/,\s]+))?$/.exec(text) const parsed = parseRoutingEndpoint(value)
if (!match) return fail("unsupported-endpoint") return { ...parsed, address: canonicalAddress(parsed.address) }
const address = canonicalAddress(match[1])
if (match[2] !== undefined && match[2] !== String(isIP(address) === 4 ? 32 : 128)) return fail("unsupported-peer-range")
return { address, interface: match[3], vrf: match[4] }
} }
function decodedRows(payload: unknown, maximum = 1000, ids = true): Row[] { function decodedRows(payload: unknown, maximum = 1000, ids = true): Row[] {
let nodes = 0 let nodes = 0
@@ -144,7 +142,7 @@ export function captureRoutingNetworkNativeParser(rawManifest: unknown) {
if (instance !== wanted.instance) continue if (instance !== wanted.instance) continue
if (local.address !== wanted.localAddress || remote.address !== wanted.remoteAddress) continue if (local.address !== wanted.localAddress || remote.address !== wanted.remoteAddress) continue
if (integer(row["remote.as"], 4294967295) !== wanted.remoteAsn || !afi(row["remote.afi"]).includes(expectedAfi) || !afi(row["local.afi"]).includes(expectedAfi)) return unknown("bgp-session-asn-or-afi-drift") if (integer(row["remote.as"], 4294967295) !== wanted.remoteAsn || !afi(row["remote.afi"]).includes(expectedAfi) || !afi(row["local.afi"]).includes(expectedAfi)) return unknown("bgp-session-asn-or-afi-drift")
if (row.vrf !== binding.vrf || row["routing-table"] !== binding.table || [local.vrf, remote.vrf].some(vrf => vrf !== undefined && vrf !== binding.vrf) || local.interface !== binding.local.interface || remote.interface !== binding.remote.interface) return unknown("bgp-session-context-drift") if (!matchesBgpSessionContext(row, binding, local, remote) || local.interface !== binding.local.interface || remote.interface !== binding.remote.interface) return unknown("bgp-session-context-drift")
relevant.push(row) relevant.push(row)
} }
if (relevant.length !== 1) return unknown("missing-or-ambiguous-bgp-session") if (relevant.length !== 1) return unknown("missing-or-ambiguous-bgp-session")
@@ -14,7 +14,9 @@ import { nativeBgpRows, bgpTemplateReferences } from "../../services/bgp-export-
import { bgpFilterName, effectiveBgpFilter, stableBgpPolicyRow } from "../../services/bgp-filter-binding-model.js" import { bgpFilterName, effectiveBgpFilter, stableBgpPolicyRow } from "../../services/bgp-filter-binding-model.js"
import { canonicalVrfMembers } from "../../services/vrf-members.js" import { canonicalVrfMembers } from "../../services/vrf-members.js"
import { readOspfEffectiveDisabled } from "../../services/ospf-effective-disabled.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 * as contextModel from "./routing-community-context-model.js"
import { prepareOriginalEffectSelection } from "./routing-original-effects.js"
import { readMeasurementAddress } from "./routing-measurement-address.js" import { readMeasurementAddress } from "./routing-measurement-address.js"
import { routeContextClients } from "./routing-community-context-native.js" import { routeContextClients } from "./routing-community-context-native.js"
import { pathProofTag } from "./path-proof-purpose.js" import { pathProofTag } from "./path-proof-purpose.js"
@@ -65,9 +67,8 @@ async function fixture(autoRecovery = false) {
afterHttp?.(); afterHttp = undefined afterHttp?.(); afterHttp = undefined
if (afterPath?.path === path) { const run = afterPath.run; afterPath = undefined; run() } if (afterPath?.path === path) { const run = afterPath.run; afterPath = undefined; run() }
}) })
await new Promise<void>(resolve => router.listen(0, "127.0.0.1", resolve))
const now = Date.now(), fingerprint = (value: unknown) => createHash("sha256").update(JSON.stringify(value)).digest("hex") const now = Date.now(), fingerprint = (value: unknown) => createHash("sha256").update(JSON.stringify(value)).digest("hex")
const endpoint = { id: 1, host: "127.0.0.1", port: (router.address() as { port: number }).port, username: "fixture", password: "private", useSsl: false, verifySsl: true, enabled: true, routingEndpointEpoch: randomUUID() } const endpoint = { id: 1, host: "127.0.0.1", port: 0, username: "fixture", password: "private", useSsl: false, verifySsl: true, enabled: true, routingEndpointEpoch: randomUUID() }
const principal = { connectionId: randomUUID(), credentialId: randomUUID(), credentialExpiresAt: new Date(now + 600000).toISOString() } const principal = { connectionId: randomUUID(), credentialId: randomUUID(), credentialExpiresAt: new Date(now + 600000).toISOString() }
const connection = { id: principal.connectionId, owner_id: "owner", generation: "generation", owner_permissions: ["mm:network:read"], const connection = { id: principal.connectionId, owner_id: "owner", generation: "generation", owner_permissions: ["mm:network:read"],
policy: { permissions: ["mm:network:read"], scope: { serverIds: [1] }, expiresAt: new Date(now + 600000).toISOString() }, owner_valid_until: new Date(now + 600000) } policy: { permissions: ["mm:network:read"], scope: { serverIds: [1] }, expiresAt: new Date(now + 600000).toISOString() }, owner_valid_until: new Date(now + 600000) }
@@ -92,6 +93,7 @@ async function fixture(autoRecovery = false) {
"./routing-community-context-native.js": { routeContextClients }, "../../services/vrf-members.js": { canonicalVrfMembers }, "../../services/ospf-effective-disabled.js": { readOspfEffectiveDisabled }, "./routing-community-context-native.js": { routeContextClients }, "../../services/vrf-members.js": { canonicalVrfMembers }, "../../services/ospf-effective-disabled.js": { readOspfEffectiveDisabled },
"./routing-measurement-address.js": { readMeasurementAddress } } "./routing-measurement-address.js": { readMeasurementAddress } }
await loadSource("./routing-community-context.ts", { ...common, "./routing-network-native-session.js": native, 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 }, "../../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, "./path-proof-purpose.js": { pathProofTag },
"./management-transport.js": { assertManagementTransport: async () => { throw Error("No alternate management fixture") } } }, context) "./management-transport.js": { assertManagementTransport: async () => { throw Error("No alternate management fixture") } } }, context)
@@ -103,14 +105,23 @@ async function fixture(autoRecovery = false) {
await loadSource("./routing-network-scoped-context.ts", { "./routing-network-native-session.js": native }, privateApi) await loadSource("./routing-network-scoped-context.ts", { "./routing-network-native-session.js": native }, privateApi)
const create = native.createRoutingNetworkNativeSession as (options: unknown) => Promise<Session> const create = native.createRoutingNetworkNativeSession as (options: unknown) => Promise<Session>
const abort = new AbortController() const abort = new AbortController()
const session = await create({ sourceOperationId: row.id, context: { principal, connection, signal: abort.signal }, authorize: authority, deadline: now + 119000, manifest: { servers: [1], probes: [] } }) await new Promise<void>(resolve => router.listen(0, "127.0.0.1", resolve))
endpoint.port = (router.address() as { port: number }).port
const closeRouter = () => {
router.closeAllConnections()
return new Promise<void>(resolve => router.close(() => resolve()))
}
const session = await create({ sourceOperationId: row.id, context: { principal, connection, signal: abort.signal }, authorize: authority, deadline: now + 119000, manifest: { servers: [1], probes: [] } }).catch(async error => {
await closeRouter()
throw error
})
const readContext = privateApi.readRoutingNetworkScopedCommunityContext as (token: Token) => Promise<Context> const readContext = privateApi.readRoutingNetworkScopedCommunityContext as (token: Token) => Promise<Context>
return { session, readContext: () => readContext(session.sharedContext), forged: () => readContext(JSON.parse(JSON.stringify(session.sharedContext))), return { session, readContext: () => readContext(session.sharedContext), forged: () => readContext(JSON.parse(JSON.stringify(session.sharedContext))),
readResource: native.readRoutingNetworkSharedResource as (token: Token, server: number, path: string, body?: Record<string, string>) => Promise<unknown>, readResource: native.readRoutingNetworkSharedResource as (token: Token, server: number, path: string, body?: Record<string, string>) => Promise<unknown>,
bypassTail: context.readRouteCommunityContextWithSharedNativeSession as (token: Token) => Promise<unknown>, bypassTail: context.readRouteCommunityContextWithSharedNativeSession as (token: Token) => Promise<unknown>,
counters: () => ({ requests, wireBytes, wireRows, rawGetObjects }), bulk: (value: number) => { bulk = value }, padding: (value: number) => { padding = value }, counters: () => ({ requests, wireBytes, wireRows, rawGetObjects }), bulk: (value: number) => { bulk = value }, padding: (value: number) => { padding = value },
revoke: () => { revoked = true }, abort: () => abort.abort(), afterHttp: (hook: () => void) => { afterHttp = hook }, afterPath: (path: string, hook: () => void) => { afterPath = { path, run: hook } }, revoke: () => { revoked = true }, abort: () => abort.abort(), afterHttp: (hook: () => void) => { afterHttp = hook }, afterPath: (path: string, hook: () => void) => { afterPath = { path, run: hook } },
cleanup: async () => { session.close(); router.closeAllConnections(); await new Promise<void>(resolve => router.close(() => resolve())) } } cleanup: async () => { session.close(); await closeRouter() } }
} }
test("actual source context and snapshots share one pinned HTTP request/byte/row budget including object /get", async () => { test("actual source context and snapshots share one pinned HTTP request/byte/row budget including object /get", async () => {
@@ -7,7 +7,9 @@ const require=createRequire(new URL('../../../package.json',import.meta.url)),ts
const hash=value=>createHash('sha256').update(JSON.stringify(value)).digest('hex'),sign=async(value,purpose)=>hash([purpose,value]); const hash=value=>createHash('sha256').update(JSON.stringify(value)).digest('hex'),sign=async(value,purpose)=>hash([purpose,value]);
const code=ts.transpileModule(await readFile(new URL('./routing-controller-baseline.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText; const code=ts.transpileModule(await readFile(new URL('./routing-controller-baseline.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
const effectCode=ts.transpileModule(await readFile(new URL('./routing-original-effects.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText,effectExports={}; const effectCode=ts.transpileModule(await readFile(new URL('./routing-original-effects.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText,effectExports={};
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{};throw Error(name);}); 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);});
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']}}; 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){ 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(); 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();
@@ -6,9 +6,11 @@ import { isIP } from 'node:net';
import { randomUUID, createHash } from 'node:crypto'; import { randomUUID, createHash } from 'node:crypto';
const require=createRequire(new URL('../../../package.json',import.meta.url)), ts=require('typescript'),{z}=require('zod'); const require=createRequire(new URL('../../../package.json',import.meta.url)), ts=require('typescript'),{z}=require('zod');
const source=await readFile(new URL('./routing-original-effects.ts',import.meta.url),'utf8'); const source=await readFile(new URL('./routing-original-effects.ts',import.meta.url),'utf8');
const exports={}; const exports={}, endpointExports={};
const endpointSource=await readFile(new URL('./routing-native-endpoint.ts',import.meta.url),'utf8');
new Function('exports','require',ts.transpileModule(endpointSource,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText)(endpointExports,require);
const gateway=value=>{const match=/^([^@%\s,]+)(?:%([^@%\s,]+))?(?:@([^@%\s,]+))?$/.exec(value);if(!match||!isIP(match[1]))throw Error();return {address:match[1],family:isIP(match[1]),interface:match[2]??null,resolverTable:match[3]??'main'};}; const gateway=value=>{const match=/^([^@%\s,]+)(?:%([^@%\s,]+))?(?:@([^@%\s,]+))?$/.exec(value);if(!match||!isIP(match[1]))throw Error();return {address:match[1],family:isIP(match[1]),interface:match[2]??null,resolverTable:match[3]??'main'};};
const providers={ 'node:net':{isIP},'node:crypto':{randomUUID},zod:{z},'./routing-community-context-model.js':{routeGatewayContext:gateway},'./security.js':{fingerprint:value=>createHash('sha256').update(JSON.stringify(value)).digest('hex')}, const providers={ 'node:net':{isIP},'node:crypto':{randomUUID},zod:{z},'./routing-community-context-model.js':{routeGatewayContext:gateway},'./routing-native-endpoint.js':endpointExports,'./security.js':{fingerprint:value=>createHash('sha256').update(JSON.stringify(value)).digest('hex')},
'./path-verification.js':{inspectControlPing:rows=>{const row=rows.at(-1),sent=Number(row.sent),received=Number(row.received);return {confirmedFailure:sent===3&&received===0,ping:{measured:sent===3&&Number.isInteger(received)&&received>=0&&received<=3,loss:(sent-received)/sent*100}};}, './path-verification.js':{inspectControlPing:rows=>{const row=rows.at(-1),sent=Number(row.sent),received=Number(row.received);return {confirmedFailure:sent===3&&received===0,ping:{measured:sent===3&&Number.isInteger(received)&&received>=0&&received<=3,loss:(sent-received)/sent*100}};},
inspectTrace:(rows,destination,expected)=>({verified:Array.isArray(rows)&&rows.at(-1)?.address===destination&&expected.every(x=>rows.some(r=>r.address===x))})}}; inspectTrace:(rows,destination,expected)=>({verified:Array.isArray(rows)&&rows.at(-1)?.address===destination&&expected.every(x=>rows.some(r=>r.address===x))})}};
new Function('exports','require',ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText)(exports,name=>{assert.ok(providers[name],name);return providers[name];}); new Function('exports','require',ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText)(exports,name=>{assert.ok(providers[name],name);return providers[name];});
@@ -26,11 +28,33 @@ function fixture(){
} }
test('actual captured predicate seals BGP destinations separately from static resolver ID',()=>{const f=fixture();assert.equal(f.selection.status,'prepared');assert.equal(f.selection.routes[0].nativeId,'*r');assert.notEqual(f.selection.routes[0].nativeId,f.raw.candidate['.id']);assert.equal(f.selection.binding.sessionId,'*s');assert.equal('sessionKey'in f.selection.routes[0],false);}); test('actual captured predicate seals BGP destinations separately from static resolver ID',()=>{const f=fixture();assert.equal(f.selection.status,'prepared');assert.equal(f.selection.routes[0].nativeId,'*r');assert.notEqual(f.selection.routes[0].nativeId,f.raw.candidate['.id']);assert.equal(f.selection.binding.sessionId,'*s');assert.equal('sessionKey'in f.selection.routes[0],false);});
test('two real effect captures can observe expected FIB/adjacency/probe while logcoverage and stronger claims stay false',async()=>{const f=fixture(),result=await f.inspect([f.sample(),f.sample()]);assert.equal(result.status,'observed-expected');assert.equal(result.logCoverage,'unknown');assert.equal(result.routeOriginVerified,false);assert.equal(result.recursiveChainVerified,false);assert.equal(result.forwardingVerified,false);assert.equal(result.autoRecoveryAllowed,false);assert.equal(result.rounds[0].relatedTopicRows,1);}); test('two real effect captures can observe expected FIB/adjacency/probe while logcoverage and stronger claims stay false',async()=>{const f=fixture(),result=await f.inspect([f.sample(),f.sample()]);assert.equal(result.status,'observed-expected');assert.equal(result.logCoverage,'unknown');assert.equal(result.routeOriginVerified,false);assert.equal(result.recursiveChainVerified,false);assert.equal(result.forwardingVerified,false);assert.equal(result.autoRecoveryAllowed,false);assert.equal(result.rounds[0].relatedTopicRows,1);});
function qualifyRuntimeSession(row) {
delete row.vrf;
row['local.address'] += '@main';
row['remote.address'] += '@main';
}
test('BGP session uses exact runtime endpoint qualifiers when VRF is absent and the installation table is separate',async()=>{
const f=fixture();
qualifyRuntimeSession(f.raw.sessions[0]);
const selection=f.prepare();
assert.equal(selection.status,'prepared');
assert.equal(selection.binding.vrf,'main');
assert.equal(selection.binding.table,'tenant');
const samples=[f.sample(),f.sample()];
const report=await exports.inspectOriginalEffectRounds(selection,samples,f.baseline);
assert.equal(report.status,'observed-expected');
assert.equal(report.routeOriginVerified,false);
});
for(const [name,change]of[ for(const [name,change]of[
['missing affected BGP route',f=>{f.raw.routes=[];}],['ambiguous prefix',f=>{f.raw.routes.push({...f.raw.routes[0],'.id':'*r2'});}], ['missing affected BGP route',f=>{f.raw.routes=[];}],['ambiguous prefix',f=>{f.raw.routes.push({...f.raw.routes[0],'.id':'*r2'});}],
['connected resolver is not affected BGP route',f=>{f.raw.routes[0].bgp='false';f.raw.routes[0].connect='true';}], ['connected resolver is not affected BGP route',f=>{f.raw.routes[0].bgp='false';f.raw.routes[0].connect='true';}],
['unresolved inheritance',f=>{f.raw.connections[0].templates='template';}],['BFD not explicitly known',f=>{delete f.raw.connections[0]['use-bfd'];}], ['unresolved inheritance',f=>{f.raw.connections[0].templates='template';}],['BFD not explicitly known',f=>{delete f.raw.connections[0]['use-bfd'];}],
['router-selected source retained as unsupported effect subset',f=>{delete f.raw.probe.source;}],['mixed session identity',f=>{f.raw.sessions.push({...f.raw.sessions[0],'.id':'*s2'});}], ['router-selected source retained as unsupported effect subset',f=>{delete f.raw.probe.source;}],['mixed session identity',f=>{f.raw.sessions.push({...f.raw.sessions[0],'.id':'*s2'});}],
['implicit VRF without both endpoint qualifiers',f=>{qualifyRuntimeSession(f.raw.sessions[0]);f.raw.sessions[0]['local.address']='192.0.2.1';}],
['qualified endpoints in the installation table',f=>{qualifyRuntimeSession(f.raw.sessions[0]);f.raw.sessions[0]['remote.address']='192.0.2.2@tenant';}],
['contradictory explicit VRF',f=>{qualifyRuntimeSession(f.raw.sessions[0]);f.raw.sessions[0].vrf='tenant';}],
['malformed peer ASN',f=>{f.raw.sessions[0]['remote.as']='0xfde9';}],
['duplicate native ID even with unrelated tuple',f=>{f.raw.sessions.push({...f.raw.sessions[0],instance:'unrelated'});}],
['noncanonical AFI prefix',f=>{f.raw.routes[0]['dst-address']='2001:db8::/64';}] ['noncanonical AFI prefix',f=>{f.raw.routes[0]['dst-address']='2001:db8::/64';}]
])test(`prepared ambiguity/unsupported remains unknown: ${name}`,()=>{const f=fixture();change(f);assert.equal(f.prepare().status,'unknown');}); ])test(`prepared ambiguity/unsupported remains unknown: ${name}`,()=>{const f=fixture();change(f);assert.equal(f.prepare().status,'unknown');});
test('baseline binds saved route IDs/prefix/gateway and does not infer operation UUID',()=>{const f=fixture(),base=f.sample();base.servers[0].routes.payload=structuredClone(f.raw.routes);assert.equal(exports.originalEffectBaselineMatches(f.selection,base),true);base.servers[0].routes.payload[0]['.id']='*different';assert.equal(exports.originalEffectBaselineMatches(f.selection,base),false);}); test('baseline binds saved route IDs/prefix/gateway and does not infer operation UUID',()=>{const f=fixture(),base=f.sample();base.servers[0].routes.payload=structuredClone(f.raw.routes);assert.equal(exports.originalEffectBaselineMatches(f.selection,base),true);base.servers[0].routes.payload[0]['.id']='*different';assert.equal(exports.originalEffectBaselineMatches(f.selection,base),false);});
@@ -2,6 +2,7 @@ import { isIP } from "node:net"
import { z } from "zod" import { z } from "zod"
import type { RouteAiProfile } from "@mmapp/contracts/mcp-routing" import type { RouteAiProfile } from "@mmapp/contracts/mcp-routing"
import { routeGatewayContext } from "./routing-community-context-model.js" import { routeGatewayContext } from "./routing-community-context-model.js"
import { matchesBgpSessionContext, parseRoutingEndpoint } from "./routing-native-endpoint.js"
import { fingerprint } from "./security.js" import { fingerprint } from "./security.js"
type Row = Record<string, unknown> type Row = Record<string, unknown>
@@ -43,10 +44,27 @@ export type OriginalEffectReport = z.infer<typeof originalEffectReportSchema>
export const unknownOriginalEffects = (reason: string): OriginalEffectReport => ({ scope: "prepared-route-fib-adjacency-control-effects", status: "unknown", reason, export const unknownOriginalEffects = (reason: string): OriginalEffectReport => ({ scope: "prepared-route-fib-adjacency-control-effects", status: "unknown", reason,
rounds: [], logSource: "routeros-rest-ring", logCoverage: "unknown", selectionIdentity: null, routeSelectionScope: "table-afi-community-observation", transitionAttribution: "unproven", switchEffectObserved: false, semanticProof: false, routeOriginVerified: false, recursiveChainVerified: false, forwardingVerified: false, autoRecoveryAllowed: false }) rounds: [], logSource: "routeros-rest-ring", logCoverage: "unknown", selectionIdentity: null, routeSelectionScope: "table-afi-community-observation", transitionAttribution: "unproven", switchEffectObserved: false, semanticProof: false, routeOriginVerified: false, recursiveChainVerified: false, forwardingVerified: false, autoRecoveryAllowed: false })
function tuple(row: Row, expected: Prepared["binding"], session: boolean) { function connectionMatches(row: Row, expected: Prepared["binding"]) {
return row.instance === expected.instance && row["routing-table"] === expected.table && row.vrf === expected.vrf && return row.instance === expected.instance && row["routing-table"] === expected.table && row.vrf === expected.vrf &&
row["local.address"] === expected.localAddress && row["remote.address"] === expected.remoteAddress && Number(row["remote.as"]) === expected.remoteAsn && row["local.address"] === expected.localAddress && row["remote.address"] === expected.remoteAddress &&
(session ? row["local.afi"] === expected.afi && row["remote.afi"] === expected.afi : row.afi === expected.afi) (row["remote.as"] === expected.remoteAsn || row["remote.as"] === String(expected.remoteAsn)) &&
row.afi === expected.afi
}
function selectBgpSession(rows: Row[], expected: Prepared["binding"]) {
try {
let match: Row | undefined
for (const row of rows) {
if (row.instance !== expected.instance) continue
const local = parseRoutingEndpoint(row["local.address"]), remote = parseRoutingEndpoint(row["remote.address"])
if (local.address !== expected.localAddress || remote.address !== expected.remoteAddress) continue
if (match || local.interface !== undefined || remote.interface !== undefined || !matchesBgpSessionContext(row, expected, local, remote) ||
(row["remote.as"] !== expected.remoteAsn && row["remote.as"] !== String(expected.remoteAsn)) || row["local.afi"] !== expected.afi || row["remote.afi"] !== expected.afi ||
row.state !== undefined || flag(row.established) === undefined || !id.safeParse(row[".id"]).success) return undefined
match = row
}
return match && unique(rows, String(match[".id"])).length === 1 ? match : undefined
} catch { return undefined }
} }
function selectedRows(rows: Row[], selection: Pick<Prepared, "table" | "family" | "community">) { function selectedRows(rows: Row[], selection: Pick<Prepared, "table" | "family" | "community">) {
return rows.filter(row => row["routing-table"] === selection.table && family(row) === selection.family && flag(row.bgp) === true && communities(row).includes(selection.community)) return rows.filter(row => row["routing-table"] === selection.table && family(row) === selection.family && flag(row.bgp) === true && communities(row).includes(selection.community))
@@ -69,10 +87,10 @@ export function prepareOriginalEffectSelection(profile: RouteAiProfile, candidat
const peer = connection[0], afi = profile.family === "ipv4" ? "ip" : "ipv6" const peer = connection[0], afi = profile.family === "ipv4" ? "ip" : "ipv6"
const wanted = tupleSchema.parse({ connectionId: peer[".id"], sessionId: "*0", instance: binding.instance, table: binding.table, vrf: binding.vrf, const wanted = tupleSchema.parse({ connectionId: peer[".id"], sessionId: "*0", instance: binding.instance, table: binding.table, vrf: binding.vrf,
localAddress: peer["local.address"], remoteAddress: peer["remote.address"], remoteAsn: Number(peer["remote.as"]), afi }) localAddress: peer["local.address"], remoteAddress: peer["remote.address"], remoteAsn: Number(peer["remote.as"]), afi })
if (!tuple(peer, wanted, false)) throw Error() if (!connectionMatches(peer, wanted)) throw Error()
const sessions = raw.sessions.filter(row => tuple(row, wanted, true)) const session = selectBgpSession(raw.sessions, wanted)
if (sessions.length !== 1 || flag(sessions[0].established) === undefined) throw Error() if (!session) throw Error()
wanted.sessionId = id.parse(sessions[0][".id"]) wanted.sessionId = id.parse(session[".id"])
const affected = selectedRows(raw.routes, profile) const affected = selectedRows(raw.routes, profile)
if (!affected.length || affected.length > 32 || new Set(affected.map(row => row["dst-address"])).size !== affected.length || new Set(affected.map(row => row[".id"])).size !== affected.length) throw Error() if (!affected.length || affected.length > 32 || new Set(affected.map(row => row["dst-address"])).size !== affected.length || new Set(affected.map(row => row[".id"])).size !== affected.length) throw Error()
for (const row of affected) { for (const row of affected) {
@@ -130,10 +148,10 @@ export async function inspectOriginalEffectRounds(rawSelection: unknown, samples
return { prefix: wanted.prefix, status } return { prefix: wanted.prefix, status }
}) })
const connections = unique(source.bgp.connectionsPayload, selection.binding.connectionId) const connections = unique(source.bgp.connectionsPayload, selection.binding.connectionId)
const sessions = source.bgp.payload.filter(row => tuple(row, selection.binding, true)) const session = selectBgpSession(source.bgp.payload, selection.binding)
let bgp: z.infer<typeof state> = "unknown" let bgp: z.infer<typeof state> = "unknown"
if (connections.length === 1 && tuple(connections[0], selection.binding, false) && flag(connections[0].disabled) === false && flag(connections[0]["use-bfd"]) === false && if (connections.length === 1 && connectionMatches(connections[0], selection.binding) && flag(connections[0].disabled) === false && flag(connections[0]["use-bfd"]) === false &&
sessions.length === 1 && sessions[0][".id"] === selection.binding.sessionId && flag(sessions[0].established) !== undefined) bgp = flag(sessions[0].established) ? "observed-expected" : "observed-other" session?.[".id"] === selection.binding.sessionId) bgp = flag(session.established) ? "observed-expected" : "observed-other"
const probe = sample.probes.find(row => row.key === selection.probe.key) const probe = sample.probes.find(row => row.key === selection.probe.key)
let controlProbe: "passed" | "failed" | "unknown" = "unknown" let controlProbe: "passed" | "failed" | "unknown" = "unknown"
if (probe) { if (probe) {
+42 -16
View File
@@ -4,21 +4,22 @@ import { prepareRoutingControllerBaseline, assertRoutingControllerBaseline, rout
import { publicRoutingControllerState } from "./routing-community-public.js" import { publicRoutingControllerState } from "./routing-community-public.js"
import { randomUUID } from "node:crypto" import { randomUUID } from "node:crypto"
import { z } from "zod" import { z } from "zod"
import { routeAiProfileSchema, type RouteAiProfile } from "@mmapp/contracts/mcp-routing" import { routeAiControllerBlockReason, routeAiProfileSchema, usesScopedRouteAiContext, type RouteAiProfile } from "@mmapp/contracts/mcp-routing"
import { pool } from "../../db/index.js" import { pool } from "../../db/index.js"
import { registerMcpService } from "./service-registry.js" import { registerMcpService } from "./service-registry.js"
import { accessGeneration, McpError } from "./security.js" import { accessGeneration, McpError } from "./security.js"
import { audit, validatePrincipal, type McpPrincipal } from "./store.js" import { audit, validatePrincipal, type McpPrincipal } from "./store.js"
import { decideRoutingSwitch, type ControllerState, type PathHealth, type rankRoutingCandidates } from "./routing-model.js" import { decideRoutingSwitch, selectFailoverReserveProbes, type ControllerState, type PathHealth, type rankRoutingCandidates } from "./routing-model.js"
import type { add } from "./actions.js" import type { add } from "./actions.js"
import { routeContextExecutionReason } from "./routing-community-context-model.js" import { COMMUNITY_PATH_MAX_AGE_MS } from "./path-proof-purpose.js"
import { controllerProfileCurrent, publishControllerState, saveControllerProfileCas, withControllerProfileLease } from "./routing-controller-fence.js" import { controllerProfileCurrent, publishControllerState, saveControllerProfileCas, withControllerProfileLease } from "./routing-controller-fence.js"
import { assertIdentifiedRoutingCompletion, createRoutingReservationStore } from "./routing-reservation-store.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 targetIds = (profile: RouteAiProfile) => [...new Set([profile.serverId, ...profile.candidates.flatMap(candidate => [candidate.jumpHostId, candidate.exitNodeId])])]
const executionGuardMessage = (profile: RouteAiProfile) => profile.autoRecovery function assertControllerCanBeEnabled(profile: RouteAiProfile) {
? "ROUTE_RECOVERY_EXECUTOR_UNAVAILABLE: автоматическое восстановление требует исполнителя с отдельным подтверждением состояния после переключения; профиль можно сохранить выключенным" const reason = routeAiControllerBlockReason(profile)
: "Контроллер scoped table/VRF/input-chain пока недоступен; профиль можно сохранить выключенным" if (reason) throw new McpError("UNSUPPORTED_CAPABILITY", reason, 409)
}
export function registerRoutingProfileActions(register: typeof add) { 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(), { 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 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)})) }),
@@ -26,7 +27,7 @@ export function registerRoutingProfileActions(register: typeof add) {
registerMcpService("routing.profile.save", { 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, 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,
execute: async input => { execute: async input => {
if (input.enabled && routeContextExecutionReason(routeAiProfileSchema.parse(input.profile))) throw new McpError("UNSUPPORTED_CAPABILITY", executionGuardMessage(routeAiProfileSchema.parse(input.profile)), 409) if (input.enabled) assertControllerCanBeEnabled(routeAiProfileSchema.parse(input.profile))
if (input.expectedVersion !== null && (!Number.isSafeInteger(input.expectedVersion) || Number(input.expectedVersion) < 1)) throw new McpError("PLAN_EXPIRED", "План не содержит ожидаемую версию профиля", 409) if (input.expectedVersion !== null && (!Number.isSafeInteger(input.expectedVersion) || Number(input.expectedVersion) < 1)) throw new McpError("PLAN_EXPIRED", "План не содержит ожидаемую версию профиля", 409)
if (input.enabled) { if (input.enabled) {
const principal=await routingControllerBaselinePrincipal(input.baseline),connection=await validatePrincipal(principal) const principal=await routingControllerBaselinePrincipal(input.baseline),connection=await validatePrincipal(principal)
@@ -43,7 +44,7 @@ export function registerRoutingProfileActions(register: typeof add) {
target: input => ({ scope: "servers", serverIds: targetIds(routeAiProfileSchema.parse(input.profile)) }), target: input => ({ scope: "servers", serverIds: targetIds(routeAiProfileSchema.parse(input.profile)) }),
prepare: async (input, context) => { prepare: async (input, context) => {
const profile = routeAiProfileSchema.parse(input.profile), ids = targetIds(profile), profileId = input.profileId ?? randomUUID() const profile = routeAiProfileSchema.parse(input.profile), ids = targetIds(profile), profileId = input.profileId ?? randomUUID()
if (input.enabled && routeContextExecutionReason(profile)) throw new McpError("UNSUPPORTED_CAPABILITY", executionGuardMessage(profile), 409) if (input.enabled) assertControllerCanBeEnabled(profile)
const expires = Date.parse(String(input.expiresAt)), credentialExpires = Date.parse(context.principal.credentialExpiresAt) 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) 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] 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]
@@ -60,7 +61,7 @@ export function registerRoutingProfileActions(register: typeof add) {
}) })
} }
interface PersistentController extends ControllerState { baseline?:RoutingControllerBaseline; planId?: string; candidateId?: string; expectedCandidateId?: string; reason?: string; checkedAt?: string; probedAt?: string } interface PersistentController extends ControllerState { baseline?:RoutingControllerBaseline; planId?: string; candidateId?: string; expectedCandidateId?: string; reason?: string; checkedAt?: string; probedAt?: string; failoverReserveCursor?: string }
/** Also fenced per profile so direct/concurrent ticks cannot publish stale state. */ /** Also fenced per profile so direct/concurrent ticks cannot publish stale state. */
export async function tickRoutingProfiles() { export async function tickRoutingProfiles() {
const { invokeMcpTool, applyPlan } = await import("./runtime.js") const { invokeMcpTool, applyPlan } = await import("./runtime.js")
@@ -77,7 +78,8 @@ export async function tickRoutingProfiles() {
} }
const current = async () => { if (!await controllerProfileCurrent(lease, row)) throw new McpError("STATE_CONFLICT", "Допуск профиля изменился во время цикла", 409) } const current = async () => { if (!await controllerProfileCurrent(lease, row)) throw new McpError("STATE_CONFLICT", "Допуск профиля изменился во время цикла", 409) }
try { try {
if (routeContextExecutionReason(profile)) { await stop(executionGuardMessage(profile)); return } const controllerBlock = routeAiControllerBlockReason(profile)
if (controllerBlock) { await stop(controllerBlock); return }
if (row.grant_expires_at.getTime() <= Date.now() || row.generation !== await accessGeneration() || !row.credential_id) { await stop("Допуск контроллера истёк или отозван"); return } if (row.grant_expires_at.getTime() <= Date.now() || row.generation !== await accessGeneration() || !row.credential_id) { await stop("Допуск контроллера истёк или отозван"); return }
const principal: McpPrincipal = await routingControllerBaselinePrincipal(state.baseline) const principal: McpPrincipal = await routingControllerBaselinePrincipal(state.baseline)
const connection = await validatePrincipal(principal) const connection = await validatePrincipal(principal)
@@ -93,6 +95,7 @@ export async function tickRoutingProfiles() {
next.lastSwitchAt = at; next.switches = [...(state.switches ?? [])]; next.consecutive = 0 next.lastSwitchAt = at; next.switches = [...(state.switches ?? [])]; next.consecutive = 0
next.expectedCandidateId = state.candidateId next.expectedCandidateId = state.candidateId
delete next.planId; delete next.candidateId; delete next.pendingCandidateId; delete next.operationId delete next.planId; delete next.candidateId; delete next.pendingCandidateId; delete next.operationId
delete next.failoverReserveCursor
next.reason = "Изменение конфигурации проверено; ожидаются новые измерения сети" next.reason = "Изменение конфигурации проверено; ожидаются новые измерения сети"
} else if (plan.operation_id) next.reason = "Операция выполняется" } else if (plan.operation_id) next.reason = "Операция выполняется"
else if (plan.expires_at.getTime() <= Date.now()) { await stop("План истёк без подтверждения"); return } else if (plan.expires_at.getTime() <= Date.now()) { await stop("План истёк без подтверждения"); return }
@@ -109,21 +112,44 @@ export async function tickRoutingProfiles() {
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 }) as { ranking: ReturnType<typeof rankRoutingCandidates>; currentCandidateId: string | null }
if (state.expectedCandidateId && analysis.currentCandidateId !== state.expectedCandidateId) { await stop("Наблюдаемый путь изменился вне ожидаемого переключения; требуется проверка вмешательства или дрейфа"); return } if (state.expectedCandidateId && analysis.currentCandidateId !== state.expectedCandidateId) { await stop("Наблюдаемый путь изменился вне ожидаемого переключения; требуется проверка вмешательства или дрейфа"); return }
if (analysis.currentCandidateId) next.expectedCandidateId = analysis.currentCandidateId if (analysis.currentCandidateId) next.expectedCandidateId = analysis.currentCandidateId
const health: PathHealth[] = []
const selected = [analysis.currentCandidateId, ...analysis.ranking.filter(value => value.eligible).slice(0, 2).map(value => value.candidate.id)]
if (profile.candidates.some(candidate => candidate.controlProbe) && !connection.policy.scope.operationIds.includes("routing.path.verify")) { await stop("Не выдано право контрольной проверки пути"); return } if (profile.candidates.some(candidate => candidate.controlProbe) && !connection.policy.scope.operationIds.includes("routing.path.verify")) { await stop("Не выдано право контрольной проверки пути"); return }
for (const candidateId of [...new Set(selected)].filter((id): id is string => !!id)) { const probeCandidate = async (candidateId: string): Promise<PathHealth | undefined> => {
if (!profile.candidates.find(candidate => candidate.id === candidateId)?.controlProbe) return
await current() await current()
if (profile.candidates.find(candidate => candidate.id === candidateId)?.controlProbe) health.push(await invokeMcpTool(principal, "mm_route_path_verify", { profile, candidateId }) as PathHealth) return await invokeMcpTool(principal, "mm_route_path_verify", { profile, candidateId }) as PathHealth
}
const currentHealth = analysis.currentCandidateId ? await probeCandidate(analysis.currentCandidateId) : undefined
const health = currentHealth ? [currentHealth] : []
const failoverProbes = analysis.currentCandidateId && currentHealth?.status === "failed"
? selectFailoverReserveProbes(analysis.ranking, analysis.currentCandidateId, state.failoverReserveCursor) : undefined
const candidates = failoverProbes?.candidateIds ?? analysis.ranking.filter(value => value.eligible).slice(0, 2)
.map(value => value.candidate.id).filter(id => id !== analysis.currentCandidateId)
for (const candidateId of candidates) {
const observed = await probeCandidate(candidateId)
if (observed) health.push(observed)
} }
await assertRoutingControllerBaseline(state.baseline,row.id,row.version,profile,row.grant_expires_at.toISOString(),{principal,connection}) await assertRoutingControllerBaseline(state.baseline,row.id,row.version,profile,row.grant_expires_at.toISOString(),{principal,connection})
const charges = await createRoutingReservationStore().readHourlyCharges(row.id, principal) const charges = await createRoutingReservationStore().readHourlyCharges(row.id, principal)
await current() await current()
const decision = decideRoutingSwitch(profile, state, analysis.ranking, analysis.currentCandidateId, Date.now(), health, charges.totalCharges) const decisionAt = Date.now()
const hasFreshPassedProbe = (candidateId: string | null) => health.some(probe => {
if (probe.candidateId !== candidateId || probe.status !== "passed") return false
const age = decisionAt - Date.parse(probe.observedAt)
return age >= 0 && age < COMMUNITY_PATH_MAX_AGE_MS
})
const decision = decideRoutingSwitch(profile, state, analysis.ranking, analysis.currentCandidateId, decisionAt, health, charges.totalCharges)
Object.assign(next, decision.state, { reason: decision.reason }) Object.assign(next, decision.state, { reason: decision.reason })
if (!failoverProbes?.cursor) delete next.failoverReserveCursor
else if (!hasFreshPassedProbe(failoverProbes.cursor)) {
// Advance past failed/unknown reserves. Keep rechecking a passing
// reserve while the current path accumulates independent failures.
next.failoverReserveCursor = failoverProbes.cursor
}
next.probedAt = new Date().toISOString() next.probedAt = new Date().toISOString()
if (decision.state.pausedReason) { await stop(decision.state.pausedReason); return } if (decision.state.pausedReason) { await stop(decision.state.pausedReason); return }
if (decision.switchTo) { if (decision.switchTo && usesScopedRouteAiContext(profile) && !hasFreshPassedProbe(decision.switchTo)) {
next.reason = "Ожидается успешная контрольная проба выбранного пути"
} else if (decision.switchTo) {
await current() await current()
await assertRoutingControllerBaseline(state.baseline,row.id,row.version,profile,row.grant_expires_at.toISOString(),{principal,connection}) await assertRoutingControllerBaseline(state.baseline,row.id,row.version,profile,row.grant_expires_at.toISOString(),{principal,connection})
const prepared = await invokeMcpTool(principal, "mm_route_optimizer_plan", { profile, candidateId: decision.switchTo }, undefined, { persistentRoutingProfile: { id: row.id, version: row.version } }) as { planId: string } const prepared = await invokeMcpTool(principal, "mm_route_optimizer_plan", { profile, candidateId: decision.switchTo }, undefined, { persistentRoutingProfile: { id: row.id, version: row.version } }) as { planId: string }
@@ -19,7 +19,7 @@ export async function acceptRoutingRecoveryAuthoritySql() {
const { pool } = await import("../../db/index.js") const { pool } = await import("../../db/index.js")
const { mcpConnectionPolicySchema } = await import("@mmapp/contracts/mcp") const { mcpConnectionPolicySchema } = await import("@mmapp/contracts/mcp")
const { routeAiProfileSchema } = await import("@mmapp/contracts/mcp-routing") const { routeAiProfileSchema } = await import("@mmapp/contracts/mcp-routing")
const { accessGeneration } = await import("./security.js") const { accessGeneration, McpError } = await import("./security.js")
const { readRoutingEndpoints, createRoutingSourceContext } = await import("./routing-observation-source.js") const { readRoutingEndpoints, createRoutingSourceContext } = await import("./routing-observation-source.js")
const { resolveRoutingProfileAuthority } = await import("./routing-profile-authority.js") const { resolveRoutingProfileAuthority } = await import("./routing-profile-authority.js")
const { captureScopedRecoveryAuthority, scopedRecoveryDeadlineCeiling } = await import("./routing-community-scoped-recovery.js") const { captureScopedRecoveryAuthority, scopedRecoveryDeadlineCeiling } = await import("./routing-community-scoped-recovery.js")
@@ -27,7 +27,7 @@ export async function acceptRoutingRecoveryAuthoritySql() {
assert.equal((await pool.query("SELECT current_database() AS name")).rows[0].name, "postgres") assert.equal((await pool.query("SELECT current_database() AS name")).rows[0].name, "postgres")
const previousEnabled = (await pool.query("SELECT enabled FROM mcp_settings WHERE id=1")).rows[0].enabled as boolean const previousEnabled = (await pool.query("SELECT enabled FROM mcp_settings WHERE id=1")).rows[0].enabled as boolean
const profileIds: string[] = [], ids: number[] = [] const profileIds: string[] = [], ids: number[] = []
let nativeHttpRequests = 0, callbackDispatches = 0, durableIntents = 0 let nativeHttpRequests = 0, callbackDispatches = 0, dispatchFences = 0
const listener = createServer((req, res) => { nativeHttpRequests++; req.resume(); res.setHeader("content-type", "application/json"); res.end("{}") }) const listener = createServer((req, res) => { nativeHttpRequests++; req.resume(); res.setHeader("content-type", "application/json"); res.end("{}") })
try { try {
await new Promise<void>((resolve, reject) => { listener.once("error", reject); listener.listen(0, "127.0.0.1", () => { listener.off("error", reject); resolve() }) }) await new Promise<void>((resolve, reject) => { listener.once("error", reject); listener.listen(0, "127.0.0.1", () => { listener.off("error", reject); resolve() }) })
@@ -65,12 +65,7 @@ export async function acceptRoutingRecoveryAuthoritySql() {
const path = "/routing/filter/rule/*1" const path = "/routing/filter/rule/*1"
await assert.rejects(withVerifiedConfigurationChange({ actorId: ownerId, connectionId, approved: true, serverIds: ids, serverWide: true, objects: [], await assert.rejects(withVerifiedConfigurationChange({ actorId: ownerId, connectionId, approved: true, serverIds: ids, serverWide: true, objects: [],
authorize: async () => { await captured.assert() }, beforeDispatch: async () => { authorize: async () => { await captured.assert() }, beforeDispatch: async () => {
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_audit WHERE connection_id=$1 AND action='configuration.intent' AND payload->>'path'=$2", [connectionId, path])).rows[0].n, 1) dispatchFences++
durableIntents++
if (change === "edit") await pool.query("UPDATE mcp_routing_profiles SET version=version+1,config=jsonb_set(config,'{name}','\"late edit\"') WHERE id=$1", [profileId])
if (change === "disable") await pool.query("UPDATE mcp_routing_profiles SET enabled=false WHERE id=$1", [profileId])
if (change === "grant") await pool.query("UPDATE mcp_routing_profiles SET grant_expires_at=clock_timestamp()-interval '1 second' WHERE id=$1", [profileId])
await captured.assert()
}, },
}, () => executeGuardedConfigurationWrite(ids[0], "PATCH", path, async () => { }, () => executeGuardedConfigurationWrite(ids[0], "PATCH", path, async () => {
callbackDispatches++ callbackDispatches++
@@ -78,12 +73,17 @@ export async function acceptRoutingRecoveryAuthoritySql() {
const req = request({ hostname: "127.0.0.1", port, path: "/rest" + path, method: "PATCH", timeout: 1000 }, res => { res.resume(); res.once("end", resolve) }) const req = request({ hostname: "127.0.0.1", port, path: "/rest" + path, method: "PATCH", timeout: 1000 }, res => { res.resume(); res.once("end", resolve) })
req.once("error", reject); req.once("timeout", () => req.destroy(new Error("Synthetic loopback timeout"))); req.end("{}") req.once("error", reject); req.once("timeout", () => req.destroy(new Error("Synthetic loopback timeout"))); req.end("{}")
}) })
})), /Постоянный допуск/) })), (error: unknown) => error instanceof McpError && error.code === "STATE_CONFLICT" && /Активный routing profile/.test(error.message))
assert.equal(callbackDispatches, 0); assert.equal(nativeHttpRequests, 0) assert.equal(callbackDispatches, 0); assert.equal(nativeHttpRequests, 0); assert.equal(dispatchFences, 0)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_audit WHERE connection_id=$1 AND action='configuration.unconfirmed'", [connectionId])).rows[0].n, 1) assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_audit WHERE connection_id=$1 AND action IN ('configuration.intent','configuration.unconfirmed','configuration.acknowledged')", [connectionId])).rows[0].n, 0)
if (change === "edit") await pool.query("UPDATE mcp_routing_profiles SET version=version+1,config=jsonb_set(config,'{name}','\"late edit\"') WHERE id=$1", [profileId])
if (change === "disable") await pool.query("UPDATE mcp_routing_profiles SET enabled=false WHERE id=$1", [profileId])
if (change === "grant") await pool.query("UPDATE mcp_routing_profiles SET grant_expires_at=clock_timestamp()-interval '1 second' WHERE id=$1", [profileId])
await assert.rejects(captured.assert(), /Постоянный допуск/)
} }
return { cases: 3, lateMutations: ["edit", "disable", "grant"], deepCaptureCases: 3, durableIntents, callbackDispatches, nativeHttpRequests, return { cases: 3, deepCaptureCases: 3, directGuardBeforeIntent: true, dispatchFences, callbackDispatches, nativeHttpRequests,
productionSessionAuthorityFence: true, realSql: true, productionHmac: true, fullInverseLifecycleRepeated: false, controllerEnabled: false, externalNetwork: false } capturedAuthorityChanges: ["edit", "disable", "grant"], realSql: true, productionHmac: true, controllerEnabled: false, externalNetwork: false }
} finally { } finally {
if (profileIds.length) await pool.query("UPDATE mcp_routing_profiles SET enabled=false WHERE id=ANY($1::uuid[])", [profileIds]) if (profileIds.length) await pool.query("UPDATE mcp_routing_profiles SET enabled=false WHERE id=ANY($1::uuid[])", [profileIds])
if (ids.length) await pool.query("UPDATE servers SET enabled=false WHERE id=ANY($1::int[])", [ids]) if (ids.length) await pool.query("UPDATE servers SET enabled=false WHERE id=ANY($1::int[])", [ids])
@@ -2,9 +2,26 @@ import { assertOwnedRoutingRecoveryAuthoritySql, acceptRoutingRecoveryAuthorityS
// Guard precedes imports that configure a pool or load private MCP state. // Guard precedes imports that configure a pool or load private MCP state.
assertOwnedRoutingRecoveryAuthoritySql() assertOwnedRoutingRecoveryAuthoritySql()
process.env.AUTH_REQUIRED = "true"
process.env.AUTH_JWT_SECRET = "owned-routing-recovery-authority-acceptance-secret"
process.env.AUTH_ISSUER = "https://owned-routing-recovery-authority.test"
const { pool, closePool } = await import("../../db/index.js") const { pool, closePool } = await import("../../db/index.js")
try { try {
const { applySqlMigrations } = await import("../../db/migrate.js") const { applySqlMigrations } = await import("../../db/migrate.js")
await applySqlMigrations(pool) await applySqlMigrations(pool)
console.log("captured recovery authority SQL acceptance:", JSON.stringify(await acceptRoutingRecoveryAuthoritySql())) console.log("captured recovery authority SQL acceptance:", JSON.stringify(await acceptRoutingRecoveryAuthoritySql()))
} finally { await closePool() } const { updateMcpSettings } = await import("./store.js")
await import("./actions.js")
const { acceptScopedRouteCommunitySwitch } = await import("./routing-community-scoped-acceptance.js")
const actor = { id: "owned-routing-recovery-authority", email: "routing-recovery@example.invalid", name: "Owned recovery authority",
apps: ["mm"], permissions: ["mm:settings:admin", "mm:mcp:admin", "mm:mcp:approve", "mm:network:read", "mm:network:write", "mm:filters:write", "mm:servers:admin"],
issuedAt: Date.now() - 1000, expiresAt: Date.now() + 900000 }
await updateMcpSettings({ enabled: true }, actor)
for (const lateInverseAuthorityChange of ["edit", "disable", "grant"] as const) {
const evidence = await acceptScopedRouteCommunitySwitch(actor, { lateInverseAuthorityChange })
console.log("genuine late inverse authority SQL acceptance:", JSON.stringify(evidence))
}
} finally {
await (await import("../../services/scheduler.js")).stopSchedulerForDatabaseOperation()
await closePool()
}
@@ -14,8 +14,10 @@ try {
issuedAt: Date.now() - 1000, expiresAt: Date.now() + 900000 } issuedAt: Date.now() - 1000, expiresAt: Date.now() + 900000 }
await (await import("./store.js")).updateMcpSettings({ enabled: true }, actor) await (await import("./store.js")).updateMcpSettings({ enabled: true }, actor)
await import("./actions.js") await import("./actions.js")
const mixed = await (await import("./routing-community-scoped-acceptance.js")).acceptScopedRouteCommunitySwitch(actor, const { acceptScopedRouteCommunitySwitch } = await import("./routing-community-scoped-acceptance.js")
{ manualInverseChargeOnly: true, manualInverseChargeCase: "mixed-forward" }) const inverse = await acceptScopedRouteCommunitySwitch(actor, { manualInverseChargeOnly: true })
console.log("private routing v8 manual inverse charge and intent SQL acceptance:", JSON.stringify(inverse))
const mixed = await acceptScopedRouteCommunitySwitch(actor, { manualInverseChargeOnly: true, manualInverseChargeCase: "mixed-forward" })
console.log("private routing mixed forward/manual inverse admission SQL acceptance:", JSON.stringify(mixed)) console.log("private routing mixed forward/manual inverse admission SQL acceptance:", JSON.stringify(mixed))
} finally { } finally {
await (await import("../../services/scheduler.js")).stopSchedulerForDatabaseOperation() await (await import("../../services/scheduler.js")).stopSchedulerForDatabaseOperation()
@@ -4,10 +4,29 @@ import { routeAiProfileSchema } from "@mmapp/contracts/mcp-routing"
import type { ChangeEvidence } from "./action-types.js" import type { ChangeEvidence } from "./action-types.js"
import type { McpPrincipal, StoredPlan } from "./store.js" import type { McpPrincipal, StoredPlan } from "./store.js"
import { decryptSecret, fingerprint, McpError } from "./security.js" import { decryptSecret, fingerprint, McpError } from "./security.js"
import { assertScopedCommunityRequest } from "./routing-community-scoped-request.js" import { assertScopedCommunityRequest, readScopedPathCheckpoint } from "./routing-community-scoped-request.js"
import { assertPersistentScopedProfileAuthority, routingProfileAuthorityBindingSchema } from "./routing-profile-authority.js" import { assertPersistentScopedProfileAuthority, routingProfileAuthorityBindingSchema } from "./routing-profile-authority.js"
import { loadRoutingMainTransitionPreparation } from "./routing-main-transition.js" import { loadRoutingMainTransitionPreparation } from "./routing-main-transition.js"
export function requiresScopedRoutingOwnerApproval(evidence: ChangeEvidence) {
const step = evidence.steps[0]
return evidence.steps.length === 1 && step.kind === "service" &&
step.service === "routing.community.scoped" && step.input.proofRevision === 3
}
export async function readScopedRoutingPathAdmissionDeadline(db: PoolClient, evidence: ChangeEvidence, connectionId: string) {
const step = evidence.steps[0]
if (step.kind !== "service" || step.service !== "routing.community.scoped" || step.input.proofRevision !== 3 ||
routeAiProfileSchema.parse(step.input.profile).autoRecovery) return
const { pathCheckId, pathCheckpointProof } = step.input
if (typeof pathCheckId !== "string" || typeof pathCheckpointProof !== "string") {
throw new McpError("MEASUREMENT_UNKNOWN", "Подтверждение пути изменилось", 409)
}
const checkpoint = await readScopedPathCheckpoint(db, pathCheckId, connectionId)
if (checkpoint.proof !== pathCheckpointProof) throw new McpError("MEASUREMENT_UNKNOWN", "Подтверждение пути изменилось", 409)
return checkpoint.validUntil
}
/** Only authenticated encrypted evidence can select this admission lane. */ /** Only authenticated encrypted evidence can select this admission lane. */
export async function routingReservationEvidence(plan: Pick<StoredPlan, "operation_id" | "operation_version">, evidence: ChangeEvidence) { export async function routingReservationEvidence(plan: Pick<StoredPlan, "operation_id" | "operation_version">, evidence: ChangeEvidence) {
if (plan.operation_id !== "routing.community.switch" || evidence.steps.length !== 1) return undefined if (plan.operation_id !== "routing.community.switch" || evidence.steps.length !== 1) return undefined
@@ -506,6 +506,7 @@ export async function assertPersistentRoutingForwardTransportCharge(db: Pick<Poo
if (source.length !== 1) throw fail("Exact transport source unavailable") if (source.length !== 1) throw fail("Exact transport source unavailable")
const row = source[0], observedAt = iso(row.observed_at) const row = source[0], observedAt = iso(row.observed_at)
if (row.operation_state !== "running" || row.plan_state !== "executing" || row.step_state !== "intent" || !row.approved_by || if (row.operation_state !== "running" || row.plan_state !== "executing" || row.step_state !== "intent" || !row.approved_by ||
metadata.serviceName === "routing.community.scoped" && row.approved_by !== ownerId ||
row.policy_version !== metadata.policyVersion || row.generation !== metadata.generation || row.policy_version !== metadata.policyVersion || row.generation !== metadata.generation ||
typeof row.evidence_sealed !== "string" || typeof row.payload_sealed !== "string" || typeof row.evidence_sealed !== "string" || typeof row.payload_sealed !== "string" ||
fingerprint(row.intended_state) !== fingerprint(redact({ kind: "service", service: metadata.serviceName, input: metadata.serviceInput })) || fingerprint(row.intended_state) !== fingerprint(redact({ kind: "service", service: metadata.serviceName, input: metadata.serviceInput })) ||
@@ -352,6 +352,47 @@ export async function acceptRoutingReservationSql(options: { runtimeAdmission?:
await assert.rejects(store.beginForwardAttempt(reservation.forwardAllocation), /capability/) await assert.rejects(store.beginForwardAttempt(reservation.forwardAllocation), /capability/)
names.push("spent forward/no genuine checkpoint and preterminal unused-proof refuse; unknown never replays or refunds") names.push("spent forward/no genuine checkpoint and preterminal unused-proof refuse; unknown never replays or refunds")
} }
{
const f = await fixture(), source = await f.makeSource(), reservation = await store.reserve(source.operationId)
const { saveScopedConfigurationCheckpoint, saveScopedNetworkCheckpoint } = await import("./routing-community-scoped-checkpoint.js")
const { executeReservedScopedRecovery } = await import("./routing-community-scoped-recovery.js")
await pool.query("UPDATE mcp_operations SET state='running' WHERE id=$1", [source.operationId])
const step = { kind: "service", service: "routing.community.scoped", input: source.input }
await pool.query("INSERT INTO mcp_operation_steps(operation_id,step_index,state,intended_state) VALUES($1,0,'intent',$2::jsonb)",
[source.operationId, JSON.stringify(security.redact(step))])
await pool.query("INSERT INTO mcp_operation_checkpoints(operation_id,step_index,before_sealed) VALUES($1,0,$2)",
[source.operationId, await security.encryptSecret(JSON.stringify(source.input.beforeRules))])
assert(reservation.forwardAllocation)
await store.beginForwardAttempt(reservation.forwardAllocation)
// Use the actual checkpoint writers for production crypto and SQL, but
// deliberately seed a synthetic failed-forward observation. No PATCH
// or genuine network observation is part of this owned component case.
const requestSeal = String(source.input.requestProof)
const configuration = await saveScopedConfigurationCheckpoint(source.input, requestSeal)
await saveScopedNetworkCheckpoint(source.input, requestSeal, { id: randomUUID(), candidateId: "b",
status: "failed", observedAt: configuration.observedAt })
const published = await store.recordForwardOutcome(reservation.reservationId)
assert(published.allocation)
assert.equal(published.receipt.state.inverse, "held")
const refused = { sourceOperationId: randomUUID(), principal: f.principal }
await assert.rejects(executeReservedScopedRecovery(published.allocation, refused), /Reserved runner identity differs/)
const unknown = await store.read(reservation.reservationId, f.principal)
assert.deepEqual(unknown.state, { ...published.receipt.state, revision: published.receipt.state.revision + 2, inverse: "unknown" })
assert.equal(unknown.chargedMutationUnits, 2)
assert.equal(unknown.chargedProfileSlots, 2)
assert.equal(unknown.refundsAvailable, false)
const shared = (await pool.query(`SELECT state,slot_phase,inverse_intent_sealed,inverse_configuration_sealed,verified_sealed
FROM mcp_scoped_recovery_intents WHERE reservation_id=$1`, [reservation.reservationId])).rows
assert.deepEqual(shared, [{ state: "unknown", slot_phase: "reserved-attempt", inverse_intent_sealed: null,
inverse_configuration_sealed: null, verified_sealed: null }])
await assert.rejects(executeReservedScopedRecovery(published.allocation, refused), /capability/)
await assert.rejects(store.recordForwardOutcome(reservation.reservationId), /republished/)
await assert.rejects(store.proveUnusedInverse(reservation.reservationId), /terminal applied/)
assert.equal(await used(f.connectionId), 2)
assert.equal((await store.readHourlyCharges(f.profileId, f.principal)).reservedCharges, 2)
assert.deepEqual((await store.read(reservation.reservationId, f.principal)).state, unknown.state)
names.push("genuine inverse capability on synthetic failed-forward checkpoints: acknowledged spend, late identity refusal, signed same-slot unknown and both charges conserved without replay or refund")
}
{ {
const f = await fixture(), source = await f.makeSource(), reservation = await store.reserve(source.operationId) const f = await fixture(), source = await f.makeSource(), reservation = await store.reserve(source.operationId)
const saved = (await pool.query("SELECT state_proof FROM mcp_routing_reservations WHERE id=$1", [reservation.reservationId])).rows[0].state_proof const saved = (await pool.query("SELECT state_proof FROM mcp_routing_reservations WHERE id=$1", [reservation.reservationId])).rows[0].state_proof
@@ -60,7 +60,7 @@ export async function acceptRoutingWorkerFixtureIsolation(actor:AuthUser) {
const changed=(await pool.query("SELECT enabled,state,version,grant_expires_at FROM mcp_routing_profiles WHERE id=$1",[id])).rows[0] const changed=(await pool.query("SELECT enabled,state,version,grant_expires_at FROM mcp_routing_profiles WHERE id=$1",[id])).rows[0]
assert.equal(changed.enabled,false);assert.equal(changed.version,1) assert.equal(changed.enabled,false);assert.equal(changed.version,1)
assert.equal(changed.grant_expires_at.toISOString(),expiry.toISOString()) assert.equal(changed.grant_expires_at.toISOString(),expiry.toISOString())
assert.match(changed.state.pausedReason,/Контроллер scoped table\/VRF\/input-chain пока недоступен/) assert.match(changed.state.pausedReason,/Укажите явную BGP-привязку/)
const pauses=(await pool.query("SELECT id FROM mcp_audit WHERE action='controller.pause' AND payload->>'profileId'=$1",[id])).rows const pauses=(await pool.query("SELECT id FROM mcp_audit WHERE action='controller.pause' AND payload->>'profileId'=$1",[id])).rows
assert.equal(pauses.length,1) assert.equal(pauses.length,1)
await assert.rejects(assertRoutingProfileAuthority(authority,context),(error:{code?:string})=>error.code==="STATE_CONFLICT") await assert.rejects(assertRoutingProfileAuthority(authority,context),(error:{code?:string})=>error.code==="STATE_CONFLICT")
+88 -14
View File
@@ -1,4 +1,8 @@
import { pollErrorLog } from "../../services/server-poll-error-provenance.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"
import { capturePublishedServerProbeTerminalProof } from "../../services/server-probe-operation-retirement.js"
import { SERVER_PROBE_OPERATION } from "../../services/server-probe-authority.js"
import { preparedPlanExpiry } from "./plan-expiry.js" import { preparedPlanExpiry } from "./plan-expiry.js"
import { acknowledgedCreateId, assertNoCallerCreateId } from "./native-create-identity.js" import { acknowledgedCreateId, assertNoCallerCreateId } from "./native-create-identity.js"
import { randomUUID, createHash } from "node:crypto" import { randomUUID, createHash } from "node:crypto"
@@ -11,6 +15,7 @@ import { logSettingsPatchSchema } from "@mmapp/contracts"
import { pool } from "../../db/index.js" import { pool } from "../../db/index.js"
import { hasPermission, type AuthUser } from "../../lib/permissions.js" import { hasPermission, type AuthUser } from "../../lib/permissions.js"
import { createUser, updateUser, deleteUser, addBinding, removeBinding, getUserById } from "../users/service/users-service.js" import { createUser, updateUser, deleteUser, addBinding, removeBinding, getUserById } from "../users/service/users-service.js"
import { normalizeBindingPeer } from "../users/peer-bind.js"
import { getLogSettings, updateLogSettings } from "../../services/router-logs-collector.js" import { getLogSettings, updateLogSettings } from "../../services/router-logs-collector.js"
import { refreshScheduler } from "../../services/scheduler.js" import { refreshScheduler } from "../../services/scheduler.js"
import { withApplicationMutationLock, withConfigMutationLock } from "../../services/config-mutation-lock.js" import { withApplicationMutationLock, withConfigMutationLock } from "../../services/config-mutation-lock.js"
@@ -18,17 +23,19 @@ import { withOperationConfigurationLockClient, assertOperationConfigurationLockS
import { sharedDatabaseGate } from "../../services/shared-database-gate.js" import { sharedDatabaseGate } from "../../services/shared-database-gate.js"
import { withRuntimePollPrincipalSource, executeRuntimePollPolicyTransaction, type RuntimePollPolicyLoan } from "./runtime-poll-principal-boundary.js" import { withRuntimePollPrincipalSource, executeRuntimePollPolicyTransaction, type RuntimePollPolicyLoan } from "./runtime-poll-principal-boundary.js"
import { readPollRuntimePolicySource, type PollRuntimePolicySource } from "./poll-runtime-policy-source.js" import { readPollRuntimePolicySource, type PollRuntimePolicySource } from "./poll-runtime-policy-source.js"
import { withVerifiedConfigurationChange } from "../../services/config-mutation-policy.js" import { withVerifiedConfigurationChange, assertVerifiedConfigurationAccess } from "../../services/config-mutation-policy.js"
import { beginMcpOperationAdmission, insertMcpOperationAdmission, retireMcpOperationAdmission, withMcpOperationExecutionOrigin, stripMcpOperationExecutionPrivateResult, type McpOperationAdmission } from "./operation-execution-origin.js" import { beginMcpOperationAdmission, insertMcpOperationAdmission, retireMcpOperationAdmission, withMcpOperationExecutionOrigin, stripMcpOperationExecutionPrivateResult, type McpOperationAdmission } from "./operation-execution-origin.js"
import { createOspfOwnerSourceGuard, verifyOspfOwnerGraph, verifyOspfOwnerGraphAtPhase } from "./ospf-owner-graph.js" import { createOspfOwnerSourceGuard, verifyOspfOwnerGraph, verifyOspfOwnerGraphAtPhase } from "./ospf-owner-graph.js"
import { evaluateMcpPolicy } from "./policy.js" import { evaluateMcpPolicy } from "./policy.js"
import { findAction, findActionByOperation } from "./actions.js" 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 { 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 { 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, resourceById, routerClient, type RosSnapshot } from "./resources.js"
import { verifyRosStepSnapshots } from "./ros-step-verification.js" import { verifyRosStepSnapshots } from "./ros-step-verification.js"
import { createRosPrefix } from "./ros-prefix.js" import { createRosPrefix } from "./ros-prefix.js"
import type { ActionContext, ChangeEvidence, ChangeStep } from "./action-types.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" import { findMcpService, type McpServiceExecutionContext } from "./service-registry.js"
import { withDiagnosticCancellation } from "./diagnostic-cancellation.js" import { withDiagnosticCancellation } from "./diagnostic-cancellation.js"
import { z } from "zod" import { z } from "zod"
@@ -44,7 +51,7 @@ import { serviceNativeProgress } from "./service-native-progress.js"
import { serviceDiagnosticProgress, serviceDiagnosticReceipt } from "./service-diagnostic-progress.js" import { serviceDiagnosticProgress, serviceDiagnosticReceipt } from "./service-diagnostic-progress.js"
import { admitManualInverseWithinOwner } from "./routing-manual-inverse-store.js" import { admitManualInverseWithinOwner } from "./routing-manual-inverse-store.js"
import { withRoutingReservationAdmission, createRoutingReservationStore, readHistoricalChargesWithinOwner, admitPersistentRoutingForwardWithinOwner, type RoutingReservedForwardAllocation } from "./routing-reservation-store.js" import { withRoutingReservationAdmission, createRoutingReservationStore, readHistoricalChargesWithinOwner, admitPersistentRoutingForwardWithinOwner, type RoutingReservedForwardAllocation } from "./routing-reservation-store.js"
import { lockRoutingReservationAdmissionSource, routingReservationEvidence } from "./routing-reservation-runtime.js" import { lockRoutingReservationAdmissionSource, readScopedRoutingPathAdmissionDeadline, requiresScopedRoutingOwnerApproval, routingReservationEvidence } from "./routing-reservation-runtime.js"
import { executeReservedRoutingStep, hasConfirmedReservedRoutingForward, type ReservedRoutingStepOutcome } from "./routing-reserved-step.js" import { executeReservedRoutingStep, hasConfirmedReservedRoutingForward, type ReservedRoutingStepOutcome } from "./routing-reserved-step.js"
let activeRunners = 0 let activeRunners = 0
@@ -74,6 +81,12 @@ async function policyInput(client: Pick<PoolClient, "query">, connection: Connec
usage: { callsInMinute: Number(usage.calls), mutationsInHour: Number(usage.mutations), concurrentReads: Number(usage.reads) }, auditAvailable: true, usage: { callsInMinute: Number(usage.calls), mutationsInHour: Number(usage.mutations), concurrentReads: Number(usage.reads) }, auditAvailable: true,
} }
} }
export async function readMcpPolicyPreviewInput(connection: ConnectionRow, input: Pick<McpPolicyPreviewRequest, "operationId" | "target" | "objects" | "objectCount">): Promise<McpPolicyPreviewRequest> {
const current = await policyInput(pool, connection, input.operationId, input.target)
return { ...current, objects: input.objects, objectCount: input.objectCount }
}
async function temporaryScope(client: Pick<PoolClient, "query">, connection: ConnectionRow, credentialId: string) { async function temporaryScope(client: Pick<PoolClient, "query">, connection: ConnectionRow, credentialId: string) {
const { rows } = await client.query("SELECT payload FROM mcp_grants WHERE connection_id=$1 AND revoked_at IS NULL AND payload->>'credentialId'=$2", [connection.id, credentialId]) const { rows } = await client.query("SELECT payload FROM mcp_grants WHERE connection_id=$1 AND revoked_at IS NULL AND payload->>'credentialId'=$2", [connection.id, credentialId])
if (!rows.length) return undefined if (!rows.length) return undefined
@@ -109,12 +122,24 @@ async function completeRequest(id: string, started: number, result: unknown, err
}) })
} }
export async function invokeMcpTool(principal: McpPrincipal, toolName: string, raw: unknown, signal?: AbortSignal, internal?: { persistentRoutingProfile: { id: string; version: number } }): Promise<unknown> { export async function invokeMcpTool(principal: McpPrincipal, toolName: string, raw: unknown, signal?: AbortSignal, internal?: { persistentRoutingProfile: { id: string; version: number } }): Promise<unknown> {
if (toolName === "mm_logs_follow") {
const maintenance = new AbortController()
return sharedDatabaseGate.request(
() => invokeMcpToolInner(principal, toolName, raw, signal, internal, maintenance.signal),
() => maintenance.abort(new McpError("RESET_REQUIRED", "База данных перешла в режим обслуживания; начните новое чтение", 409)),
)
}
return sharedDatabaseGate.request(() => invokeMcpToolInner(principal, toolName, raw, signal, internal)) return sharedDatabaseGate.request(() => invokeMcpToolInner(principal, toolName, raw, signal, internal))
} }
async function invokeMcpToolInner(principal: McpPrincipal, toolName: string, raw: unknown, signal?: AbortSignal, internal?: { persistentRoutingProfile: { id: string; version: number } }): Promise<unknown> { async function invokeMcpToolInner(principal: McpPrincipal, toolName: string, raw: unknown, signal?: AbortSignal, internal?: { persistentRoutingProfile: { id: string; version: number } }, logsFollowMaintenance?: AbortSignal): Promise<unknown> {
const action = findAction(toolName), input = action.schema.parse(raw) as Record<string, unknown>, started = Date.now() const action = findAction(toolName), input = action.schema.parse(raw) as Record<string, unknown>, started = Date.now()
const call = await reserveCall(principal, toolName, input, signal) const call = await reserveCall(principal, toolName, input, signal)
const cancelled = () => logsFollowMaintenance && logsFollowCancellation(logsFollowMaintenance, signal)
const assertActive = () => { const failure = cancelled(); if (failure) throw failure }
let vrfPreparing = false
try { try {
assertActive()
if (logsFollowMaintenance) call.context.logsFollowMaintenance = logsFollowMaintenance
if (internal) { if (internal) {
if (action.operation.id !== "routing.community.switch") throw new McpError("INVALID_INPUT", "Контекст профиля не относится к этой операции", 400) if (action.operation.id !== "routing.community.switch") throw new McpError("INVALID_INPUT", "Контекст профиля не относится к этой операции", 400)
call.context.persistentRoutingProfile = z.object({ id: z.string().uuid(), version: z.number().int().positive().safe() }).strict().parse(internal.persistentRoutingProfile) call.context.persistentRoutingProfile = z.object({ id: z.string().uuid(), version: z.number().int().positive().safe() }).strict().parse(internal.persistentRoutingProfile)
@@ -122,6 +147,7 @@ async function invokeMcpToolInner(principal: McpPrincipal, toolName: string, raw
let result: unknown let result: unknown
if (action.read) result = await action.read(input, call.context) if (action.read) result = await action.read(input, call.context)
else if (action.prepare) { else if (action.prepare) {
vrfPreparing = ["ros.vrf.create", "ros.vrf.update", "ros.vrf.delete"].includes(action.operation.id)
const evidence = await action.prepare(input, call.context) const evidence = await action.prepare(input, call.context)
result = await transaction(async client => { result = await transaction(async client => {
await client.query("SELECT id FROM mcp_connections WHERE id=$1 FOR UPDATE", [principal.connectionId]) await client.query("SELECT id FROM mcp_connections WHERE id=$1 FOR UPDATE", [principal.connectionId])
@@ -133,7 +159,7 @@ async function invokeMcpToolInner(principal: McpPrincipal, toolName: string, raw
let decision = evaluateMcpPolicy(check) let decision = evaluateMcpPolicy(check)
if (decision.decision === "denied") throw new McpError(decision.reasons[0].code, decision.reasons.map(reason => reason.message).join("; ")) if (decision.decision === "denied") throw new McpError(decision.reasons[0].code, decision.reasons.map(reason => reason.message).join("; "))
const id = randomUUID(), expiresAt = preparedPlanExpiry(action.operation.id, action.operation.section, evidence) const id = randomUUID(), expiresAt = preparedPlanExpiry(action.operation.id, action.operation.section, evidence)
if (!expiresAt) throw new McpError("PLAN_EXPIRED", "Срок исходного плана опроса истёк или недоступен; подготовьте новый план", 409) if (!expiresAt) throw new McpError("PLAN_EXPIRED", "Срок исходных данных плана истёк или недоступен; подготовьте новый план", 409)
const generation = await accessGeneration() const generation = await accessGeneration()
await client.query("INSERT INTO mcp_plans(id,connection_id,operation_id,operation_version,policy_version,generation,payload,evidence,state,expires_at) VALUES($1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9,$10)", [id, connection.id, action.operation.id, action.operation.version, connection.policy.version, generation, JSON.stringify({ sealed: await encryptSecret(JSON.stringify(input)) }), JSON.stringify({ sealed: await encryptSecret(JSON.stringify(evidence)) }), decision.decision === "approval-required" ? "awaiting-approval" : "draft", expiresAt]) await client.query("INSERT INTO mcp_plans(id,connection_id,operation_id,operation_version,policy_version,generation,payload,evidence,state,expires_at) VALUES($1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9,$10)", [id, connection.id, action.operation.id, action.operation.version, connection.policy.version, generation, JSON.stringify({ sealed: await encryptSecret(JSON.stringify(input)) }), JSON.stringify({ sealed: await encryptSecret(JSON.stringify(evidence)) }), decision.decision === "approval-required" ? "awaiting-approval" : "draft", expiresAt])
const vrfTicket = await prepareRuntimeVrfTicket({ operationId: action.operation.id, operationVersion: action.operation.version, planId: id, connectionId: connection.id, ownerId: connection.owner_id, evidence, sql: client, const vrfTicket = await prepareRuntimeVrfTicket({ operationId: action.operation.id, operationVersion: action.operation.version, planId: id, connectionId: connection.id, ownerId: connection.owner_id, evidence, sql: client,
@@ -169,13 +195,27 @@ async function invokeMcpToolInner(principal: McpPrincipal, toolName: string, raw
await audit(client, connection.owner_id, "plan.create", connection.id, { planId: id, operationId: action.operation.id, decision }) await audit(client, connection.owner_id, "plan.create", connection.id, { planId: id, operationId: action.operation.id, decision })
return { planId: id, expiresAt: expiresAt.toISOString(), state: decision.decision === "approval-required" ? "awaiting-approval" : "draft", decision, changes: redact(publicMcpChangeEvidence(action.operation.id, evidence)), ...(vrfTicket ? { vrfTicket } : {}), executionAvailable: true } return { planId: id, expiresAt: expiresAt.toISOString(), state: decision.decision === "approval-required" ? "awaiting-approval" : "draft", decision, changes: redact(publicMcpChangeEvidence(action.operation.id, evidence)), ...(vrfTicket ? { vrfTicket } : {}), executionAvailable: true }
}) })
vrfPreparing = false
} else throw new McpError("UNSUPPORTED_CAPABILITY", "Нет обработчика операции", 409) } else throw new McpError("UNSUPPORTED_CAPABILITY", "Нет обработчика операции", 409)
assertActive()
const current = await effectiveMcpConnection(principal) 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) 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 output = redact(result) const output = redact(result)
await completeRequest(call.requestId, started, output) await completeRequest(call.requestId, started, output)
assertActive()
return output return output
} catch (error) { await completeRequest(call.requestId, started, null, error); throw error } } catch (error) {
if (vrfPreparing) {
try {
const diagnostic = vrfPreparationDiagnostic(call.requestId, action.operation.id, error)
if (diagnostic) console.error(JSON.stringify(diagnostic))
} catch { /* Diagnostics must never replace the original failure. */ }
}
const failure = cancelled() ?? error
await completeRequest(call.requestId, started, null, failure)
throw failure
}
} }
export async function loadPlan(id: string, client: Pick<PoolClient, "query"> = pool): Promise<StoredPlan> { export async function loadPlan(id: string, client: Pick<PoolClient, "query"> = pool): Promise<StoredPlan> {
@@ -197,6 +237,10 @@ export async function approvePlan(id: string, actor: AuthUser, approve: boolean)
if (!["awaiting-approval", "draft"].includes(plan.state) || plan.expires_at.getTime() <= Date.now() || plan.generation !== await accessGeneration()) throw new McpError("PLAN_EXPIRED", "План устарел или уже запущен", 409) if (!["awaiting-approval", "draft"].includes(plan.state) || plan.expires_at.getTime() <= Date.now() || plan.generation !== await accessGeneration()) throw new McpError("PLAN_EXPIRED", "План устарел или уже запущен", 409)
const action = findActionByOperation(plan.operation_id) const action = findActionByOperation(plan.operation_id)
if (!hasPermission(actor.permissions, "mm:mcp:approve") || action.operation.requiredPermissions.some(permission => !hasPermission(actor.permissions, permission))) throw new McpError("PERMISSION_DENIED", "Нет прав на подтверждение действия") if (!hasPermission(actor.permissions, "mm:mcp:approve") || action.operation.requiredPermissions.some(permission => !hasPermission(actor.permissions, permission))) throw new McpError("PERMISSION_DENIED", "Нет прав на подтверждение действия")
if (approve && actor.id !== connection.owner_id && plan.operation_id === "routing.community.switch" &&
requiresScopedRoutingOwnerApproval(JSON.parse(await decryptSecret(plan.evidence.sealed)) as ChangeEvidence)) {
throw new McpError("PERMISSION_DENIED", "Переключение постоянного профиля подтверждает владелец подключения", 403)
}
await client.query("UPDATE mcp_plans SET state=$2,approved_by=$3,approved_at=now() WHERE id=$1", [id, approve ? "approved" : "cancelled", actor.id]) await client.query("UPDATE mcp_plans SET state=$2,approved_by=$3,approved_at=now() WHERE id=$1", [id, approve ? "approved" : "cancelled", actor.id])
await audit(client, actor.id, approve ? "plan.approve" : "plan.reject", connection.id, { planId: id }) await audit(client, actor.id, approve ? "plan.approve" : "plan.reject", connection.id, { planId: id })
}) })
@@ -236,7 +280,7 @@ async function serviceExecute(service: string, input: Record<string, unknown>, c
if (service === "users.create") return createUser(appUserCreateSchema.parse(input.user)) if (service === "users.create") return createUser(appUserCreateSchema.parse(input.user))
if (service === "users.update") return updateUser(String(input.id), appUserUpdateSchema.parse(input.user)) if (service === "users.update") return updateUser(String(input.id), appUserUpdateSchema.parse(input.user))
if (service === "users.delete") return deleteUser(String(input.id)) if (service === "users.delete") return deleteUser(String(input.id))
if (service === "users.binding_add") return addBinding(String(input.id), userBindingCreateSchema.parse(input.binding)) if (service === "users.binding_add") return addBinding(String(input.id), userBindingCreateSchema.parse(input.binding), { assertCurrent: assertVerifiedConfigurationAccess })
if (service === "users.binding_remove") return removeBinding(String(input.id), String(input.bindingId)) if (service === "users.binding_remove") return removeBinding(String(input.id), String(input.bindingId))
if (service === "logs.settings.update") { const result = await updateLogSettings(logSettingsPatchSchema.parse(input.settings)); await refreshScheduler(); return result } if (service === "logs.settings.update") { const result = await updateLogSettings(logSettingsPatchSchema.parse(input.settings)); await refreshScheduler(); return result }
throw new McpError("UNSUPPORTED_CAPABILITY", "Неизвестный сервис", 409) throw new McpError("UNSUPPORTED_CAPABILITY", "Неизвестный сервис", 409)
@@ -260,7 +304,11 @@ export async function verifyStep(step: ChangeStep, result: unknown, before?: Ros
if (step.service === "users.binding_remove") { const current = await getUserById(String(step.input.id)); return !!current && !current.bindings.some(binding => binding.id === step.input.bindingId) } if (step.service === "users.binding_remove") { const current = await getUserById(String(step.input.id)); return !!current && !current.bindings.some(binding => binding.id === step.input.bindingId) }
if (step.service === "users.binding_add") { if (step.service === "users.binding_add") {
const binding = userBindingCreateSchema.parse(step.input.binding), current = await getUserById(String(step.input.id)) const binding = userBindingCreateSchema.parse(step.input.binding), current = await getUserById(String(step.input.id))
return !!current?.bindings.some(item => item.serverId === binding.serverId && item.interfaceName === binding.interfaceName && item.peerPublicKey === (binding.peerPublicKey ?? "")) const createdId = result && typeof result === "object" ? (result as { id?: unknown }).id : undefined
return typeof createdId === "string" && !!current?.bindings.some(item => item.id === createdId
&& item.serverId === binding.serverId && item.interfaceName === binding.interfaceName.trim()
&& (binding.interfaceType === undefined || item.interfaceType === binding.interfaceType)
&& item.peerPublicKey === normalizeBindingPeer(item.interfaceType, binding.peerPublicKey))
} }
if (step.service === "logs.settings.update") { if (step.service === "logs.settings.update") {
const current = await getLogSettings() as unknown as Record<string, unknown> const current = await getLogSettings() as unknown as Record<string, unknown>
@@ -291,6 +339,7 @@ export async function applyPlan(principal: McpPrincipal, planId: string, idempot
reservedRouting = !!await routingReservationEvidence(original, originalEvidence) reservedRouting = !!await routingReservationEvidence(original, originalEvidence)
return existing return existing
} }
await refuseExpiredOperationReplay(client, principal, connection.id, planId, idempotencyKey)
await client.query("SELECT pg_advisory_xact_lock(hashtextextended('mcp-runner-admission',0))") await client.query("SELECT pg_advisory_xact_lock(hashtextextended('mcp-runner-admission',0))")
const pending = await client.query<{ count: number }>("SELECT count(*)::int AS count FROM mcp_operations WHERE state IN ('pending','running')") const pending = await client.query<{ count: number }>("SELECT count(*)::int AS count FROM mcp_operations WHERE state IN ('pending','running')")
if (pending.rows[0].count >= 100) throw new McpError("BUDGET_EXCEEDED", "Очередь исполнителя заполнена; повторите подготовленное применение позже", 429) if (pending.rows[0].count >= 100) throw new McpError("BUDGET_EXCEEDED", "Очередь исполнителя заполнена; повторите подготовленное применение позже", 429)
@@ -301,6 +350,11 @@ export async function applyPlan(principal: McpPrincipal, planId: string, idempot
const action = findActionByOperation(plan.operation_id) 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 (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) if (!["draft", "approved"].includes(plan.state)) throw new McpError("APPROVAL_REQUIRED", "План требует подтверждения или недоступен", 409)
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)
scopedPathExpiresAt = await readScopedRoutingPathAdmissionDeadline(client, evidence, connection.id)
}
if (evidence.validUntil && (!Number.isFinite(Date.parse(evidence.validUntil)) || Date.now() >= Date.parse(evidence.validUntil))) { if (evidence.validUntil && (!Number.isFinite(Date.parse(evidence.validUntil)) || Date.now() >= Date.parse(evidence.validUntil))) {
throw new McpError("MEASUREMENT_UNKNOWN", "Измерения плана устарели; подготовьте новый план", 409) throw new McpError("MEASUREMENT_UNKNOWN", "Измерения плана устарели; подготовьте новый план", 409)
} }
@@ -323,7 +377,7 @@ export async function applyPlan(principal: McpPrincipal, planId: string, idempot
{ {
const grantWall=Date.now(),grantMonotonic=performance.now() const grantWall=Date.now(),grantMonotonic=performance.now()
const grant=await readMcpTemporaryGrantOrigin(client,connection.id,principal.credentialId) const grant=await readMcpTemporaryGrantOrigin(client,connection.id,principal.credentialId)
const admissionExpiresAt=Math.min(plan.expires_at.getTime(),evidence.validUntil?Date.parse(evidence.validUntil):Infinity,Date.parse(principal.credentialExpiresAt),Date.parse(connection.policy.expiresAt),connection.owner_valid_until.getTime(),grant.expiresAt??Infinity) const admissionExpiresAt=Math.min(plan.expires_at.getTime(),evidence.validUntil?Date.parse(evidence.validUntil):Infinity,Date.parse(principal.credentialExpiresAt),Date.parse(connection.policy.expiresAt),connection.owner_valid_until.getTime(),grant.expiresAt??Infinity,scopedPathExpiresAt??Infinity)
originalServiceGrant=Object.freeze({...grant,monotonicEnd:grant.expiresAt===null?Infinity:grantMonotonic+Math.max(0,grant.expiresAt-grantWall),admissionExpiresAt,admissionMonotonicEnd:grantMonotonic+Math.max(0,admissionExpiresAt-grantWall)}) originalServiceGrant=Object.freeze({...grant,monotonicEnd:grant.expiresAt===null?Infinity:grantMonotonic+Math.max(0,grant.expiresAt-grantWall),admissionExpiresAt,admissionMonotonicEnd:grantMonotonic+Math.max(0,admissionExpiresAt-grantWall)})
await assertOriginalMcpTemporaryGrant(client,connection.id,principal.credentialId,originalServiceGrant) await assertOriginalMcpTemporaryGrant(client,connection.id,principal.credentialId,originalServiceGrant)
} }
@@ -359,6 +413,7 @@ export async function applyPlan(principal: McpPrincipal, planId: string, idempot
async function executeOperation(operationId: string, principal: McpPrincipal, routingReservationForward?: RoutingReservedForwardAllocation, routingReservationId?: string,originalServiceGrant?:OriginalServiceGrant, operationAdmission?: McpOperationAdmission): Promise<void> { async function executeOperation(operationId: string, principal: McpPrincipal, routingReservationForward?: RoutingReservedForwardAllocation, routingReservationId?: string,originalServiceGrant?:OriginalServiceGrant, operationAdmission?: McpOperationAdmission): Promise<void> {
return withMcpOperationExecutionOrigin(operationAdmission, async () => { return withMcpOperationExecutionOrigin(operationAdmission, async () => {
const lease = await pool.connect() const lease = await pool.connect()
let terminalProbe: Awaited<ReturnType<typeof executeOperationWithLease>>
let locked = false, released = false let locked = false, released = false
const ownerLifetime = new AbortController() const ownerLifetime = new AbortController()
// This closure is the SINGLE owner of physical destruction/release. // This closure is the SINGLE owner of physical destruction/release.
@@ -380,7 +435,7 @@ async function executeOperation(operationId: string, principal: McpPrincipal, ro
throw error throw error
} }
if (released || !locked) return if (released || !locked) return
await withOperationConfigurationLockClient(lease, retire, ownerLifetime.signal, terminalProbe = await withOperationConfigurationLockClient(lease, retire, ownerLifetime.signal,
() => executeOperationWithLease(operationId, principal, routingReservationForward, routingReservationId, originalServiceGrant)) () => executeOperationWithLease(operationId, principal, routingReservationForward, routingReservationId, originalServiceGrant))
} finally { } finally {
if (!released && locked) { if (!released && locked) {
@@ -395,14 +450,20 @@ async function executeOperation(operationId: string, principal: McpPrincipal, ro
lease.release() lease.release()
} }
} }
// Capture has its own bounded checkout and may fail without rewriting the
// acknowledged terminal outcome or redispatching the diagnostic.
if (terminalProbe) {
try { await capturePublishedServerProbeTerminalProof(terminalProbe) } catch { /* Existing retention remains authoritative. */ }
}
}) })
} }
async function executeOperationWithLease(operationId: string, principal: McpPrincipal, routingReservationForward?: RoutingReservedForwardAllocation, routingReservationId?: string,originalServiceGrant?:OriginalServiceGrant): Promise<void> { async function executeOperationWithLease(operationId: string, principal: McpPrincipal, routingReservationForward?: RoutingReservedForwardAllocation, routingReservationId?: string,originalServiceGrant?:OriginalServiceGrant) {
const { rows: [operation] } = await pool.query<StoredOperation>("SELECT * FROM mcp_operations WHERE id=$1 AND state='running'", [operationId]) const { rows: [operation] } = await pool.query<StoredOperation>("SELECT * FROM mcp_operations WHERE id=$1 AND state='running'", [operationId])
if (!operation) return if (!operation) return
const plan = await loadPlan(operation.plan_id), evidence = JSON.parse(await decryptSecret(plan.evidence.sealed)) as ChangeEvidence 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] : []) 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 attempted = false, confirmed = 0, activeServiceStep = false
let vrfDiagnosticFallback: ReturnType<typeof createVrfRuntimeDiagnosticBoundary> | undefined
let reservedForwardPassed = false let reservedForwardPassed = false
let reservedRecoverySummary: Record<string, unknown> | undefined let reservedRecoverySummary: Record<string, unknown> | undefined
const diagnosticOnly = evidence.steps.length === 1 && evidence.steps.every(step => step.kind === "service" && findMcpService(step.service)?.cancellableDiagnostic === true) const diagnosticOnly = evidence.steps.length === 1 && evidence.steps.every(step => step.kind === "service" && findMcpService(step.service)?.cancellableDiagnostic === true)
@@ -510,17 +571,23 @@ async function executeOperationWithLease(operationId: string, principal: McpPrin
originalExpiresAt:Math.min(plan.expires_at.getTime(),evidence.validUntil?Date.parse(evidence.validUntil):Infinity,Date.parse(principal.credentialExpiresAt),Date.parse(connection.policy.expiresAt),connection.owner_valid_until.getTime(),originalServiceGrant!.admissionExpiresAt), originalExpiresAt:Math.min(plan.expires_at.getTime(),evidence.validUntil?Date.parse(evidence.validUntil):Infinity,Date.parse(principal.credentialExpiresAt),Date.parse(connection.policy.expiresAt),connection.owner_valid_until.getTime(),originalServiceGrant!.admissionExpiresAt),
serviceInput:step.input}:undefined serviceInput:step.input}:undefined
const reservedStep: { outcome?: ReservedRoutingStepOutcome<unknown> } = {} 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 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() } } } : {}) },
async () => { async () => {
vrfDiagnosticFallback?.executorEntered()
if (step.kind === "ros") { if (step.kind === "ros") {
if (!productionVrf) return executeRosStep(step.step) if (!productionVrf) return executeRosStep(step.step)
const before = evidence.snapshots.find(snapshot => snapshot.resource === "vrf" && snapshot.serverId === step.step.serverId) const before = evidence.snapshots.find(snapshot => snapshot.resource === "vrf" && snapshot.serverId === step.step.serverId)
if (!before?.sourceProof) throw new McpError("STATE_CONFLICT", "Нет VRF source-v3 proof", 409) if (!before?.sourceProof) throw new McpError("STATE_CONFLICT", "Нет VRF source-v3 proof", 409)
if (!["ros.vrf.create", "ros.vrf.update", "ros.vrf.delete", "routing.vrf.update"].includes(plan.operation_id)) throw new McpError("STATE_CONFLICT", "VRF action binding изменён", 409) if (!["ros.vrf.create", "ros.vrf.update", "ros.vrf.delete", "routing.vrf.update"].includes(plan.operation_id)) throw new McpError("STATE_CONFLICT", "VRF action binding изменён", 409)
return executeVrfProductionStep({ operationId, planId: plan.id, connectionId: connection.id, ownerId: connection.owner_id, const receipt = await executeVrfProductionStep({ operationId, planId: plan.id, connectionId: connection.id, ownerId: connection.owner_id,
operationName: plan.operation_id as "ros.vrf.create" | "ros.vrf.update" | "ros.vrf.delete" | "routing.vrf.update", operationVersion: plan.operation_version, operationName: plan.operation_id as "ros.vrf.create" | "ros.vrf.update" | "ros.vrf.delete" | "routing.vrf.update", operationVersion: plan.operation_version,
stepIndex: index, step: step.step, sourceProof: before.sourceProof, originalSnapshots: evidence.snapshots, authorize: authorizeCatalog, stepIndex: index, step: step.step, sourceProof: before.sourceProof, originalSnapshots: evidence.snapshots, authorize: authorizeCatalog,
assertDependencies: () => prefix!.check(authorizeCatalog), installBeforeDispatch: guard => { vrfBeforeDispatch = guard } }) assertDependencies: () => prefix!.check(authorizeCatalog), installBeforeDispatch: guard => { vrfBeforeDispatch = guard },
onDiagnostic: diagnostic => { vrfDiagnosticFallback?.observed(diagnostic) } })
vrfDiagnosticFallback?.executorReturned()
return receipt
} }
const forward = () => serviceExecute(step.service, step.input, { operationId, phase: "forward", stepIndex: index, ...(routingReservationForward ? { routingReservationForward } : {}) }) const forward = () => serviceExecute(step.service, step.input, { operationId, phase: "forward", stepIndex: index, ...(routingReservationForward ? { routingReservationForward } : {}) })
if (!routingReservationId) return step.service === "servers.poll" ? withRuntimePollPrincipalSource(principal, async (current, policyLoan) => { if (!routingReservationId) return step.service === "servers.poll" ? withRuntimePollPrincipalSource(principal, async (current, policyLoan) => {
@@ -533,6 +600,7 @@ async function executeOperationWithLease(operationId: string, principal: McpPrin
reservedStep.outcome = await executeReservedRoutingStep({ reservationId: routingReservationId, sourceOperationId: operationId, principal, forward }) reservedStep.outcome = await executeReservedRoutingStep({ reservationId: routingReservationId, sourceOperationId: operationId, principal, forward })
return reservedStep.outcome.forwardResult return reservedStep.outcome.forwardResult
}) })
vrfDiagnosticFallback = undefined
if (reservedStep.outcome?.kind === "recovered") { if (reservedStep.outcome?.kind === "recovered") {
// The generic source remains intent/result=null until the private // The generic source remains intent/result=null until the private
// inverse has finished. Record the forward ACK, never B VERIFIED // inverse has finished. Record the forward ACK, never B VERIFIED
@@ -576,14 +644,18 @@ async function executeOperationWithLease(operationId: string, principal: McpPrin
}) })
await (evidence.steps.some(step => step.kind === "service") ? withApplicationMutationLock(applyWithLocks) : applyWithLocks()) await (evidence.steps.some(step => step.kind === "service") ? withApplicationMutationLock(applyWithLocks) : applyWithLocks())
assertOperationConfigurationLockCheckpoint() assertOperationConfigurationLockCheckpoint()
await transaction(async client => { const receiptId = plan.operation_id === SERVER_PROBE_OPERATION ? diagnosticReceipt?.receiptId : undefined
const finishedAt = await transaction(async client => {
assertOperationConfigurationLockCheckpoint() assertOperationConfigurationLockCheckpoint()
await client.query("UPDATE mcp_operations SET state='applied',verification=$3,result=$2::jsonb,finished_at=now() WHERE id=$1", [operationId, JSON.stringify({ confirmedWrites: diagnosticOnly ? 0 : confirmed, verificationScope: evidence.verificationScope ?? "configuration", ...(diagnosticOnly ? { completedServiceSteps: confirmed, diagnosticReceipt } : {}) }), evidence.verificationScope === "acknowledgement" ? "inconclusive" : "passed"]) const terminal = await client.query<{ finished_at: string }>("UPDATE mcp_operations SET state='applied',verification=$3,result=$2::jsonb,finished_at=now() WHERE id=$1"
+ (receiptId ? ` RETURNING to_char(finished_at AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS finished_at` : ""),
[operationId, JSON.stringify({ confirmedWrites: diagnosticOnly ? 0 : confirmed, verificationScope: evidence.verificationScope ?? "configuration", ...(diagnosticOnly ? { completedServiceSteps: confirmed, diagnosticReceipt } : {}) }), evidence.verificationScope === "acknowledgement" ? "inconclusive" : "passed"])
assertOperationConfigurationLockCheckpoint() assertOperationConfigurationLockCheckpoint()
await client.query("UPDATE mcp_plans SET state='completed' WHERE id=$1", [plan.id]) await client.query("UPDATE mcp_plans SET state='completed' WHERE id=$1", [plan.id])
assertOperationConfigurationLockCheckpoint() assertOperationConfigurationLockCheckpoint()
await audit(client, "mcp-runner", "operation.complete", operation.connection_id, { operationId, confirmedWrites: diagnosticOnly ? 0 : confirmed, ...(diagnosticOnly ? { completedServiceSteps: confirmed, diagnosticReceipt } : {}) }) await audit(client, "mcp-runner", "operation.complete", operation.connection_id, { operationId, confirmedWrites: diagnosticOnly ? 0 : confirmed, ...(diagnosticOnly ? { completedServiceSteps: confirmed, diagnosticReceipt } : {}) })
assertOperationConfigurationLockCheckpoint() assertOperationConfigurationLockCheckpoint()
return receiptId ? terminal.rows[0]?.finished_at : undefined
}) })
assertOperationConfigurationLockCheckpoint() assertOperationConfigurationLockCheckpoint()
if (routingReservationId && reservedForwardPassed) { if (routingReservationId && reservedForwardPassed) {
@@ -592,7 +664,9 @@ async function executeOperationWithLease(operationId: string, principal: McpPrin
await createRoutingReservationStore().proveUnusedInverse(routingReservationId).catch(() => {}) await createRoutingReservationStore().proveUnusedInverse(routingReservationId).catch(() => {})
} }
assertOperationConfigurationLockCheckpoint() assertOperationConfigurationLockCheckpoint()
if (receiptId && finishedAt) return { receiptId, operationId, connectionId:operation.connection_id, finishedAt }
} catch (error) { } catch (error) {
vrfDiagnosticFallback?.failure(error)
if (routingReservationId && hasConfirmedReservedRoutingForward(error)) confirmed = Math.max(confirmed, 1) if (routingReservationId && hasConfirmedReservedRoutingForward(error)) confirmed = Math.max(confirmed, 1)
if (error instanceof VrfProductionAcknowledgedFailure) confirmed = Math.max(confirmed, error.acknowledgedWrites) if (error instanceof VrfProductionAcknowledgedFailure) confirmed = Math.max(confirmed, error.acknowledgedWrites)
const code = error instanceof VrfProductionClaimOutcomeUnknown ? "VRF_CLAIM_OUTCOME_UNKNOWN" : error instanceof McpError ? error.code : "DEVICE_UNAVAILABLE" const code = error instanceof VrfProductionClaimOutcomeUnknown ? "VRF_CLAIM_OUTCOME_UNKNOWN" : error instanceof McpError ? error.code : "DEVICE_UNAVAILABLE"
@@ -12,9 +12,9 @@ const principal = { connectionId, credentialId, credentialExpiresAt: new Date(no
const proof = async (value: unknown) => createHash("sha256").update(JSON.stringify(value)).digest("hex") const proof = async (value: unknown) => createHash("sha256").update(JSON.stringify(value)).digest("hex")
const context = { principal, connection: { id: connectionId, owner_id: "owner", owner_claims_issued_at: new Date(now - 1000) } } as ActionContext const context = { principal, connection: { id: connectionId, owner_id: "owner", owner_claims_issued_at: new Date(now - 1000) } } as ActionContext
let assertions = 0 let assertions = 0
function fixture() { function fixture(retiredSource?: ServerProbeMcpDependencies["retiredSource"]) {
const events: string[] = [], receipts = new Map<string, ServerProbeReceipt>(), contexts: ServerProbeJournalContext[] = [], sealed = new Map<string, string>() const events: string[] = [], receipts = new Map<string, ServerProbeReceipt>(), contexts: ServerProbeJournalContext[] = [], sealed = new Map<string, string>()
let evidence: ChangeEvidence, result: unknown = null, stepState = "intent", operationState = "running", scope = [2, 9], revoked = false, sqlTamper: string | undefined, dispatchFailure: unknown, preparedTool: PreparedServerProbe["tool"] = "ping", mismatchDestination = false, nextCipher = 0, revokeOnQuery = false, expiredPrepared = false, clock = now let evidence: ChangeEvidence, result: unknown = null, stepState = "intent", operationState = "running", scope = [2, 9], revoked = false, sqlTamper: string | undefined, dispatchFailure: unknown, preparedTool: PreparedServerProbe["tool"] = "ping", mismatchDestination = false, nextCipher = 0, revokeOnQuery = false, expiredPrepared = false, clock = now, liveHistory = true
const access: ServerProbeAuthority = { transport: "mcp", ownerId: "owner", connectionId, credentialId, expiresAt: now + 600000, generation: "generation", policyProof: "1".repeat(64), ownerProof: "2".repeat(64), active: true, operationAllowed: true, scopeServerIds: scope, ownerPermissions: ["mm:servers:write"], policyPermissions: ["mm:servers:write"] } const access: ServerProbeAuthority = { transport: "mcp", ownerId: "owner", connectionId, credentialId, expiresAt: now + 600000, generation: "generation", policyProof: "1".repeat(64), ownerProof: "2".repeat(64), active: true, operationAllowed: true, scopeServerIds: scope, ownerPermissions: ["mm:servers:write"], policyPermissions: ["mm:servers:write"] }
const readAuthority = async () => { events.push("authority"); if (revoked) throw new ServerProbeExecutionError("PERMISSION_DENIED", "PRIVATE_PROVIDER_SECRET", 403); return { ...access, expiresAt: clock + 600000, scopeServerIds: [...scope] } } const readAuthority = async () => { events.push("authority"); if (revoked) throw new ServerProbeExecutionError("PERMISSION_DENIED", "PRIVATE_PROVIDER_SECRET", 403); return { ...access, expiresAt: clock + 600000, scopeServerIds: [...scope] } }
const assertProtections = async (ids: number[]) => { events.push("protections"); if (ids.some(id => !scope.includes(id))) throw new Error("out of scope") } const assertProtections = async (ids: number[]) => { events.push("protections"); if (ids.some(id => !scope.includes(id))) throw new Error("out of scope") }
@@ -28,6 +28,7 @@ function fixture() {
read: async id => { events.push("journal.read"); return receipts.get(id) }, read: async id => { events.push("journal.read"); return receipts.get(id) },
} }
const deps: ServerProbeMcpDependencies = { const deps: ServerProbeMcpDependencies = {
retiredSource,
authority: (() => ({ readAuthority, assertProtections })) as ServerProbeMcpDependencies["authority"], authority: (() => ({ readAuthority, assertProtections })) as ServerProbeMcpDependencies["authority"],
historicalAuthority: ((currentPrincipal: typeof principal) => ({ readAuthority: async () => { events.push("read-authority"); return { ...await readAuthority(), connectionId: currentPrincipal.connectionId, credentialId: currentPrincipal.credentialId } }, assertProtections })) as ServerProbeMcpDependencies["historicalAuthority"], historicalAuthority: ((currentPrincipal: typeof principal) => ({ readAuthority: async () => { events.push("read-authority"); return { ...await readAuthority(), connectionId: currentPrincipal.connectionId, credentialId: currentPrincipal.credentialId } }, assertProtections })) as ServerProbeMcpDependencies["historicalAuthority"],
history: dependencies => createServerProbeJournalHistoryReader({ ...dependencies, loadArchive: async binding => { history: dependencies => createServerProbeJournalHistoryReader({ ...dependencies, loadArchive: async binding => {
@@ -51,7 +52,7 @@ function fixture() {
journal: (ctx, dependencies, execution) => { assert.equal(execution?.deadlineAt, Date.parse(prepared().expiresAt)); contexts.push(ctx); return { ...journal, read: async (...args) => { await dependencies.assertAuthority(ctx, args[1]); return journal.read(...args) }, claim: async binding => { await dependencies.assertAuthority(ctx, binding.bindingProof); return journal.claim(binding) } } }, journal: (ctx, dependencies, execution) => { assert.equal(execution?.deadlineAt, Date.parse(prepared().expiresAt)); contexts.push(ctx); return { ...journal, read: async (...args) => { await dependencies.assertAuthority(ctx, args[1]); return journal.read(...args) }, claim: async binding => { await dependencies.assertAuthority(ctx, binding.bindingProof); return journal.claim(binding) } } },
query: async (sql, values) => { query: async (sql, values) => {
events.push("sql"); if (revokeOnQuery) revoked = true events.push("sql"); if (revokeOnQuery) revoked = true
if (sql.includes("probe-history:scope")) { assert.equal(values[2], connectionId); assert.equal(values[3], "owner"); return [{ source_server_id: 9, destination_server_id: preparedTool === "bandwidth" ? 2 : null }] } if (sql.includes("probe-history:scope")) { assert.equal(values[2], connectionId); assert.equal(values[3], "owner"); return liveHistory ? [{ source_server_id: 9, destination_server_id: preparedTool === "bandwidth" ? 2 : null }] : [] }
if (sql.includes("FROM servers")) { assert.ok(sql.includes("ANY($1::int[])")); assert.deepEqual(values[0], scope); return sql.includes("AND id=$2") ? [] : [{ id: 2, host: "peer", enabled: true }] } if (sql.includes("FROM servers")) { assert.ok(sql.includes("ANY($1::int[])")); assert.deepEqual(values[0], scope); return sql.includes("AND id=$2") ? [] : [{ id: 2, host: "peer", enabled: true }] }
if (!sql.includes("mcp_operation_steps")) return [{ evidence: { sealed: await seal(JSON.stringify(evidence)) } }] if (!sql.includes("mcp_operation_steps")) return [{ evidence: { sealed: await seal(JSON.stringify(evidence)) } }]
assert.equal(values[0], connectionId); assert.equal(values[1], "owner"); assert.equal(values[2], "servers.probes.run") assert.equal(values[0], connectionId); assert.equal(values[1], "owner"); assert.equal(values[2], "servers.probes.run")
@@ -68,8 +69,9 @@ function fixture() {
}, seal, open, proof, now: () => clock, }, seal, open, proof, now: () => clock,
} }
const adapter = createServerProbeMcpAdapter(deps) const adapter = createServerProbeMcpAdapter(deps)
const retireHistory = () => { liveHistory = false }
const prepare = async (tool: PreparedServerProbe["tool"] = "ping") => { evidence = await adapter.prepare({ serverId: 9, request: tool === "bandwidth" ? { tool, bwRemoteAddress: "peer", dstServerId: 8 } : { tool, target: "192.0.2.1" } }, context); const step = evidence.steps[0]; assert.equal(step.kind, "service"); return step.kind === "service" ? step.input : assert.fail() } const prepare = async (tool: PreparedServerProbe["tool"] = "ping") => { evidence = await adapter.prepare({ serverId: 9, request: tool === "bandwidth" ? { tool, bwRemoteAddress: "peer", dstServerId: 8 } : { tool, target: "192.0.2.1" } }, context); const step = evidence.steps[0]; assert.equal(step.kind, "service"); return step.kind === "service" ? step.input : assert.fail() }
return { adapter, events, receipts, contexts, prepare, setResult: (value: unknown) => { result = value; stepState = "applied" }, finishOperation: () => { operationState = "applied"; stepState = "acknowledged" }, setTamper: (value?: string) => { sqlTamper = value }, revoke: () => { revoked = true }, setScope: (value: number[]) => { scope = value }, fail: (value: unknown) => { dispatchFailure = value }, revokeOnQuery: () => { revokeOnQuery = true }, evidence: () => evidence, mismatchDestination: () => { mismatchDestination = true }, expirePrepared: () => { expiredPrepared = true; clock = now + 3600000 } } return { adapter, events, receipts, contexts, prepare, retireHistory, setResult: (value: unknown) => { result = value; stepState = "applied" }, finishOperation: () => { operationState = "applied"; stepState = "acknowledged" }, setTamper: (value?: string) => { sqlTamper = value }, revoke: () => { revoked = true }, setScope: (value: number[]) => { scope = value }, fail: (value: unknown) => { dispatchFailure = value }, revokeOnQuery: () => { revokeOnQuery = true }, evidence: () => evidence, mismatchDestination: () => { mismatchDestination = true }, expirePrepared: () => { expiredPrepared = true; clock = now + 3600000 } }
} }
for (const tool of ["ping", "traceroute", "bandwidth", "dns", "route", "mtu"] as const) { for (const tool of ["ping", "traceroute", "bandwidth", "dns", "route", "mtu"] as const) {
@@ -128,6 +130,38 @@ for (const input of [{ serverId: 9, request: { tool: "ping", target: "x", creden
await assert.rejects(f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0, signal: controller.signal })); assert.equal(f.events.filter(event => event === "execute").length, attempted); assertions += 3 await assert.rejects(f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0, signal: controller.signal })); assert.equal(f.events.filter(event => event === "execute").length, attempted); assertions += 3
} }
assert.ok(!("diagnosticProgress" in serverProbeSafeMcpError(new ServerProbeExecutionError("X", "secret", 500, "unknown", 99), { receiptId, operationId, stageBudget: 1 }))); assertions++ assert.ok(!("diagnosticProgress" in serverProbeSafeMcpError(new ServerProbeExecutionError("X", "secret", 500, "unknown", 99), { receiptId, operationId, stageBudget: 1 }))); assertions++
{
const hash = "3".repeat(64), date = (value: number) => new Date(value).toISOString().replace("Z", "000Z")
const retired = { source: { version: 1 as const, operation: "servers.probes.run" as const, operationVersion: 1 as const, tool: "ping" as const,
receiptId, executionId: operationId, connectionId, ownerId: "owner", planId: randomUUID(), originalCredentialId: credentialId,
sourceServerId: 9, serverIds: [9], issuedAt: now - 1000, authorityExpiresAt: date(now + 600000), terminalAt: date(now + 600000),
activityAt: date(now + 600000), gcNotBefore: date(now + 87060000), retentionDays: 1, safetyMs: 60000,
bindingProof: hash, sourceDigest: hash, terminalDigest: hash, stageSnapshotDigest: hash, receiptProof: hash, ciphertextSha256: hash,
}, payloadPurged: false }
let lookups = 0, changeOnRecheck = false
const f = fixture(async lookup => {
lookups++
assert.deepEqual(lookup, { receiptId, operationId, connectionId, ownerId: "owner", scopeServerIds: [2, 9] })
return changeOnRecheck && lookups % 2 === 0 ? undefined : retired
})
const input = await f.prepare(), output = await f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0 })
f.setResult(output); f.finishOperation()
const request = { operationId, receiptId, page: 0 }
const live = await f.adapter.readPage(request, context)
assert.equal(lookups, 0)
f.retireHistory()
const rotated = { ...context, principal: { ...principal, credentialId: randomUUID() } }
assert.deepEqual(await f.adapter.readPage(request, rotated), live)
assert.deepEqual(await f.adapter.receiptTarget(request, rotated), { scope: "servers", serverIds: [9] })
const beforeForeign = lookups
await assert.rejects(f.adapter.readPage(request, { ...rotated, connection: { ...context.connection, owner_id: "foreign" } }))
assert.equal(lookups, beforeForeign)
lookups = 0; changeOnRecheck = true
const privateReads = f.events.filter(event => event === "history.read").length
await assert.rejects(f.adapter.readPage(request, rotated))
assert.equal(f.events.filter(event => event === "history.read").length, privateReads)
assertions += 7
}
{ {
const f = fixture(), input = await f.prepare("bandwidth"), output = await f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0 }); f.setResult(output); f.finishOperation(); f.expirePrepared() const f = fixture(), input = await f.prepare("bandwidth"), output = await f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0 }); f.setResult(output); f.finishOperation(); f.expirePrepared()
await assert.rejects(f.adapter.service.current(input)) await assert.rejects(f.adapter.service.current(input))
@@ -10,6 +10,7 @@ import { serverProbeReceiptPage } from "../../services/server-probe-public-resul
import { decryptSecret, encryptSecret, fingerprint, privateSecretFingerprint, McpError, redact } from "./security.js" import { decryptSecret, encryptSecret, fingerprint, privateSecretFingerprint, McpError, redact } from "./security.js"
import type { ActionContext, ChangeEvidence } from "./action-types.js" import type { ActionContext, ChangeEvidence } from "./action-types.js"
import type { McpServiceHandler } from "./service-registry.js" import type { McpServiceHandler } from "./service-registry.js"
import type { findRetiredServerProbeHistory, ServerProbeRetiredLookup } from "../../services/server-probe-retired-history.js"
/** Foundations only: importing this module registers no tools or services. */ /** Foundations only: importing this module registers no tools or services. */
export const serverProbeActionSchema = z.object({ serverId: z.number().int().positive().safe(), request: serverProbeExecutionRequest }).strict() export const serverProbeActionSchema = z.object({ serverId: z.number().int().positive().safe(), request: serverProbeExecutionRequest }).strict()
@@ -32,6 +33,7 @@ export interface ServerProbeMcpDependencies {
executor: typeof createProductionServerProbeExecutor executor: typeof createProductionServerProbeExecutor
journal: (context: ServerProbeJournalContext, dependencies: ServerProbeJournalDependencies, execution?: { signal?: AbortSignal; deadlineAt?: number }) => ServerProbeJournal journal: (context: ServerProbeJournalContext, dependencies: ServerProbeJournalDependencies, execution?: { signal?: AbortSignal; deadlineAt?: number }) => ServerProbeJournal
query: Query query: Query
retiredSource?: (lookup: ServerProbeRetiredLookup) => ReturnType<typeof findRetiredServerProbeHistory>
seal: (value: string) => Promise<string> seal: (value: string) => Promise<string>
open: (value: string) => Promise<string> open: (value: string) => Promise<string>
proof: (value: unknown) => Promise<string> proof: (value: unknown) => Promise<string>
@@ -52,7 +54,7 @@ export async function openServerProbeMcpBinding(raw: Record<string, unknown>, cr
return { input, binding } return { input, binding }
} }
const ids = (prepared: PreparedServerProbe) => [...new Set([prepared.serverId, ...(prepared.destinationServerId ? [prepared.destinationServerId] : [])])].sort((a, b) => a - b) const ids = (prepared: PreparedServerProbe) => [...new Set([prepared.serverId, ...(prepared.destinationServerId ? [prepared.destinationServerId] : [])])].sort((a, b) => a - b)
function metadata(receipt: ServerProbeReceipt, operationId: string, stageBudget: number) { export function serverProbeReceiptMetadata(receipt: ServerProbeReceipt, operationId: string, stageBudget: number) {
const page = serverProbeReceiptPage(receipt, 0) const page = serverProbeReceiptPage(receipt, 0)
if (!Number.isSafeInteger(receipt.attemptedStages) || !Number.isSafeInteger(receipt.completedStages) || receipt.completedStages < 0 || receipt.completedStages > receipt.attemptedStages || receipt.attemptedStages > stageBudget) return denied() if (!Number.isSafeInteger(receipt.attemptedStages) || !Number.isSafeInteger(receipt.completedStages) || receipt.completedStages < 0 || receipt.completedStages > receipt.attemptedStages || receipt.attemptedStages > stageBudget) return denied()
return { operationId, receiptId: receipt.receiptId, tool: receipt.tool, status: receipt.state, httpStatus: receipt.httpStatus, stageBudget, attemptedStages: receipt.attemptedStages, completedStages: receipt.completedStages, sha256: page.sha256, totalPages: page.totalPages, bytes: page.bytes, automaticReplay: false, forwardingVerified: false } return { operationId, receiptId: receipt.receiptId, tool: receipt.tool, status: receipt.state, httpStatus: receipt.httpStatus, stageBudget, attemptedStages: receipt.attemptedStages, completedStages: receipt.completedStages, sha256: page.sha256, totalPages: page.totalPages, bytes: page.bytes, automaticReplay: false, forwardingVerified: false }
@@ -119,7 +121,7 @@ export function createServerProbeMcpAdapter(overrides: Partial<ServerProbeMcpDep
await owned(input, binding, row.operation_id) await owned(input, binding, row.operation_id)
await preflight(binding, signal) await preflight(binding, signal)
if (!receipt || receipt.receiptId !== input.receiptId || receipt.serverId !== binding.prepared.serverId || receipt.destinationServerId !== binding.prepared.destinationServerId || receipt.tool !== binding.prepared.tool) return denied() if (!receipt || receipt.receiptId !== input.receiptId || receipt.serverId !== binding.prepared.serverId || receipt.destinationServerId !== binding.prepared.destinationServerId || receipt.tool !== binding.prepared.tool) return denied()
return { row, receipt, result: metadata(receipt, row.operation_id, binding.stageBudget) } return { row, receipt, result: serverProbeReceiptMetadata(receipt, row.operation_id, binding.stageBudget) }
} }
const target = async (raw: Record<string, unknown>, context: ActionContext): Promise<ChangeEvidence["target"]> => { const target = async (raw: Record<string, unknown>, context: ActionContext): Promise<ChangeEvidence["target"]> => {
const input = serverProbeActionSchema.parse(raw), access = deps.authority(context.principal, { signal: context.signal }) const input = serverProbeActionSchema.parse(raw), access = deps.authority(context.principal, { signal: context.signal })
@@ -177,7 +179,7 @@ export function createServerProbeMcpAdapter(overrides: Partial<ServerProbeMcpDep
const { executor } = execution(input, binding, row, true, context.signal) const { executor } = execution(input, binding, row, true, context.signal)
const receipt = await executor.execute(binding.prepared, context.signal) const receipt = await executor.execute(binding.prepared, context.signal)
await preflight(binding, context.signal); await owned(input, binding, context.operationId, true); await preflight(binding, context.signal) await preflight(binding, context.signal); await owned(input, binding, context.operationId, true); await preflight(binding, context.signal)
return metadata(receipt, context.operationId, binding.stageBudget) return serverProbeReceiptMetadata(receipt, context.operationId, binding.stageBudget)
} catch (error) { throw serverProbeSafeMcpError(error, progress) } } catch (error) { throw serverProbeSafeMcpError(error, progress) }
}, },
verify: async (raw, result) => { verify: async (raw, result) => {
@@ -200,19 +202,47 @@ export function createServerProbeMcpAdapter(overrides: Partial<ServerProbeMcpDep
AND j.source_server_id=ANY($6::int[]) AND (j.destination_server_id IS NULL OR j.destination_server_id=ANY($6::int[])) LIMIT 2`, AND j.source_server_id=ANY($6::int[]) AND (j.destination_server_id IS NULL OR j.destination_server_id=ANY($6::int[])) LIMIT 2`,
[request.receiptId, request.operationId, context.principal.connectionId, initial.ownerId, SERVER_PROBE_OPERATION, initial.scopeServerIds]) [request.receiptId, request.operationId, context.principal.connectionId, initial.ownerId, SERVER_PROBE_OPERATION, initial.scopeServerIds])
await fresh() await fresh()
if (rows.length !== 1) return denied() const loadRetired = deps.retiredSource
const row = rows[0], serverIds = [...new Set([z.number().int().positive().safe().parse(row.source_server_id), ...(row.destination_server_id === null ? [] : [z.number().int().positive().safe().parse(row.destination_server_id)])])].sort((a, b) => a - b) const readRetired = rows.length === 0 && loadRetired ? () => loadRetired({
receiptId: request.receiptId, operationId: request.operationId, connectionId: context.principal.connectionId,
ownerId: initial.ownerId, scopeServerIds: initial.scopeServerIds,
}) : undefined
const retired = readRetired ? await readRetired() : undefined
if (readRetired) await fresh()
if (rows.length !== 1 && !retired) return denied()
const row = rows[0]
const serverIds = retired ? retired.source.serverIds : [...new Set([z.number().int().positive().safe().parse(row.source_server_id), ...(row.destination_server_id === null ? [] : [z.number().int().positive().safe().parse(row.destination_server_id)])])].sort((a, b) => a - b)
const fence = async () => { const current = await fresh(); if (serverIds.some(id => !current.scopeServerIds.includes(id))) return denied(); await access.assertProtections(serverIds, current); await fresh() } const fence = async () => { const current = await fresh(); if (serverIds.some(id => !current.scopeServerIds.includes(id))) return denied(); await access.assertProtections(serverIds, current); await fresh() }
await fence() await fence()
return { initial, serverIds, fence } const retiredFence = retired && readRetired ? async () => {
await fence()
const current = await readRetired()
await fence()
if (!current || fingerprint(current) !== fingerprint(retired)) return denied()
} : undefined
return { initial, serverIds, fence, retired, retiredFence }
} }
const receiptTarget = async (raw: Record<string, unknown>, context: ActionContext): Promise<ChangeEvidence["target"]> => { const receiptTarget = async (raw: Record<string, unknown>, context: ActionContext): Promise<ChangeEvidence["target"]> => {
const request = serverProbeReceiptReadSchema.parse(raw), { serverIds, fence } = await historicalAccess(request, context) const request = serverProbeReceiptReadSchema.parse(raw), { serverIds, fence, retiredFence } = await historicalAccess(request, context)
await fence(); return { scope: "servers", serverIds } await (retiredFence ?? fence)(); return { scope: "servers", serverIds }
} }
const readPage = async (raw: Record<string, unknown>, context: ActionContext) => { const readPage = async (raw: Record<string, unknown>, context: ActionContext) => {
try { try {
const request = serverProbeReceiptReadSchema.parse(raw), { initial, serverIds, fence } = await historicalAccess(request, context) const request = serverProbeReceiptReadSchema.parse(raw), { initial, serverIds, fence, retired, retiredFence } = await historicalAccess(request, context)
if (retired && retiredFence) {
const source = retired.source
const originalContext: ServerProbeJournalContext = {
identity: { kind: "mcp", ownerId: source.ownerId, executionId: source.executionId, connectionId: source.connectionId, credentialId: source.originalCredentialId,
issuedAt: source.issuedAt, expiresAt: Date.parse(source.authorityExpiresAt), operationName: SERVER_PROBE_OPERATION },
sourceServerId: source.sourceServerId, ...(source.destinationServerId !== undefined ? { destinationServerId: source.destinationServerId } : {}), tool: source.tool,
}
await retiredFence()
const reader = deps.history({ assertReadAuthority: retiredFence }, { signal: context.signal })
const receipt = await reader.read({ context: originalContext, receiptId: source.receiptId, bindingProof: source.bindingProof })
const page = serverProbeReceiptPage(receipt, request.page)
await retiredFence()
return page
}
await fence() await fence()
const rows = await deps.query(`SELECT p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id JOIN mcp_connections c ON c.id=o.connection_id WHERE o.id=$1 AND o.connection_id=$2 AND c.owner_id=$3 AND p.operation_id=$4 AND p.operation_version=1 AND octet_length(p.evidence::text)<=2097152 LIMIT 2`, [request.operationId, context.principal.connectionId, initial.ownerId, SERVER_PROBE_OPERATION]) const rows = await deps.query(`SELECT p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id JOIN mcp_connections c ON c.id=o.connection_id WHERE o.id=$1 AND o.connection_id=$2 AND c.owner_id=$3 AND p.operation_id=$4 AND p.operation_version=1 AND octet_length(p.evidence::text)<=2097152 LIMIT 2`, [request.operationId, context.principal.connectionId, initial.ownerId, SERVER_PROBE_OPERATION])
await fence(); if (rows.length !== 1) return denied() await fence(); if (rows.length !== 1) return denied()
@@ -171,6 +171,16 @@ export async function acceptServerProbeAllToolsSql() {
assert.equal(done.result.confirmedWrites, 0); assert.equal(done.result.completedServiceSteps, 1); assert.equal(done.result.verificationScope, "acknowledgement") assert.equal(done.result.confirmedWrites, 0); assert.equal(done.result.completedServiceSteps, 1); assert.equal(done.result.verificationScope, "acknowledgement")
const receipt = done.result.diagnosticReceipt as { receiptId: string; stageBudget: number; attemptedStages: number; completedStages: number; totalPages: number; sha256: string } const receipt = done.result.diagnosticReceipt as { receiptId: string; stageBudget: number; attemptedStages: number; completedStages: number; totalPages: number; sha256: string }
assert(receipt); assert.equal(receipt.attemptedStages, receipt.completedStages); assert(receipt.attemptedStages > 0 && receipt.attemptedStages <= receipt.stageBudget) assert(receipt); assert.equal(receipt.attemptedStages, receipt.completedStages); assert(receipt.attemptedStages > 0 && receipt.attemptedStages <= receipt.stageBudget)
let captured = false
for (const until = Date.now()+2000; Date.now()<until;) {
const proof = (await pool.query(`SELECT f.source->>'terminalAt'=to_char(GREATEST(o.finished_at,p.expires_at) AT TIME ZONE 'UTC',
'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS terminal_matches FROM server_probe_terminal_proofs f
JOIN mcp_operations o ON o.id=f.operation_id JOIN mcp_plans p ON p.id=o.plan_id
WHERE f.operation_id=$1 AND f.receipt_id=$2`, [done.id,receipt.receiptId])).rows[0]
if (proof) { assert.equal(proof.terminal_matches,true); captured = true; break }
await delay(20)
}
assert.equal(captured,true,"Owned successful diagnostic did not publish its terminal proof")
const step = (await pool.query("SELECT state,result FROM mcp_operation_steps WHERE operation_id=$1", [done.id])).rows const step = (await pool.query("SELECT state,result FROM mcp_operation_steps WHERE operation_id=$1", [done.id])).rows
assert.equal(step.length, 1); assert.equal(step[0].state, "acknowledged") assert.equal(step.length, 1); assert.equal(step[0].state, "acknowledged")
for (const field of ["output", "data", "contentB64", "bindingCiphertext"]) assert(!Object.hasOwn(step[0].result, field)) for (const field of ["output", "data", "contentB64", "bindingCiphertext"]) assert(!Object.hasOwn(step[0].result, field))
@@ -201,6 +211,49 @@ export async function acceptServerProbeAllToolsSql() {
cases.push(tool + ": actual public plan/approval/apply, receipt metadata, full stage ledger and checksum pages without redispatch") cases.push(tool + ": actual public plan/approval/apply, receipt metadata, full stage ledger and checksum pages without redispatch")
} }
// Force a genuine capture SQL failure after the real diagnostic has
// committed. A sequence records the rejected attempt despite rollback.
const captureFault = await fixture()
const faultPlan = await prepare(captureFault, { tool: "route", target: "127.0.0.1" })
await pool.query("CREATE SEQUENCE public.owned_probe_capture_failure")
try {
await pool.query(`CREATE FUNCTION public.owned_probe_capture_failure() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NEW.source->>'connectionId'=TG_ARGV[0] THEN
PERFORM nextval('public.owned_probe_capture_failure');
RAISE EXCEPTION 'owned terminal proof insertion refused';
END IF;
RETURN NEW;
END $$`)
try {
assert.match(captureFault.principal.connectionId,/^[a-f0-9-]{36}$/)
await pool.query(`CREATE TRIGGER owned_probe_capture_failure BEFORE INSERT ON public.server_probe_terminal_proofs
FOR EACH ROW EXECUTE FUNCTION public.owned_probe_capture_failure('${captureFault.principal.connectionId}')`)
try {
const beforeFault = requests+dnsUdp+dnsTcp, faultKey=randomUUID()
const applied = await applyPlan(captureFault.principal,faultPlan,faultKey), done=await terminal(applied.id)
let attempted = false
for (const until=Date.now()+2500;Date.now()<until;) {
attempted = (await pool.query("SELECT is_called FROM public.owned_probe_capture_failure")).rows[0].is_called
if (attempted) break
await delay(20)
}
assert.equal(attempted,true,"Owned post-COMMIT capture insert must have reached the injected SQL failure")
assert.equal(done.state,"applied"); assert.equal(done.error_code,null)
assert.equal((await pool.query("SELECT 1 FROM server_probe_terminal_proofs WHERE operation_id=$1",[applied.id])).rows.length,0)
const originalReceipt = done.result.diagnosticReceipt as { receiptId:string; sha256:string }
const page = await invokeMcpTool(captureFault.principal,"mm_server_probe_receipt_get",
{ operationId:applied.id,receiptId:originalReceipt.receiptId,page:0 }) as {sha256:string}
assert.equal(page.sha256,originalReceipt.sha256)
assert.equal((await applyPlan(captureFault.principal,faultPlan,faultKey)).id,applied.id)
assert.equal((await terminal(applied.id)).state,"applied")
assert.equal(requests+dnsUdp+dnsTcp,beforeFault+1)
assert.equal((await pool.query("SELECT last_value::int AS attempts FROM public.owned_probe_capture_failure")).rows[0].attempts,1)
} finally { await pool.query("DROP TRIGGER owned_probe_capture_failure ON public.server_probe_terminal_proofs") }
} finally { await pool.query("DROP FUNCTION public.owned_probe_capture_failure()") }
} finally { await pool.query("DROP SEQUENCE public.owned_probe_capture_failure") }
cases.push("terminal proof SQL refusal: acknowledged result remains applied and readable, exactly one loopback measurement and no retry")
// Pause only the one runner's pending->running SQL BEFORE sending it. This // Pause only the one runner's pending->running SQL BEFORE sending it. This
// leaves a real pending approved source, allowing genuine durable cancel. // leaves a real pending approved source, allowing genuine durable cancel.
// A server-table lock after 'running' would have no intent step yet and the // A server-table lock after 'running' would have no intent step yet and the
@@ -209,8 +262,9 @@ export async function acceptServerProbeAllToolsSql() {
const entered = gate(), resume = gate(); releasePending = resume.resolve const entered = gate(), resume = gate(); releasePending = resume.resolve
let heldOperation = "", held = false let heldOperation = "", held = false
pool.query = (async function (...args: unknown[]) { pool.query = (async function (...args: unknown[]) {
if (!held && typeof args[0] === "string" && args[0].startsWith("UPDATE mcp_operations SET state='running'")) { const query = args[0] as { text?: string; values?: unknown[] } | undefined
held = true; heldOperation = String((args[1] as unknown[])[0]); entered.resolve(); await resume.promise if (!held && query?.text?.startsWith("/* mcp-operation-root:claim */") && typeof query.values?.[0] === "string") {
held = true; heldOperation = query.values[0]; entered.resolve(); await resume.promise
} }
return Reflect.apply(originalQuery, pool, args) return Reflect.apply(originalQuery, pool, args)
}) as typeof pool.query }) as typeof pool.query
@@ -2,6 +2,7 @@
import { createMcpServerProbeAuthority, createMcpServerProbeReadAuthority } from "../../services/server-probe-authority.js" import { createMcpServerProbeAuthority, createMcpServerProbeReadAuthority } from "../../services/server-probe-authority.js"
import { createProductionServerProbeExecutor } from "../../services/server-probe-execution.js" import { createProductionServerProbeExecutor } from "../../services/server-probe-execution.js"
import { createServerProbeStorageHistoryReader } from "../../services/server-probe-storage-history.js" import { createServerProbeStorageHistoryReader } from "../../services/server-probe-storage-history.js"
import { findRetiredServerProbeHistory } from "../../services/server-probe-retired-history.js"
import { createSqlServerProbeSourceReaders } from "../../services/server-probe-storage-source.js" import { createSqlServerProbeSourceReaders } from "../../services/server-probe-storage-source.js"
import { createProbeStorageHostIo } from "../../services/server-probe-storage-host-io.js" import { createProbeStorageHostIo } from "../../services/server-probe-storage-host-io.js"
import { assertVerifiedConfigurationAccess } from "../../services/config-mutation-policy.js" import { assertVerifiedConfigurationAccess } from "../../services/config-mutation-policy.js"
@@ -28,6 +29,7 @@ export function createStorageServerProbeMcpAdapter(limits: ProbeStorageLimits =
}, },
} }
const adapter = createServerProbeMcpAdapter({ query: io.rows, journal: io.journal, const adapter = createServerProbeMcpAdapter({ query: io.rows, journal: io.journal,
retiredSource: lookup => findRetiredServerProbeHistory(io.rows, lookup),
authority: (principal, options) => createMcpServerProbeAuthority(principal, options, authorityDependencies), authority: (principal, options) => createMcpServerProbeAuthority(principal, options, authorityDependencies),
historicalAuthority: (principal, options) => createMcpServerProbeReadAuthority(principal, options, authorityDependencies), historicalAuthority: (principal, options) => createMcpServerProbeReadAuthority(principal, options, authorityDependencies),
history: (dependencies, execution) => createServerProbeStorageHistoryReader({ assertReadAuthority: dependencies.assertReadAuthority, history: (dependencies, execution) => createServerProbeStorageHistoryReader({ assertReadAuthority: dependencies.assertReadAuthority,
@@ -0,0 +1,15 @@
import assert from "node:assert/strict"
process.env.AUTH_REQUIRED = "true"
process.env.AUTH_JWT_SECRET = "users-bindings-owned-acceptance-secret"
const { pool, closePool } = await import("../../db/index.js")
const { applySqlMigrations } = await import("../../db/migrate.js")
const { updateMcpSettings } = await import("./store.js")
const actor = { id: "users-bindings-owner", email: "users@example.invalid", name: "User binding owner", apps: ["mm"], permissions: ["mm:settings:admin"], expiresAt: Date.now() + 3600000 }
try {
await applySqlMigrations(pool)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM servers")).rows[0].n, 0, "Disposable empty database required")
await updateMcpSettings({ enabled: true }, actor)
await (await import("../../test/mcp-users-bindings.js")).verifyMcpUserBindings(actor)
} finally {
await closePool()
}
@@ -0,0 +1,97 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { vrfPreparationDiagnostic } from "./vrf-preparation-diagnostic.js"
const requestId = "12345678-1234-4123-8123-123456789abc"
const operationId = "ros.vrf.update"
function fixture(message = "VRF production transition refused: static address provenance/context", stack?: string) {
const error = new Error(message)
if (stack !== undefined) Object.defineProperty(error, "stack", { value: stack, configurable: true, writable: true })
return error
}
const stack = "Error: SECRET-PASSWORD 192.0.2.9 native-body\n at must (C:\\private\\backend\\src\\services\\vrf-production-transition.ts:80:82)\n at wrapped (C:\\private\\backend\\src\\modules\\mcp\\vrf-ticket-runtime.ts:76:9)"
test("plain Error at known refusal produces bounded first owned source, stage and constant reason", () => {
const diagnostic = vrfPreparationDiagnostic(requestId, operationId, fixture(undefined, stack))
assert.deepEqual(diagnostic, { event: "mcp.vrf.preparation-failed", requestId, operationId, stage: "transition", errorClass: "Error", firstOwnedFrame: { source: "vrf-production-transition", line: 80 }, reason: "static-address-context" })
for (const secret of ["SECRET-PASSWORD", "192.0.2.9", "private", "native-body", "must", "stack", "message"]) assert.ok(!JSON.stringify(diagnostic).includes(secret), secret)
})
test("real constructed Error with native stack never returns function names or raw text", () => {
const diagnostic = vrfPreparationDiagnostic(requestId, "ros.vrf.create", new Error("SECRET native payload"))!
assert.equal(diagnostic.errorClass, "Error"); assert.equal(diagnostic.reason, "other")
assert.ok(!JSON.stringify(diagnostic).includes("SECRET"))
const known = new Error("VRF production transition refused: dependency connected route identity")
const result = vrfPreparationDiagnostic(requestId, operationId, known)!
assert.equal(result.reason, "connected-route-identity"); assert.equal(result.stage, "transition")
// Native lazy stack accessors stay unmaterialized. No prepareStackTrace hook
// is needed to classify the actual own-data refusal message.
assert.equal(typeof Object.getOwnPropertyDescriptor(known, "stack")?.get, "function")
assert.equal(result.firstOwnedFrame, undefined)
})
test("exact known messages map to enums; appended secrets remain other", () => {
for (const [message, reason] of [
["VRF production ticket refused: original dependency projection changed during preparation", "original-dependency-projection"],
["VRF production transition refused: dependency connected route identity", "connected-route-identity"],
["VRF production transition refused: missing ordered main VRF", "main-vrf-context"],
["VRF production transition refused: main VRF shape", "main-vrf-context"],
]) {
assert.equal(vrfPreparationDiagnostic(requestId, operationId, fixture(message))!.reason, reason)
assert.equal(vrfPreparationDiagnostic(requestId, operationId, fixture(message + " SECRET"))!.reason, "other")
}
})
test("actual native Error unknown refusal suffix maps only reviewed stage without exposing secrets", () => {
for (const [prefix, stage] of [["VRF production read refused: ", "read"], ["VRF production admission refused: ", "admission"], ["VRF production transition refused: ", "transition"], ["VRF production ticket refused: ", "ticket"]]) {
const error = new Error(prefix + "SECRET-PASSWORD 192.0.2.9 native-body")
Object.defineProperty(error, "name", { get() { throw Error("SECRET name getter invoked") } })
const result = vrfPreparationDiagnostic(requestId, operationId, error)!
assert.equal(result.stage, stage); assert.equal(result.reason, "other"); assert.equal(result.firstOwnedFrame, undefined)
for (const secret of ["SECRET-PASSWORD", "192.0.2.9", "native-body", prefix]) assert.ok(!JSON.stringify(result).includes(secret))
}
const tooLong = fixture("VRF production read refused: " + "S".repeat(512))
assert.equal(vrfPreparationDiagnostic(requestId, operationId, tooLong)!.stage, "other")
})
test("proxy, prototype and toStringTag spoofing reject without triggering traps", () => {
let traps = 0
const fail = () => { traps++; throw Error("SECRET getter called") }
const proxy = new Proxy(fixture(), { get: fail, getPrototypeOf: fail, getOwnPropertyDescriptor: fail, ownKeys: fail })
const spoof = Object.create(Error.prototype) as object
Object.defineProperties(spoof, { message: { get: fail }, stack: { get: fail }, [Symbol.toStringTag]: { get: fail } })
for (const error of [proxy, spoof, { name: "Error", message: "SECRET", stack }]) assert.deepEqual(vrfPreparationDiagnostic(requestId, operationId, error), { event: "mcp.vrf.preparation-failed", requestId, operationId, stage: "other", errorClass: "NonError", reason: "other" })
assert.equal(traps, 0)
})
test("actual Error own accessors and constructor/name getters are never invoked", () => {
for (const field of ["message", "stack"]) {
const error = fixture(undefined, stack), calls = { count: 0 }
Object.defineProperty(error, field, { get() { calls.count++; throw Error("SECRET") } })
Object.defineProperty(error, "name", { get() { calls.count++; throw Error("SECRET") } })
Object.defineProperty(error, "constructor", { get() { calls.count++; throw Error("SECRET") } })
const diagnostic = vrfPreparationDiagnostic(requestId, operationId, error)!
assert.equal(diagnostic.reason, field === "stack" ? "static-address-context" : "other"); assert.equal(diagnostic.stage, field === "stack" ? "transition" : "other"); assert.equal(diagnostic.firstOwnedFrame, undefined)
assert.equal(calls.count, 0)
}
})
test("built-in error classes use prototype identity and subclasses remain a fixed enum", () => {
for (const [error, expected] of [[new TypeError("SECRET"), "TypeError"], [new RangeError("SECRET"), "RangeError"], [new SyntaxError("SECRET"), "SyntaxError"], [new (class SecretError extends Error {})("SECRET"), "OtherNativeError"]] as const) assert.equal(vrfPreparationDiagnostic(requestId, operationId, error)!.errorClass, expected)
})
test("nested Linux/Windows/compiled paths return source enums with bounded line only", () => {
for (const path of ["/app/dist/services/vrf-production-read.js", "file:///app/dist/services/vrf-production-read.js", "file:///C:/workspace/backend/src/services/vrf-production-read.ts", "C:\\workspace\\backend\\src\\services\\vrf-production-read.ts"]) {
const diagnostic = vrfPreparationDiagnostic(requestId, operationId, fixture("unexpected", `Error: SECRET\n at external (/tmp/node_modules/native.js:99:2)\n at closure (${path}:203:12)`))!
assert.equal(diagnostic.stage, "read"); assert.deepEqual(diagnostic.firstOwnedFrame, { source: "vrf-production-read", line: 203 })
assert.ok(!JSON.stringify(diagnostic).includes(path))
}
})
test("non-owned paths, wrong directories and invalid line numbers never pass through", () => {
for (const path of ["/app/dist/services/private-secret.ts:1:1", "/app/dist/other/vrf-production-read.ts:1:1", "/app/dist/services/vrf-production-read.ts:0:1", "/app/dist/services/vrf-production-read.ts:1000001:1", "https://secret.example/src/services/vrf-production-read.ts:1:1"]) assert.equal(vrfPreparationDiagnostic(requestId, operationId, fixture("unexpected", `Error: SECRET\n at run (${path})`))!.firstOwnedFrame, undefined)
})
test("stack scan has fixed byte/line bounds and never inspects error causes", () => {
const error = fixture("SECRET", "Error: SECRET\n" + " at external (/tmp/external.js:1:1)\n".repeat(70) + " at internal (/app/dist/services/vrf-production-transition.js:80:1)")
Object.defineProperty(error, "cause", { get() { throw Error("SECRET cause accessed") } })
assert.equal(vrfPreparationDiagnostic(requestId, operationId, error)!.firstOwnedFrame, undefined)
error.stack = "Error: " + "S".repeat(17000) + "\n at internal (/app/dist/services/vrf-production-transition.js:80:1)"
assert.equal(vrfPreparationDiagnostic(requestId, operationId, error)!.firstOwnedFrame, undefined)
})
test("nonerrors have no copied fields; non-VRF/non-UUID calls have no diagnostic", () => {
for (const error of [null, undefined, "SECRET", 42, Symbol("SECRET"), () => { throw Error("SECRET") }]) assert.equal(vrfPreparationDiagnostic(requestId, operationId, error)!.errorClass, "NonError")
for (const operation of ["ros.vrf.list", "routing.vrf.update", "mcp.operation.apply", "ros.bridge.update", "ros.vrf.update SECRET"]) assert.equal(vrfPreparationDiagnostic(requestId, operation, fixture()), undefined)
assert.equal(vrfPreparationDiagnostic("SECRET-not-a-uuid", operationId, fixture()), undefined)
})
@@ -0,0 +1,86 @@
import { types } from "node:util"
const ownedSources = {
"vrf-production-read": { directory: "services", stage: "read" },
"vrf-production-admission": { directory: "services", stage: "admission" },
"vrf-production-transition": { directory: "services", stage: "transition" },
"vrf-production-ticket-store": { directory: "services", stage: "ticket" },
"vrf-production-action-binding": { directory: "services", stage: "admission" },
"vrf-members": { directory: "services", stage: "read" },
"vrf-ticket-runtime": { directory: "modules/mcp", stage: "ticket" },
"resources": { directory: "modules/mcp", stage: "read" },
"actions": { directory: "modules/mcp", stage: "other" },
"runtime": { directory: "modules/mcp", stage: "runtime" },
} as const
type Source = keyof typeof ownedSources
type Stage = typeof ownedSources[Source]["stage"]
type Reason = "original-dependency-projection" | "static-address-context" | "connected-route-identity" | "main-vrf-context" | "other"
type ErrorClass = "Error" | "TypeError" | "RangeError" | "SyntaxError" | "ReferenceError" | "URIError" | "EvalError" | "OtherNativeError" | "NonError"
type Operation = "ros.vrf.create" | "ros.vrf.update" | "ros.vrf.delete"
export interface VrfPreparationDiagnostic {
event: "mcp.vrf.preparation-failed"; requestId: string; operationId: Operation;
stage: Stage; errorClass: ErrorClass;
firstOwnedFrame?: { source: Source; line: number };
reason: Reason;
}
// Only complete, reviewed constant refusals. No user/native text is copied and
// appending any text (including secrets) intentionally maps back to "other".
const reasons = new Map<string, Reason>([
["VRF production ticket refused: original dependency projection changed during preparation", "original-dependency-projection"],
["VRF production transition refused: static address provenance/context", "static-address-context"],
["VRF production transition refused: dependency connected route identity", "connected-route-identity"],
["VRF production transition refused: missing ordered main VRF", "main-vrf-context"],
["VRF production transition refused: main VRF shape", "main-vrf-context"],
])
const refusalStages: ReadonlyArray<readonly [string, Stage]> = [
["VRF production read refused: ", "read"],
["VRF production admission refused: ", "admission"],
["VRF production transition refused: ", "transition"],
["VRF production ticket refused: ", "ticket"],
]
const classes: Array<[object, ErrorClass]> = [
[Error.prototype, "Error"], [TypeError.prototype, "TypeError"], [RangeError.prototype, "RangeError"], [SyntaxError.prototype, "SyntaxError"],
[ReferenceError.prototype, "ReferenceError"], [URIError.prototype, "URIError"], [EvalError.prototype, "EvalError"],
]
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
function dataString(error: object, key: "message" | "stack"): string | undefined {
const descriptor = Object.getOwnPropertyDescriptor(error, key)
return descriptor && Object.hasOwn(descriptor, "value") && typeof descriptor.value === "string" ? descriptor.value : undefined
}
function ownedFrame(stack: string | undefined): VrfPreparationDiagnostic["firstOwnedFrame"] {
if (!stack) return undefined
// Bound work before splitting, and ignore the first error-message line.
for (const line of stack.slice(0, 16384).split(/\r?\n/).slice(1, 65)) {
if (line.length > 2048 || !/^\s+at\s/.test(line)) continue
const match = /(?:^|[ (])(?:file:\/\/)?(?:[A-Za-z]:[\\/]|\/)[^\r\n]*?[\\/](?:src|dist)[\\/](services|modules[\\/]mcp)[\\/]([a-z-]+)\.(?:ts|js):([1-9]\d{0,6}):[1-9]\d{0,6}\)?$/.exec(line)
if (!match || !Object.hasOwn(ownedSources, match[2])) continue
const source = match[2] as Source, lineNumber = Number(match[3])
if (match[1].replaceAll("\\", "/") !== ownedSources[source].directory || lineNumber > 1000000) continue
return { source, line: lineNumber }
}
return undefined
}
/** Pure bounded diagnostic. Never inspect arbitrary error getters/proxies,
* retain original objects, or expose messages, paths, addresses or stack text. */
export function vrfPreparationDiagnostic(requestId: string, operationId: string, error: unknown): VrfPreparationDiagnostic | undefined {
if (typeof requestId !== "string" || !uuid.test(requestId) || !["ros.vrf.create", "ros.vrf.update", "ros.vrf.delete"].includes(operationId)) return undefined
const result: VrfPreparationDiagnostic = { event: "mcp.vrf.preparation-failed", requestId, operationId: operationId as Operation, stage: "other", errorClass: "NonError", reason: "other" }
// isNativeError is an internal-slot check, unlike instanceof/toString. It
// rejects Proxy(Error) and prototype/toStringTag imitations without traps.
if (!types.isNativeError(error)) return result
result.errorClass = classes.find(([prototype]) => Object.getPrototypeOf(error) === prototype)?.[1] ?? "OtherNativeError"
const message = dataString(error, "message"), stack = dataString(error, "stack")
if (message !== undefined && message.length <= 512) {
result.reason = reasons.get(message) ?? "other"
result.stage = refusalStages.find(([prefix]) => message.startsWith(prefix))?.[1] ?? "other"
}
// Node 22 uses a native lazy stack accessor. We deliberately do not invoke
// it (nor Error.prepareStackTrace); exact own-message reasons still work.
if (message === undefined || stack === undefined) return result
const frame = ownedFrame(stack)
if (frame) { result.firstOwnedFrame = frame; result.stage = ownedSources[frame.source].stage }
return result
}
@@ -49,7 +49,7 @@ export async function prepareRuntimeVrfTicket(input: VrfRuntimeTicketInput, depe
const actionId = vrfNativeActionFor(input.operationId, request) const actionId = vrfNativeActionFor(input.operationId, request)
const context: PrepareVrfProductionTicketInput["context"] = { binding: { planId: input.planId, actorId: input.ownerId, serverId: native.serverId, sourceProof }, connectionId: input.connectionId, actionId, const context: PrepareVrfProductionTicketInput["context"] = { binding: { planId: input.planId, actorId: input.ownerId, serverId: native.serverId, sourceProof }, connectionId: input.connectionId, actionId,
actionVersion: input.operationId === "routing.vrf.update" ? Number(findMcpOperation(actionId)?.version) : input.operationVersion, actionVersion: input.operationId === "routing.vrf.update" ? Number(findMcpOperation(actionId)?.version) : input.operationVersion,
...(input.operationId === "routing.vrf.update" ? { authorityAction: { id: "routing.vrf.update" as const, version: input.operationVersion as 3 } } : {}) } ...(input.operationId === "routing.vrf.update" ? { authorityAction: { id: "routing.vrf.update" as const, version: input.operationVersion as 4 } } : {}) }
assertVrfActionContext(context, request); assertVrfAliasEvidence(context, request, evidence) assertVrfActionContext(context, request); assertVrfAliasEvidence(context, request, evidence)
const authority = async () => { const authority = async () => {
await input.assertAuthority() await input.assertAuthority()
+20 -3
View File
@@ -1,10 +1,13 @@
import { randomUUID } from "node:crypto" import { randomUUID } from "node:crypto"
import { retainServerProbeStorage } from "../../services/server-probe-retention.js" import { retainServerProbeStorage } from "../../services/server-probe-retention.js"
import { maintainServerProbeOperationRetirement } from "../../services/server-probe-operation-retirement.js"
import { collectExpiredServerPollStorage } from "../../services/server-poll-storage-retention.js" import { collectExpiredServerPollStorage } from "../../services/server-poll-storage-retention.js"
import { compactCompletedServerPollStorage } from "../../services/server-poll-terminal-compaction.js" import { compactCompletedServerPollStorage } from "../../services/server-poll-terminal-compaction.js"
import { retireCompletedServerPollOperations } from "../../services/server-poll-operation-retirement.js"
import { pool } from "../../db/index.js" import { pool } from "../../db/index.js"
import { sharedDatabaseGate } from "../../services/shared-database-gate.js" import { sharedDatabaseGate } from "../../services/shared-database-gate.js"
import { recoverMcpOperations, verifyStep } from "./runtime.js" import { recoverMcpOperations, verifyStep } from "./runtime.js"
import { retireOrdinaryMcpOperations } from "./ordinary-operation-retirement.js"
import { audit, getMcpSettings } from "./store.js" import { audit, getMcpSettings } from "./store.js"
import { accessGeneration, decryptSecret, redact } from "./security.js" import { accessGeneration, decryptSecret, redact } from "./security.js"
import { tickRoutingProfiles } from "./routing-profiles.js" import { tickRoutingProfiles } from "./routing-profiles.js"
@@ -48,11 +51,22 @@ export async function detectMcpDrift() {
} }
} }
const mcpRetentionBatchSize = 1000
async function retainMcpData() { async function retainMcpData() {
const settings = await getMcpSettings() const settings = await getMcpSettings()
await pool.query("DELETE FROM mcp_requests WHERE created_at<now()-make_interval(days=>$1) AND status NOT IN ('running')", [settings.requestsRetentionDays]) await pool.query(`WITH expired AS (
await pool.query("DELETE FROM mcp_audit WHERE created_at<now()-make_interval(days=>$1)", [settings.auditRetentionDays]) SELECT id FROM mcp_requests WHERE created_at<now()-make_interval(days=>$1) AND status NOT IN ('running')
await pool.query("DELETE FROM mcp_artifacts WHERE expires_at<now()") 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 (
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_oauth_requests WHERE expires_at<now()-interval '1 day'")
await pool.query("DELETE FROM mcp_budget_buckets WHERE bucket<now()-interval '2 days'") await pool.query("DELETE FROM mcp_budget_buckets WHERE bucket<now()-interval '2 days'")
await routingReservationTransaction(db => retainManualInverseChargesWithinOwner(db, settings.requestsRetentionDays)) await routingReservationTransaction(db => retainManualInverseChargesWithinOwner(db, settings.requestsRetentionDays))
@@ -79,8 +93,11 @@ export async function tickMcpWorkers() {
await retainMcpData() await retainMcpData()
await retainServerProbeStorage() await retainServerProbeStorage()
await compactCompletedServerPollStorage() await compactCompletedServerPollStorage()
await retireCompletedServerPollOperations()
await collectExpiredServerPollStorage() await collectExpiredServerPollStorage()
await retireOrdinaryMcpOperations()
if ((await getMcpSettings()).enabled) { await detectMcpDrift(); await tickRoutingProfiles() } if ((await getMcpSettings()).enabled) { await detectMcpDrift(); await tickRoutingProfiles() }
await maintainServerProbeOperationRetirement()
} finally { } finally {
if (locked) try { await lease.query("SELECT pg_advisory_unlock(hashtextextended('mcp-workers',0))") } catch { unsafe = true } if (locked) try { await lease.query("SELECT pg_advisory_unlock(hashtextextended('mcp-workers',0))") } catch { unsafe = true }
lease.release(unsafe) lease.release(unsafe)
@@ -0,0 +1,26 @@
import type { UserBindingCreate } from "@mmapp/contracts/users"
import { getServerRowById } from "../servers/repository/servers-repository.js"
import { MikrotikClient } from "../../services/mikrotik.js"
import { sameWireguardEndpoint } from "../../services/wireguard-source.js"
import { ipsecClientBindingKeys, type IpsecServerState } from "../../services/ipsec-live.js"
import { bindingNativeRows, readBindingTarget, UserBindingTargetError } from "./binding-target.js"
export function readNativeUserBindingTarget(input: UserBindingCreate, options: { signal?: AbortSignal; assertCurrent?: () => Promise<unknown> } = {}) {
return readBindingTarget(input, async () => {
const server = await getServerRowById(input.serverId)
if (!server?.enabled) throw new UserBindingTargetError("Сервер отключён или не найден", 404)
const client = MikrotikClient.fromServer(server)
return {
async assertCurrent() {
const fresh = await getServerRowById(input.serverId)
if (!fresh || !sameWireguardEndpoint(fresh, server)) throw new UserBindingTargetError("Источник привязки изменился", 409)
},
get: (path, timeout, signal, maximum) => client.get(path, timeout, signal, maximum),
ipsecKeys: (certs, peers, identities) => ipsecClientBindingKeys({ server,
certs: bindingNativeRows(certs, [".id", "name"]) as IpsecServerState["certs"],
peers: bindingNativeRows(peers, [".id"]) as IpsecServerState["peers"],
identities: bindingNativeRows(identities, [".id"]) as IpsecServerState["identities"],
}),
}
}, options)
}
Loaded 100 of 251 files, more files were not shown because too many files have changed in this diff. Show more