fix(mcp): обновить проверенный снимок маршрутизации и хранения операций

This commit is contained in:
Denozordec committed 2026-11-04 17:14:57 +07:00
1 parent f1af1a85e5
commit 5ce56b5168
117 files changed
+9960 -182

No files matched your search

@@ -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;
+3 -2
View File
@@ -66,7 +66,7 @@
"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: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-approval:pg": "node scripts/test-server-poll-approval-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-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-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-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: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",
@@ -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 })
}
}
+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')
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')
const mcpOperationOrigin = process.argv.includes('--mcp-operation-origin')
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 pollOperationRetirement = process.argv.includes('--poll-operation-retirement')
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')
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')
@@ -243,13 +246,13 @@ try {
if (regressionOnly) console.log('Selected regression only: primary integration and terminal restore are not run')
if (profilesOnly) console.log('Profiles-only regression matrix: primary integration NOT RUN; terminal restore NOT RUN; full acceptance is not claimed')
const result = regressionOnly || profilesOnly ? 0 : await new Promise((resolve, reject) => {
const child = spawn(process.execPath, pollMemory ? ['--max-old-space-size=192', '--max-semi-space-size=16', fileURLToPath(new URL('../dist-mcp-memory/services/server-poll-memory.owned.test.js', import.meta.url))] : ['--import', 'tsx', principalSecuritySource ? 'src/modules/mcp/principal-security-source.owned.test.ts' : pollJournalSnapshot ? 'src/services/server-poll-journal-snapshot.owned.test.ts' : pollTerminalArchive ? 'src/services/server-poll-terminal-archive.owned.test.ts' : pollArchiveSchema ? 'src/db/server-poll-terminal-archives.sql.test.ts' : telemetrySettingsConfiguration ? 'src/modules/mcp/telemetry-settings-configuration.owned.test.ts' : routingReservationSchema ? 'src/db/routing-reservation-schema.owned.test.ts' : routingForwardRounds ? 'src/modules/mcp/routing-network-forward-rounds.owned.test.ts' : flowActiveServices ? 'src/services/traffic-flow-active-services.owned.test.ts' : flowActiveRebuild ? 'src/services/traffic-flow-active-rebuild.owned.test.ts' : flowQuality ? 'src/services/traffic-flow-quality.owned.test.ts' : flowWorkerIpc ? 'src/services/traffic-flow-worker-ipc.owned.test.ts' : manualUptime ? 'src/modules/mcp/manual-uptime-http.test.ts' : manualPingGroup ? 'src/modules/mcp/manual-ping-group-http.test.ts' : manualTraffic ? 'src/modules/mcp/manual-traffic-http.test.ts' : manualApiPing ? 'src/modules/mcp/manual-api-ping-http.test.ts' : simpleSpeedChr ? 'src/modules/mcp/simple-speed-chr.test.ts' : simpleSpeed ? 'src/modules/mcp/simple-speed-http.test.ts' : externalPeerMetadata ? 'src/services/bandwidth-external-peer-metadata.sql.test.ts' : operatorPeerMetadata ? 'src/services/bandwidth-operator-metadata.sql.test.ts' : publicRegistryMetadata ? 'src/services/bandwidth-public-registry.sql.test.ts' : operatorDockerSql ? 'src/services/bandwidth-operator-docker-stop.sql.test.ts' : 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),
...(routingOriginalEffects ? { MMAPP_OWN_ROUTING_ORIGINAL_EFFECTS: 'true' } : {}),
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',
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))
})
@@ -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 && 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 && 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 && 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')
+5 -5
View File
@@ -12,10 +12,10 @@ const reviewed = (source: string, extra: Partial<MigrationReview> = {}) => {
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 })
}
await test("all44 exact file pins and every top-level schema operation are inventoried", () => {
assert.equal(inventory.length, 44); assert.equal(MIGRATION_MANIFEST.length, 44)
await test("all 53 exact file pins and every top-level schema operation are inventoried", () => {
assert.equal(inventory.length, 53); assert.equal(MIGRATION_MANIFEST.length, 53)
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, 36)
assert(inventory.every(file => file.operations.length === file.statementCount && file.effects.length > 0))
let tables = new Set<string>()
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.deepEqual(endpoint.opaqueBodies.map(body => body.kind), ["stored-function-body", "executed-do-body"])
})
await test("0035 through0044 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"])
await test("0035 through 0053 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", "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"])
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)
})
+127 -1
View File
@@ -702,6 +702,132 @@ 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.",
"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."
]
}
]
@@ -724,7 +850,7 @@ export const migrationManifestDigest = () => hash(JSON.stringify(MIGRATION_MANIF
// resetting the accumulated public table set at the reviewed destructive0002.
// This is a necessary presence check, not a substitute for exact live column,
// 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 {
inventoryComplete: boolean; databaseGeneration: string; unresolvedAttemptIds: readonly string[]
+7 -1
View File
@@ -14,6 +14,7 @@ import { logFiltersSchema, logSettingsPatchSchema } from "@mmapp/contracts"
import { appUserCreateSchema, appUserUpdateSchema, appUserRoleSchema, sectionPermSchema, serverPermSchema, userBindingCreateSchema } from "@mmapp/contracts/users"
import type { McpOperationDescriptor } from "@mmapp/contracts/mcp"
import { hasPermission } from "../../lib/permissions.js"
import { assertOperationReplayScope, findOperationReplayTombstone, operationHistoryExpired } from "./operation-replay-tombstones.js"
import { pool } from "../../db/index.js"
import { getServerReadById } from "../servers/service/servers-service.js"
import { listUsers, getUserById, listInterfaceCatalog, getInterfaceCatalogState } from "../users/service/users-service.js"
@@ -473,7 +474,12 @@ add("changes.cancel", "mm_change_cancel", "Отменить свой незап
add("operations.read", "mm_operation_get", "Результат своей операции", "mcp", null, "read", "connection", z.object({ operationId: z.string().uuid() }).strict(), {
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])
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)
const { rows: steps } = await pool.query("SELECT * FROM mcp_operation_steps WHERE operation_id=$1 ORDER BY step_index", [input.operationId])
return { ...operation, steps }
@@ -115,3 +115,9 @@ export const createDiagnosticCancellationSourceForTests = sourceLoader
/** Historical authentication only; confers no execution/cancellation grant. */
export const readServerProbeExecutionSource = readSourceLoader(productionCrypto)
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 { pool } from "../../db/index.js"
import { readRetiredServerPollHistory } from "../../services/server-poll-history-reader.js"
import { hasPermission } from "../../lib/permissions.js"
import { effectiveMcpConnection } from "./store.js"
import { decryptSecret, fingerprint, McpError } from "./security.js"
@@ -7,40 +8,84 @@ import { findMcpOperation } from "./registry.js"
import { findMcpService } from "./service-registry.js"
import type { add } from "./actions.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()
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)
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.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]
if (!row) throw new McpError("NOT_FOUND", "Операция не найдена", 404)
const original = findMcpOperation(row.operation_id)
return current
}
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)
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)
}
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)
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(), {
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) => {
const { row, evidence } = await authorizedSource(String(input.operationId), context)
const authorize = async () => { await authorizedSource(String(input.operationId), context) }
const operationId = String(input.operationId)
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 = []
for (const [index, step] of evidence.steps.entries()) {
if (step.kind !== "service") throw new McpError("UNSUPPORTED_CAPABILITY", "Нативная глобальная сверка недоступна", 409)
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()
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
@@ -5,6 +5,8 @@ await prepareOwnedBandwidthState()
const { pool, closePool } = await import("../../db/index.js")
try {
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")
console.log("Owned actual MCP operation root:", JSON.stringify(await acceptOwnedMcpOperationRoot()))
} 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: "[email protected]", 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()
}
@@ -0,0 +1,148 @@
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 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:"[email protected]",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"],
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","operations.read","changes.apply"],allowGlobal:true},
limits:{callsPerMinute:100,mutationsPerHour:10}}),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(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))
}
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))
console.log("Owned settings operations: five exact families, authenticated apply, physical 180-day purge, preserved settings and scoped replay")
}finally{resumeSchedulerJobs();await closePool()}
@@ -0,0 +1,199 @@
import { randomUUID } from "node:crypto"
import { logSettingsPatchSchema, logSettingsSchema } from "@mmapp/contracts"
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)
// 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),
} 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)
) 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])=>effect[key]===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))
}
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}
})
}
@@ -1,5 +1,8 @@
import assert from "node:assert/strict"
import { readFile } from "node:fs/promises"
import { test } from "node:test"
import { runInNewContext } from "node:vm"
import ts from "typescript"
import type { ActionContext } from "./action-types.js"
import { loadReservedScopedRecoverySource } from "./routing-community-scoped-recovery-source.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/)
})
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 () => {
const expected = {} as Parameters<typeof validateReservedInverseVerified>[1]
for (const row of [{}, { state: "verified", phase: "RECOVERY_VERIFIED", configurationVerified: true }, { inverse_intent_sealed: "x".repeat(524289) }]) {
@@ -35,8 +35,9 @@ import { registerManagementChannel } from "./management.js"
import { isSchedulerJobsPaused, pauseSchedulerJobsAndWait, resumeSchedulerJobs } from "../../services/scheduler-running.js"
/** 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" } = {}) {
const sid = 131, writer: AuthUser = { ...actor, permissions: [...new Set([...actor.permissions, "mm:filters:write"])] }
export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options: { originalEffectsOnly?: boolean; originalCredentialRevocationOnly?: boolean; forwardRoundsOnly?: boolean; manualInverseChargeOnly?: boolean; manualInverseChargeCase?: "positive" | "quota" | "concurrent" | "unknown" | "revoked" | "nonpersistent" | "mixed-forward"; lateInverseAuthorityChange?: "edit" | "disable" | "grant" } = {}) {
const sid = options.lateInverseAuthorityChange ? { edit: 131, disable: 132, grant: 133 }[options.lateInverseAuthorityChange] : 131
const writer: AuthUser = { ...actor, permissions: [...new Set([...actor.permissions, "mm:filters:write"])] }
const initial = compileManagedFilterRules([{ community: "65000:10", gateway: "192.0.2.1", outIface: "", action: "route", description: "" }, { community: "65000:20", gateway: "192.0.2.5", outIface: "", action: "route", description: "" }])
const filters = ["a", "b"].map(key => ({ ".id": "*f" + key, chain: "custom-" + key, rule: initial, disabled: "false", comment: managedComment("NEVER_SCOPED_COMMENT_" + key), opaque: "NEVER_SCOPED_OPAQUE" }))
let mode: "normal" | "ignored" | "lost" | "collateral" | "post-failed" | "post-unknown" = "normal", writes = 0, requests = 0
@@ -423,8 +424,9 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
await assertOriginalCredentialRevocation()
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"
if (options.lateInverseAuthorityChange) assert.equal(chargeCase, "positive")
const persistent = chargeCase !== "nonpersistent"
// Same genuine fixed-reader baseline/CAS used by the existing persistent
// acceptance below. No hand-written baseline, sealed plan or authority.
@@ -573,6 +575,48 @@ export async function acceptScopedRouteCommunitySwitch(actor: AuthUser, options:
realSql: true, allWriterLedgerComplete: false, reservedDispatchEnabled: false, autoRecoveryEnabled: false }
}
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 === "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
@@ -257,24 +257,30 @@ async function checkpoint(value: Session, intent: Intent, phase: Checkpoint["pha
export async function executeReservedScopedRecovery(allocation: RoutingReservedInverseAllocation, expected: { sourceOperationId: string; principal: McpPrincipal }) {
const store = createRoutingReservationStore(), spent = await store.beginInverseAttempt(allocation)
const execution = spent.execution
const binding = await store.inspectInverseExecution(execution)
if (binding.sourceOperationId !== expected.sourceOperationId || fingerprint(binding.principal) !== fingerprint(expected.principal)) throw fail("Reserved runner identity differs")
const current = await effectiveMcpConnection(binding.principal)
const context: ActionContext = { principal: binding.principal, connection: current }
const source = await loadReservedScopedRecoverySource(execution, context)
const channel: ManagementTransportBinding | undefined = source.input.managementBindingCiphertext == null ? undefined
: JSON.parse(await decryptSecret(String(source.input.managementBindingCiphertext)))
const input: Record<string, unknown> = { mode: "reserved", serverId: source.profile.serverId, sourceOperationId: binding.sourceOperationId,
sourceProof: source.sourceProof, configurationProof: binding.configurationProof, sourceReceiptId: binding.sourceReceiptId,
profile: source.profile, candidateId: source.configuration.packet.originalCandidateId, rules: source.input.beforeRules, beforeRules: source.input.rules,
principal: binding.principal, ownerId: current.owner_id, ownerProof: fingerprint(current.owner_permissions), policyProof: fingerprint(current.policy), connectionGeneration: current.generation,
generation: await accessGeneration(), receiptId: binding.inverseReceiptId, proofRevision: 1,
validUntil: new Date(Math.min(Date.parse(binding.deadline), deadline(source, context, channel?.expiresAt))).toISOString(), managementBindingCiphertext: source.input.managementBindingCiphertext ?? null }
input.requestProof = await sealedProof(input, "request")
reservedExecutions.set(input, execution)
try { return await executeScopedRecovery(input) }
catch (error) { await store.markUnknown(binding.reservationId, "inverse").catch(() => {}); throw error }
finally { reservedExecutions.delete(input) }
try {
const binding = await store.inspectInverseExecution(execution)
if (binding.sourceOperationId !== expected.sourceOperationId || fingerprint(binding.principal) !== fingerprint(expected.principal)) throw fail("Reserved runner identity differs")
const current = await effectiveMcpConnection(binding.principal)
const context: ActionContext = { principal: binding.principal, connection: current }
const source = await loadReservedScopedRecoverySource(execution, context)
const channel: ManagementTransportBinding | undefined = source.input.managementBindingCiphertext == null ? undefined
: JSON.parse(await decryptSecret(String(source.input.managementBindingCiphertext)))
const input: Record<string, unknown> = { mode: "reserved", serverId: source.profile.serverId, sourceOperationId: binding.sourceOperationId,
sourceProof: source.sourceProof, configurationProof: binding.configurationProof, sourceReceiptId: binding.sourceReceiptId,
profile: source.profile, candidateId: source.configuration.packet.originalCandidateId, rules: source.input.beforeRules, beforeRules: source.input.rules,
principal: binding.principal, ownerId: current.owner_id, ownerProof: fingerprint(current.owner_permissions), policyProof: fingerprint(current.policy), connectionGeneration: current.generation,
generation: await accessGeneration(), receiptId: binding.inverseReceiptId, proofRevision: 1,
validUntil: new Date(Math.min(Date.parse(binding.deadline), deadline(source, context, channel?.expiresAt))).toISOString(), managementBindingCiphertext: source.input.managementBindingCiphertext ?? null }
input.requestProof = await sealedProof(input, "request")
reservedExecutions.set(input, execution)
try { return await executeScopedRecovery(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
@@ -14,7 +14,9 @@ import { nativeBgpRows, bgpTemplateReferences } from "../../services/bgp-export-
import { bgpFilterName, effectiveBgpFilter, stableBgpPolicyRow } from "../../services/bgp-filter-binding-model.js"
import { canonicalVrfMembers } from "../../services/vrf-members.js"
import { readOspfEffectiveDisabled } from "../../services/ospf-effective-disabled.js"
import { isConfirmedRoutingFibTable } from "../../services/routing-table-fib.js"
import * as contextModel from "./routing-community-context-model.js"
import { prepareOriginalEffectSelection } from "./routing-original-effects.js"
import { readMeasurementAddress } from "./routing-measurement-address.js"
import { routeContextClients } from "./routing-community-context-native.js"
import { pathProofTag } from "./path-proof-purpose.js"
@@ -65,9 +67,8 @@ async function fixture(autoRecovery = false) {
afterHttp?.(); afterHttp = undefined
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 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 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) }
@@ -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-measurement-address.js": { readMeasurementAddress } }
await loadSource("./routing-community-context.ts", { ...common, "./routing-network-native-session.js": native,
"../../services/routing-table-fib.js": { isConfirmedRoutingFibTable }, "./routing-original-effects.js": { prepareOriginalEffectSelection },
"../../services/bgp-export-model.js": { nativeBgpRows, bgpTemplateReferences }, "../../services/bgp-filter-binding-model.js": { bgpFilterName, effectiveBgpFilter, stableBgpPolicyRow },
"./routing-community-context-model.js": contextModel, "./path-proof-purpose.js": { pathProofTag },
"./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)
const create = native.createRoutingNetworkNativeSession as (options: unknown) => Promise<Session>
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>
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>,
bypassTail: context.readRouteCommunityContextWithSharedNativeSession as (token: Token) => Promise<unknown>,
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 } },
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 () => {
@@ -19,7 +19,7 @@ export async function acceptRoutingRecoveryAuthoritySql() {
const { pool } = await import("../../db/index.js")
const { mcpConnectionPolicySchema } = await import("@mmapp/contracts/mcp")
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 { resolveRoutingProfileAuthority } = await import("./routing-profile-authority.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")
const previousEnabled = (await pool.query("SELECT enabled FROM mcp_settings WHERE id=1")).rows[0].enabled as boolean
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("{}") })
try {
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"
await assert.rejects(withVerifiedConfigurationChange({ actorId: ownerId, connectionId, approved: true, serverIds: ids, serverWide: true, objects: [],
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)
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()
dispatchFences++
},
}, () => executeGuardedConfigurationWrite(ids[0], "PATCH", path, async () => {
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) })
req.once("error", reject); req.once("timeout", () => req.destroy(new Error("Synthetic loopback timeout"))); req.end("{}")
})
})), /Постоянный допуск/)
assert.equal(callbackDispatches, 0); assert.equal(nativeHttpRequests, 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)
})), (error: unknown) => error instanceof McpError && error.code === "STATE_CONFLICT" && /Активный routing profile/.test(error.message))
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 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,
productionSessionAuthorityFence: true, realSql: true, productionHmac: true, fullInverseLifecycleRepeated: false, controllerEnabled: false, externalNetwork: false }
return { cases: 3, deepCaptureCases: 3, directGuardBeforeIntent: true, dispatchFences, callbackDispatches, nativeHttpRequests,
capturedAuthorityChanges: ["edit", "disable", "grant"], realSql: true, productionHmac: true, controllerEnabled: false, externalNetwork: false }
} finally {
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])
@@ -2,9 +2,26 @@ import { assertOwnedRoutingRecoveryAuthoritySql, acceptRoutingRecoveryAuthorityS
// Guard precedes imports that configure a pool or load private MCP state.
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")
try {
const { applySqlMigrations } = await import("../../db/migrate.js")
await applySqlMigrations(pool)
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: "[email protected]", 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()
}
@@ -352,6 +352,47 @@ export async function acceptRoutingReservationSql(options: { runtimeAdmission?:
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")
}
{
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 saved = (await pool.query("SELECT state_proof FROM mcp_routing_reservations WHERE id=$1", [reservation.reservationId])).rows[0].state_proof
+19 -4
View File
@@ -1,4 +1,6 @@
import { pollErrorLog } from "../../services/server-poll-error-provenance.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 { acknowledgedCreateId, assertNoCallerCreateId } from "./native-create-identity.js"
import { randomUUID, createHash } from "node:crypto"
@@ -23,6 +25,7 @@ import { beginMcpOperationAdmission, insertMcpOperationAdmission, retireMcpOpera
import { createOspfOwnerSourceGuard, verifyOspfOwnerGraph, verifyOspfOwnerGraphAtPhase } from "./ospf-owner-graph.js"
import { evaluateMcpPolicy } from "./policy.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 { 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"
@@ -291,6 +294,7 @@ export async function applyPlan(principal: McpPrincipal, planId: string, idempot
reservedRouting = !!await routingReservationEvidence(original, originalEvidence)
return existing
}
await refuseExpiredOperationReplay(client, principal, connection.id, planId, idempotencyKey)
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')")
if (pending.rows[0].count >= 100) throw new McpError("BUDGET_EXCEEDED", "Очередь исполнителя заполнена; повторите подготовленное применение позже", 429)
@@ -359,6 +363,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> {
return withMcpOperationExecutionOrigin(operationAdmission, async () => {
const lease = await pool.connect()
let terminalProbe: Awaited<ReturnType<typeof executeOperationWithLease>>
let locked = false, released = false
const ownerLifetime = new AbortController()
// This closure is the SINGLE owner of physical destruction/release.
@@ -380,7 +385,7 @@ async function executeOperation(operationId: string, principal: McpPrincipal, ro
throw error
}
if (released || !locked) return
await withOperationConfigurationLockClient(lease, retire, ownerLifetime.signal,
terminalProbe = await withOperationConfigurationLockClient(lease, retire, ownerLifetime.signal,
() => executeOperationWithLease(operationId, principal, routingReservationForward, routingReservationId, originalServiceGrant))
} finally {
if (!released && locked) {
@@ -395,9 +400,14 @@ async function executeOperation(operationId: string, principal: McpPrincipal, ro
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])
if (!operation) return
const plan = await loadPlan(operation.plan_id), evidence = JSON.parse(await decryptSecret(plan.evidence.sealed)) as ChangeEvidence
@@ -576,14 +586,18 @@ async function executeOperationWithLease(operationId: string, principal: McpPrin
})
await (evidence.steps.some(step => step.kind === "service") ? withApplicationMutationLock(applyWithLocks) : applyWithLocks())
assertOperationConfigurationLockCheckpoint()
await transaction(async client => {
const receiptId = plan.operation_id === SERVER_PROBE_OPERATION ? diagnosticReceipt?.receiptId : undefined
const finishedAt = await transaction(async client => {
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()
await client.query("UPDATE mcp_plans SET state='completed' WHERE id=$1", [plan.id])
assertOperationConfigurationLockCheckpoint()
await audit(client, "mcp-runner", "operation.complete", operation.connection_id, { operationId, confirmedWrites: diagnosticOnly ? 0 : confirmed, ...(diagnosticOnly ? { completedServiceSteps: confirmed, diagnosticReceipt } : {}) })
assertOperationConfigurationLockCheckpoint()
return receiptId ? terminal.rows[0]?.finished_at : undefined
})
assertOperationConfigurationLockCheckpoint()
if (routingReservationId && reservedForwardPassed) {
@@ -592,6 +606,7 @@ async function executeOperationWithLease(operationId: string, principal: McpPrin
await createRoutingReservationStore().proveUnusedInverse(routingReservationId).catch(() => {})
}
assertOperationConfigurationLockCheckpoint()
if (receiptId && finishedAt) return { receiptId, operationId, connectionId:operation.connection_id, finishedAt }
} catch (error) {
if (routingReservationId && hasConfirmedReservedRoutingForward(error)) confirmed = Math.max(confirmed, 1)
if (error instanceof VrfProductionAcknowledgedFailure) confirmed = Math.max(confirmed, error.acknowledgedWrites)
@@ -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 context = { principal, connection: { id: connectionId, owner_id: "owner", owner_claims_issued_at: new Date(now - 1000) } } as ActionContext
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>()
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 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") }
@@ -28,6 +28,7 @@ function fixture() {
read: async id => { events.push("journal.read"); return receipts.get(id) },
}
const deps: ServerProbeMcpDependencies = {
retiredSource,
authority: (() => ({ readAuthority, assertProtections })) as ServerProbeMcpDependencies["authority"],
historicalAuthority: ((currentPrincipal: typeof principal) => ({ readAuthority: async () => { events.push("read-authority"); return { ...await readAuthority(), connectionId: currentPrincipal.connectionId, credentialId: currentPrincipal.credentialId } }, assertProtections })) as ServerProbeMcpDependencies["historicalAuthority"],
history: dependencies => createServerProbeJournalHistoryReader({ ...dependencies, loadArchive: async binding => {
@@ -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) } } },
query: async (sql, values) => {
events.push("sql"); if (revokeOnQuery) revoked = true
if (sql.includes("probe-history:scope")) { assert.equal(values[2], connectionId); assert.equal(values[3], "owner"); return [{ 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("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")
@@ -68,8 +69,9 @@ function fixture() {
}, seal, open, proof, now: () => clock,
}
const adapter = createServerProbeMcpAdapter(deps)
const retireHistory = () => { liveHistory = false }
const prepare = async (tool: PreparedServerProbe["tool"] = "ping") => { evidence = await adapter.prepare({ serverId: 9, request: tool === "bandwidth" ? { tool, bwRemoteAddress: "peer", dstServerId: 8 } : { tool, target: "192.0.2.1" } }, context); const step = evidence.steps[0]; assert.equal(step.kind, "service"); return step.kind === "service" ? step.input : assert.fail() }
return { adapter, events, receipts, contexts, prepare, 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) {
@@ -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
}
assert.ok(!("diagnosticProgress" in serverProbeSafeMcpError(new ServerProbeExecutionError("X", "secret", 500, "unknown", 99), { receiptId, operationId, stageBudget: 1 }))); assertions++
{
const hash = "3".repeat(64), date = (value: number) => new Date(value).toISOString().replace("Z", "000Z")
const retired = { source: { version: 1 as const, operation: "servers.probes.run" as const, operationVersion: 1 as const, tool: "ping" as const,
receiptId, executionId: operationId, connectionId, ownerId: "owner", planId: randomUUID(), originalCredentialId: credentialId,
sourceServerId: 9, serverIds: [9], issuedAt: now - 1000, authorityExpiresAt: date(now + 600000), terminalAt: date(now + 600000),
activityAt: date(now + 600000), gcNotBefore: date(now + 87060000), retentionDays: 1, safetyMs: 60000,
bindingProof: hash, sourceDigest: hash, terminalDigest: hash, stageSnapshotDigest: hash, receiptProof: hash, ciphertextSha256: hash,
}, payloadPurged: false }
let lookups = 0, changeOnRecheck = false
const f = fixture(async lookup => {
lookups++
assert.deepEqual(lookup, { receiptId, operationId, connectionId, ownerId: "owner", scopeServerIds: [2, 9] })
return changeOnRecheck && lookups % 2 === 0 ? undefined : retired
})
const input = await f.prepare(), output = await f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0 })
f.setResult(output); f.finishOperation()
const request = { operationId, receiptId, page: 0 }
const live = await f.adapter.readPage(request, context)
assert.equal(lookups, 0)
f.retireHistory()
const rotated = { ...context, principal: { ...principal, credentialId: randomUUID() } }
assert.deepEqual(await f.adapter.readPage(request, rotated), live)
assert.deepEqual(await f.adapter.receiptTarget(request, rotated), { scope: "servers", serverIds: [9] })
const beforeForeign = lookups
await assert.rejects(f.adapter.readPage(request, { ...rotated, connection: { ...context.connection, owner_id: "foreign" } }))
assert.equal(lookups, beforeForeign)
lookups = 0; changeOnRecheck = true
const privateReads = f.events.filter(event => event === "history.read").length
await assert.rejects(f.adapter.readPage(request, rotated))
assert.equal(f.events.filter(event => event === "history.read").length, privateReads)
assertions += 7
}
{
const f = fixture(), input = await f.prepare("bandwidth"), output = await f.adapter.service.execute(input, { operationId, phase: "forward", stepIndex: 0 }); f.setResult(output); f.finishOperation(); f.expirePrepared()
await assert.rejects(f.adapter.service.current(input))
@@ -10,6 +10,7 @@ import { serverProbeReceiptPage } from "../../services/server-probe-public-resul
import { decryptSecret, encryptSecret, fingerprint, privateSecretFingerprint, McpError, redact } from "./security.js"
import type { ActionContext, ChangeEvidence } from "./action-types.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. */
export const serverProbeActionSchema = z.object({ serverId: z.number().int().positive().safe(), request: serverProbeExecutionRequest }).strict()
@@ -32,6 +33,7 @@ export interface ServerProbeMcpDependencies {
executor: typeof createProductionServerProbeExecutor
journal: (context: ServerProbeJournalContext, dependencies: ServerProbeJournalDependencies, execution?: { signal?: AbortSignal; deadlineAt?: number }) => ServerProbeJournal
query: Query
retiredSource?: (lookup: ServerProbeRetiredLookup) => ReturnType<typeof findRetiredServerProbeHistory>
seal: (value: string) => Promise<string>
open: (value: string) => Promise<string>
proof: (value: unknown) => Promise<string>
@@ -52,7 +54,7 @@ export async function openServerProbeMcpBinding(raw: Record<string, unknown>, cr
return { input, binding }
}
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)
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 }
@@ -119,7 +121,7 @@ export function createServerProbeMcpAdapter(overrides: Partial<ServerProbeMcpDep
await owned(input, binding, row.operation_id)
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()
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 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 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)
return metadata(receipt, context.operationId, binding.stageBudget)
return serverProbeReceiptMetadata(receipt, context.operationId, binding.stageBudget)
} catch (error) { throw serverProbeSafeMcpError(error, progress) }
},
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`,
[request.receiptId, request.operationId, context.principal.connectionId, initial.ownerId, SERVER_PROBE_OPERATION, initial.scopeServerIds])
await fresh()
if (rows.length !== 1) return denied()
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 loadRetired = deps.retiredSource
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() }
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 request = serverProbeReceiptReadSchema.parse(raw), { serverIds, fence } = await historicalAccess(request, context)
await fence(); return { scope: "servers", serverIds }
const request = serverProbeReceiptReadSchema.parse(raw), { serverIds, fence, retiredFence } = await historicalAccess(request, context)
await (retiredFence ?? fence)(); return { scope: "servers", serverIds }
}
const readPage = async (raw: Record<string, unknown>, context: ActionContext) => {
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()
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()
@@ -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")
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)
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
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))
@@ -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")
}
// 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
// leaves a real pending approved source, allowing genuine durable cancel.
// 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
let heldOperation = "", held = false
pool.query = (async function (...args: unknown[]) {
if (!held && typeof args[0] === "string" && args[0].startsWith("UPDATE mcp_operations SET state='running'")) {
held = true; heldOperation = String((args[1] as unknown[])[0]); entered.resolve(); await resume.promise
const query = args[0] as { text?: string; values?: unknown[] } | undefined
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)
}) as typeof pool.query
@@ -2,6 +2,7 @@
import { createMcpServerProbeAuthority, createMcpServerProbeReadAuthority } from "../../services/server-probe-authority.js"
import { createProductionServerProbeExecutor } from "../../services/server-probe-execution.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 { createProbeStorageHostIo } from "../../services/server-probe-storage-host-io.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,
retiredSource: lookup => findRetiredServerProbeHistory(io.rows, lookup),
authority: (principal, options) => createMcpServerProbeAuthority(principal, options, authorityDependencies),
historicalAuthority: (principal, options) => createMcpServerProbeReadAuthority(principal, options, authorityDependencies),
history: (dependencies, execution) => createServerProbeStorageHistoryReader({ assertReadAuthority: dependencies.assertReadAuthority,
+20 -3
View File
@@ -1,10 +1,13 @@
import { randomUUID } from "node:crypto"
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 { compactCompletedServerPollStorage } from "../../services/server-poll-terminal-compaction.js"
import { retireCompletedServerPollOperations } from "../../services/server-poll-operation-retirement.js"
import { pool } from "../../db/index.js"
import { sharedDatabaseGate } from "../../services/shared-database-gate.js"
import { recoverMcpOperations, verifyStep } from "./runtime.js"
import { retireOrdinaryMcpOperations } from "./ordinary-operation-retirement.js"
import { audit, getMcpSettings } from "./store.js"
import { accessGeneration, decryptSecret, redact } from "./security.js"
import { tickRoutingProfiles } from "./routing-profiles.js"
@@ -48,11 +51,22 @@ export async function detectMcpDrift() {
}
}
const mcpRetentionBatchSize = 1000
async function retainMcpData() {
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("DELETE FROM mcp_audit WHERE created_at<now()-make_interval(days=>$1)", [settings.auditRetentionDays])
await pool.query("DELETE FROM mcp_artifacts WHERE expires_at<now()")
await pool.query(`WITH expired AS (
SELECT id FROM mcp_requests WHERE created_at<now()-make_interval(days=>$1) AND status NOT IN ('running')
ORDER BY created_at,id LIMIT $2 FOR UPDATE
) DELETE FROM mcp_requests USING expired WHERE mcp_requests.id=expired.id`, [settings.requestsRetentionDays, mcpRetentionBatchSize])
await pool.query(`WITH expired AS (
SELECT id FROM mcp_audit WHERE created_at<now()-make_interval(days=>$1)
ORDER BY created_at,id LIMIT $2 FOR UPDATE
) DELETE FROM mcp_audit USING expired WHERE mcp_audit.id=expired.id`, [settings.auditRetentionDays, mcpRetentionBatchSize])
await pool.query(`WITH expired AS (
SELECT id FROM mcp_artifacts WHERE expires_at<now()
ORDER BY expires_at,id LIMIT $1 FOR UPDATE
) DELETE FROM mcp_artifacts USING expired WHERE mcp_artifacts.id=expired.id`, [mcpRetentionBatchSize])
await pool.query("DELETE FROM mcp_oauth_requests WHERE expires_at<now()-interval '1 day'")
await pool.query("DELETE FROM mcp_budget_buckets WHERE bucket<now()-interval '2 days'")
await routingReservationTransaction(db => retainManualInverseChargesWithinOwner(db, settings.requestsRetentionDays))
@@ -79,8 +93,11 @@ export async function tickMcpWorkers() {
await retainMcpData()
await retainServerProbeStorage()
await compactCompletedServerPollStorage()
await retireCompletedServerPollOperations()
await collectExpiredServerPollStorage()
await retireOrdinaryMcpOperations()
if ((await getMcpSettings()).enabled) { await detectMcpDrift(); await tickRoutingProfiles() }
await maintainServerProbeOperationRetirement()
} finally {
if (locked) try { await lease.query("SELECT pg_advisory_unlock(hashtextextended('mcp-workers',0))") } catch { unsafe = true }
lease.release(unsafe)
+11 -1
View File
@@ -18,6 +18,7 @@ import { accessGeneration, decryptSecret, encryptSecret, McpError, randomToken,
import { createMcpConnection, listMcpConnections, getMcpSettings, updateMcpSettings, getConnection, assertConnectionVisible, rotateConnectionToken, setConnectionStatus, updateMcpConnection, transaction, audit } from "../modules/mcp/store.js"
import { approvePlan, loadPlan, publicPlan } from "../modules/mcp/runtime.js"
import { applyPlan, invokeMcpTool } from "../modules/mcp/runtime.js"
import { findOwnerPlanReplayTombstone } from "../modules/mcp/operation-replay-tombstones.js"
import { routeAiProfileSchema } from "@mmapp/contracts/mcp-routing"
import { revalidateMcpOwner } from "../modules/mcp/store.js"
import { resourceById } from "../modules/mcp/resources.js"
@@ -243,7 +244,16 @@ export function createMcpRoutes(checkClaims = assertPortalClaimsCurrent, options
return ownerDelegate(async () => invokeMcpTool(principal, action === "recover" ? "mm_operation_recovery_plan" : globalReconcile ? "mm_global_operation_reconcile" : "mm_operation_reconcile", { operationId: id }))
})
app.post("/mcp/plans/:id/apply", async request => {
const plan = await loadPlan(idOf(request.params)), actor = actorOf(request.authUser)
const id = idOf(request.params), actor = actorOf(request.authUser)
let plan: { id: string; connection_id: string; operation_id: string }
try { plan = await loadPlan(id) }
catch (error) {
if (!(error instanceof McpError) || error.code !== "NOT_FOUND") throw error
const tombstone = await findOwnerPlanReplayTombstone(pool, actor.id, id)
if (!tombstone) throw error
const principal = await ownerPrincipal(tombstone.connection_id, actor, tombstone.action_id)
return ownerDelegate(async () => applyPlan(principal, tombstone.plan_id, `web:${tombstone.plan_id}`))
}
const principal = await ownerPrincipal(plan.connection_id, actor, plan.operation_id)
await audit(pool, actor.id, "plan.web.apply", plan.connection_id, { planId: plan.id })
return ownerDelegate(async () => applyPlan(principal, plan.id, `web:${plan.id}`))
+2 -1
View File
@@ -6,10 +6,11 @@ import { observedBoolean, observedDurationMs, observedInteger } from "./ospf-obs
import { readGreNativeRows } from "./gre-observed-state.js"
import { greAddressKey } from "./entity-snapshots.js"
import type { GreMutation } from "./gre-mutation-input.js"
import { greDependencyPaths } from "./routeros-native-paths.js"
export { greDependencyPaths } from "./routeros-native-paths.js"
export const greMutationProblem = (message: string) => Object.assign(new Error(message), { statusCode: 409 })
export type GreNativeRow = Record<string, string>
export const greDependencyPaths = ["/interface", "/interface/bridge/port", "/interface/vlan", "/interface/list/member", "/ip/vrf", "/ip/route", "/ipv6/route", "/routing/filter/rule", "/routing/ospf/interface-template", "/routing/bfd/configuration", "/routing/bgp/connection", "/routing/rule", ...["ip","ipv6"].flatMap(family=>["filter","nat","mangle","raw"].map(table=>`/${family}/firewall/${table}`))] as const
export interface GreMutationState {
serverConfiguration: string; gre: GreNativeRow[]; ipv4: GreNativeRow[]; ipv6: GreNativeRow[];
restoreProtectionProof?:string;
+1 -4
View File
@@ -617,10 +617,7 @@ export function firewallRestPath(
return `${root}/${table}`
}
export function encodeRosId(rosId: string): string {
const id = rosId.startsWith("*") ? rosId : `*${rosId.replace(/^\*/, "")}`
return encodeURIComponent(id)
}
export { encodeRosId } from "./routeros-native-paths.js"
// ── MikrotikClient ─────────────────────────────────────────────────────────────
@@ -0,0 +1,17 @@
export function encodeRosId(rosId: string): string {
const id = rosId.startsWith("*") ? rosId : `*${rosId.replace(/^\*/, "")}`
return encodeURIComponent(id)
}
export const greDependencyPaths = ["/interface", "/interface/bridge/port", "/interface/vlan",
"/interface/list/member", "/ip/vrf", "/ip/route", "/ipv6/route", "/routing/filter/rule",
"/routing/ospf/interface-template", "/routing/bfd/configuration", "/routing/bgp/connection", "/routing/rule",
...["ip", "ipv6"].flatMap(family => ["filter", "nat", "mangle", "raw"].map(table => `/${family}/firewall/${table}`))] as const
export const wireguardInterfaceRuntimeDependencyPaths = ["/routing/bgp/session", "/routing/ospf/neighbor",
"/routing/ospf/interface", "/routing/bfd/session"] as const
export const wireguardInterfaceDependencyPaths = [...new Set<string>([...greDependencyPaths, "/interface/vxlan",
"/interface/vrrp", "/interface/bonding", "/interface/eoip", "/interface/gre", "/interface/ipip", "/ip/dhcp-server",
"/ip/dhcp-client", "/ipv6/dhcp-server", "/ipv6/dhcp-client", "/ip/ipsec/peer", "/ip/ipsec/policy",
...wireguardInterfaceRuntimeDependencyPaths])]
@@ -4,6 +4,7 @@ import assert from "node:assert/strict"
import { createCipheriv, createDecipheriv, createHmac, createHash, randomBytes, randomUUID } from "node:crypto"
import { test } from "node:test"
import { assertServerPollArchiveAccountingForTests } from "./server-poll-archive-accounting.js"
import type { ServerPollRetiredArchive } from "./server-poll-retired-archive.js"
import { createServerPollTerminalArchiveForTests, type ServerPollTerminalArchive, type ServerPollArchiveSourceMetadata } from "./server-poll-terminal-archive.js"
import { PollStorageRefused, SERVER_POLL_STORAGE_POLICY as policy } from "./server-poll-storage-model.js"
import type { ProbeStorageSql } from "./server-probe-journal-maintenance.js"
@@ -19,7 +20,7 @@ const codec = createServerPollTerminalArchiveForTests({
verifySource: async source => source.admission.metadata as unknown as ServerPollArchiveSourceMetadata,
})
const id = (n: number) => "00000000-0000-4000-8000-" + n.toString(16).padStart(12, "0")
interface Entry { row: Record<string, unknown>; archive?: ServerPollTerminalArchive }
interface Entry { row: Record<string, unknown>; archive?: ServerPollTerminalArchive | ServerPollRetiredArchive }
const full = (n: number, owner: string): Entry => ({ row: { admission_receipt_id: id(n), identity_kind: "rest", admission_execution_id: randomUUID(), admission_connection_id: null,
admission_owner_id: owner, admission_binding_proof: "a".repeat(64), admission_context_proof: "b".repeat(64), storage_state: "full", compact_at: null,
admission_archive_bytes: null, admission_activity_at: null, receipt_id: null, has_children: true } })
@@ -34,7 +35,7 @@ async function compact(n: number, owner: string): Promise<Entry> {
storage_state: "compacted", compact_at: new Date("2026-10-29T02:00:00Z"), admission_archive_bytes: archive.ciphertext_bytes,
admission_activity_at: archive.original_activity_at, admission_expires_at: archive.original_expires_at, asserted_ciphertext_bytes: archive.ciphertext_bytes, has_children: false } }
}
function fixture(entries: Entry[]) {
function fixture(entries: Entry[], retiredCharge?: Parameters<typeof assertServerPollArchiveAccountingForTests>[2]) {
const totals = new Map<string, { heavy: number; compact: number; archiveBytes: number; bytes: number; rows: number }>()
for (const entry of entries) {
for (const scope of ["global", "owner:" + entry.row.admission_owner_id]) {
@@ -73,10 +74,35 @@ function fixture(entries: Entry[]) {
} }
return { entries, usage, state, queries, batches, usageBatches, counts: () => ({ factories, charges }), run: () => assertServerPollArchiveAccountingForTests(sql, async () => {
factories++; return async row => { charges++; return codec.verifyCharge(row) }
}) }
}, retiredCharge) }
}
const refused = (work: Promise<unknown>) => assert.rejects(work, PollStorageRefused)
test("mixed V1/V2 allocations authenticate actual stored ciphertext and never fall back across formats", async () => {
const { createServerPollRetiredArchiveForTests } = await import("./server-poll-retired-archive.js")
const unexpected = async () => { throw new Error("Metadata accounting must not open or write archives") }
const v2 = createServerPollRetiredArchiveForTests({
proof, decrypt: unexpected, encrypt: unexpected, openV1: unexpected, verifyHistory: unexpected,
})
const first = await compact(1, "owner"), second = await compact(2, "owner")
const { archive_proof: _v1Proof, ciphertext, ...previous } = second.archive!
const metadata = { ...previous, version: 2 as const }
const archiveProof = await proof("server-poll-retired-archive-record-v2", { ...metadata, identity_kind: "mcp",
storage_state: "compacted", admission_bytes: 32768, charged_rows: 2 })
second.archive = { ...metadata, ciphertext, archive_proof: archiveProof }
Object.assign(second.row, { version: 2, archive_proof: archiveProof })
const f = fixture([first, second], v2.verifyCharge)
await f.run()
assert.deepEqual(f.counts(), { factories: 1, charges: 1 })
second.archive.ciphertext = "A" + ciphertext.slice(1)
await refused(f.run())
assert.deepEqual(f.counts(), { factories: 2, charges: 2 })
second.archive.ciphertext = ciphertext
second.row.version = 3
await refused(f.run())
assert.deepEqual(f.counts(), { factories: 3, charges: 3 })
})
test("empty/global0 and full128 ledgers use complete exact global/owner counters without archive crypto", async () => {
const empty = fixture([]); await empty.run(); assert.deepEqual(empty.counts(), { factories: 0, charges: 0 })
const f = fixture(Array.from({ length: 128 }, (_, n) => full(n + 1, "owner-" + Math.floor(n / 8))))
@@ -3,6 +3,7 @@
import type { ProbeStorageSql } from "./server-probe-journal-maintenance.js"
import { SERVER_POLL_STORAGE_POLICY as policy, PollStorageRefused } from "./server-poll-storage-model.js"
import { createServerPollArchiveChargeVerifier, type verifyServerPollArchiveCharge } from "./server-poll-terminal-archive.js"
import type { verifyServerPollRetiredArchiveCharge } from "./server-poll-retired-archive.js"
const BATCH = 128
const MAX_ENTRIES = Math.floor(policy.globalBytes / policy.admissionBytes)
@@ -15,6 +16,8 @@ function integer(value: unknown): number {
}
interface Total { heavy: number; compact: number; archiveBytes: number; rows: number; bytes: number }
const zero = (): Total => ({ heavy: 0, compact: 0, archiveBytes: 0, rows: 0, bytes: 0 })
const verifyRetiredCharge: typeof verifyServerPollRetiredArchiveCharge = async row =>
(await import("./server-poll-retired-archive.js")).verifyServerPollRetiredArchiveCharge(row)
const QUERY = `SELECT a.receipt_id AS admission_receipt_id,a.identity_kind,a.execution_id AS admission_execution_id,
a.connection_id AS admission_connection_id,a.owner_id AS admission_owner_id,a.binding_proof AS admission_binding_proof,
a.context_proof AS admission_context_proof,a.storage_state,a.compact_at,a.archive_bytes AS admission_archive_bytes,
@@ -34,7 +37,8 @@ const QUERY = `SELECT a.receipt_id AS admission_receipt_id,a.identity_kind,a.exe
FROM server_poll_storage_admissions a LEFT JOIN server_poll_archives r ON r.receipt_id=a.receipt_id
WHERE ($1::uuid IS NULL OR a.receipt_id>$1::uuid) ORDER BY a.receipt_id LIMIT 128`
async function verify(sql: ProbeStorageSql, factory: () => Promise<typeof verifyServerPollArchiveCharge>) {
async function verify(sql: ProbeStorageSql, factory: () => Promise<typeof verifyServerPollArchiveCharge>,
retiredCharge: typeof verifyServerPollRetiredArchiveCharge) {
const global = zero(), owners = new Map<string, Total>()
const seenReceipts = new Set<string>()
let cursor: string | null = null, count = 0, charge: typeof verifyServerPollArchiveCharge | undefined
@@ -61,8 +65,13 @@ async function verify(sql: ProbeStorageSql, factory: () => Promise<typeof verify
row.binding_proof !== row.admission_binding_proof || row.context_proof !== row.admission_context_proof ||
row.original_activity_at !== row.admission_activity_at || row.original_expires_at !== row.admission_expires_at ||
integer(row.ciphertext_bytes) !== integer(row.asserted_ciphertext_bytes) || integer(row.ciphertext_bytes) !== integer(row.admission_archive_bytes)) fail()
charge ??= await factory()
const verified = await charge(row)
let verified
if (row.version === 1) {
charge ??= await factory()
verified = await charge(row)
} else if (row.version === 2) {
verified = await retiredCharge(row)
} else return fail()
addition = { heavy: 0, compact: 1, archiveBytes: verified.metadata.ciphertext_bytes, rows: verified.rows, bytes: verified.bytes }
} else return fail()
const owned = owners.get(row.admission_owner_id) ?? zero()
@@ -102,8 +111,9 @@ async function verify(sql: ProbeStorageSql, factory: () => Promise<typeof verify
/** Caller must already own server-poll-storage-v1. Any malformed/missing MAC or
* counter refuses admission before an INSERT, credential read or native I/O. */
export async function assertServerPollArchiveAccounting(sql: ProbeStorageSql): Promise<void> {
try { await verify(sql, createServerPollArchiveChargeVerifier) } catch { return fail() }
try { await verify(sql, createServerPollArchiveChargeVerifier, verifyRetiredCharge) } catch { return fail() }
}
export async function assertServerPollArchiveAccountingForTests(sql: ProbeStorageSql, factory: () => Promise<typeof verifyServerPollArchiveCharge>): Promise<void> {
try { await verify(sql, factory) } catch { return fail() }
export async function assertServerPollArchiveAccountingForTests(sql: ProbeStorageSql, factory: () => Promise<typeof verifyServerPollArchiveCharge>,
retiredCharge = verifyRetiredCharge): Promise<void> {
try { await verify(sql, factory, retiredCharge) } catch { return fail() }
}
@@ -5,7 +5,8 @@ import assert from "node:assert/strict"
import { createCipheriv, createDecipheriv, createHmac, randomBytes, randomUUID } from "node:crypto"
import { test } from "node:test"
import { fingerprint } from "../modules/mcp/security.js"
import { readServerPollArchivedHistoryForTests } from "./server-poll-history-reader.js"
import { readRetiredServerPollHistoryForTests, readServerPollArchivedHistoryForTests } from "./server-poll-history-reader.js"
import type { openServerPollRetiredArchive } from "./server-poll-retired-archive.js"
import type { openServerPollTerminalArchive, ServerPollArchiveSource } from "./server-poll-terminal-archive.js"
function fixture(){
@@ -110,3 +111,61 @@ test("caller mutation while decrypt awaits cannot replace authenticated lookup i
}}),true)
assert.equal(f.calls[0].values[0],f.raw().receiptId)
})
function retiredFixture() {
const now = Date.now(), operationId = randomUUID(), connectionId = randomUUID(), planId = randomUUID(), ownerId = "retired-owner"
const identity = { operationId, connectionId, ownerId }
const row = { plan_id: planId, target_server_ids: ["2", "1"], gc_not_before: "2026-10-31T01:00:00.123456Z" }
const failure = Error("authority revoked")
let queries = 0, opens = 0, checks = 0
let mode = ""
const run = () => readRetiredServerPollHistoryForTests(identity, async () => {
checks++
if (mode === "revoked-after-open" && opens) throw failure
}, { deadlineAt: now + 1000 }, {
now: () => now,
query: async (sql, parameters) => {
queries++
assert.deepEqual(parameters, [operationId, connectionId, ownerId])
assert.match(sql, /mcp_operation_replay_tombstones/)
assert.match(sql, /r\.version=2/)
assert.match(sql, /NOT EXISTS\(SELECT 1 FROM public\.mcp_operations/)
if (mode === "gone" && queries === 2) return []
if (mode === "changed" && queries === 2) return [{ ...row, target_server_ids: ["1"] }]
return [row]
},
open: async () => {
opens++
if (mode === "bad-proof") throw Error("private proof failure")
return { metadata: { execution_id: operationId, connection_id: connectionId, owner_id: ownerId },
history: { planId, affectedServerIds: [1, 2], gcNotBefore: "2026-10-31T01:00:00.123456Z" } } as Awaited<ReturnType<typeof openServerPollRetiredArchive>>
},
})
return { row, failure, run, mode: (value: string) => { mode = value }, counts: () => ({ queries, opens, checks }) }
}
test("retired lookup verifies a current tombstone, archive proof and a second SQL snapshot before returning only scope", async () => {
const f = retiredFixture()
assert.deepEqual(await f.run(), { affectedServerIds: [1, 2] })
assert.deepEqual(f.counts(), { queries: 2, opens: 1, checks: 4 })
})
test("retired lookup refuses proof, plan, scope or retention drift and GC between reads", async () => {
for (const mode of ["bad-proof", "changed", "gone"]) {
const f = retiredFixture()
f.mode(mode)
assert.equal(await f.run(), undefined)
}
for (const row of [{ plan_id: randomUUID() }, { target_server_ids: ["1", "1"] }, { gc_not_before: "2026-10-31T01:00:00.123457Z" }]) {
const f = retiredFixture()
Object.assign(f.row, row)
assert.equal(await f.run(), undefined)
}
})
test("retired lookup propagates exact current authorization failure after archive open", async () => {
const f = retiredFixture()
f.mode("revoked-after-open")
await assert.rejects(f.run(), error => error === f.failure)
assert.equal(f.counts().queries, 1)
})
@@ -3,7 +3,8 @@
* Original credentials authenticate history only; they are never renewed. */
import { z } from "zod"
import { decryptSecret, fingerprint, privateSecretFingerprint } from "../modules/mcp/security.js"
import { openServerPollTerminalArchive } from "./server-poll-terminal-archive.js"
import { canonicalPollArchiveTimestamp, openServerPollTerminalArchive } from "./server-poll-terminal-archive.js"
import { openServerPollRetiredArchive } from "./server-poll-retired-archive.js"
export interface ServerPollHistoryReadWindow { deadlineAt: number; signal?: AbortSignal }
type Query = (text: string, values: unknown[], options: ServerPollHistoryReadWindow) => Promise<Record<string, unknown>[]>
@@ -99,3 +100,72 @@ const production:Dependencies={
export const readServerPollArchivedHistory=(raw:Record<string,unknown>,authorize:()=>Promise<unknown>,options:ServerPollHistoryReadWindow)=>read(raw,authorize,options,production)
/** Explicit component-only composition. No public input selects these hooks. */
export const readServerPollArchivedHistoryForTests=(raw:Record<string,unknown>,authorize:()=>Promise<unknown>,options:ServerPollHistoryReadWindow,deps:Dependencies)=>read(raw,authorize,options,deps)
const RETIRED_QUERY = `SELECT r.version,r.receipt_id,r.execution_id,r.connection_id,r.owner_id,r.binding_proof,r.context_proof,
r.source_digest,r.source_sha256,r.terminal_digest,r.decoded_bytes,r.compressed_bytes,r.archive_proof,
to_char(r.original_activity_at AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS original_activity_at,
to_char(r.original_expires_at AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS original_expires_at,
to_char(a.gc_not_before AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS gc_not_before,
a.retention_days,a.safety_ms,octet_length(r.ciphertext) AS ciphertext_bytes,
pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(r.ciphertext,'UTF8')),'hex') AS ciphertext_sha256,
CASE WHEN octet_length(r.ciphertext)<=33000 THEN r.ciphertext END AS ciphertext,
t.plan_id,t.target_server_ids
FROM public.server_poll_archives r JOIN public.server_poll_storage_admissions a USING(receipt_id)
JOIN public.mcp_connections c ON c.id=a.connection_id AND c.owner_id=a.owner_id
JOIN public.mcp_operation_replay_tombstones t ON t.operation_id=a.execution_id AND t.connection_id=a.connection_id
WHERE r.execution_id=$1 AND a.execution_id=$1 AND a.connection_id=$2 AND a.owner_id=$3
AND a.identity_kind='mcp' AND a.storage_state='compacted' AND r.version=2
AND r.connection_id=a.connection_id AND r.owner_id=a.owner_id AND r.binding_proof=a.binding_proof AND r.context_proof=a.context_proof
AND r.ciphertext_bytes=a.archive_bytes AND r.ciphertext_bytes=octet_length(r.ciphertext)
AND r.original_activity_at=a.original_activity_at AND r.original_expires_at=a.authority_expires_at
AND t.action_id='servers.poll' AND t.target_global IS TRUE AND cardinality(t.target_server_ids) BETWEEN 1 AND 1000
AND NOT EXISTS(SELECT 1 FROM public.mcp_operations o WHERE o.id=t.operation_id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_plans p WHERE p.id=t.plan_id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_operation_steps s WHERE s.operation_id=t.operation_id)
LIMIT 2`
interface RetiredDependencies {
query: Query; now(): number; open: typeof openServerPollRetiredArchive
}
export interface RetiredSourceIdentity { operationId: string; connectionId: string; ownerId: string }
async function readRetired(identity: RetiredSourceIdentity, currentAuthorize: () => Promise<unknown>,
rawWindow: ServerPollHistoryReadWindow, deps: RetiredDependencies): Promise<{ affectedServerIds: number[] } | undefined> {
let authFailure: { error: unknown } | undefined
try {
const { operationId, connectionId, ownerId } = identity
if (!uuid.safeParse(operationId).success || !uuid.safeParse(connectionId).success || !ownerId || ownerId.length > 200) fail()
const options = Object.freeze({ ...rawWindow, deadlineAt: Math.min(rawWindow.deadlineAt, deps.now() + 180000) })
const live = () => { if (!Number.isSafeInteger(options.deadlineAt) || deps.now() >= options.deadlineAt || options.signal?.aborted) fail() }
const authorize = async () => {
live()
try { await currentAuthorize() } catch (error) { authFailure = { error }; throw error }
live()
}
const values = [operationId, connectionId, ownerId]
await authorize()
const rows = await deps.query(RETIRED_QUERY, values, options)
await authorize()
if (rows.length !== 1) return undefined
const row = structuredClone(rows[0])
const { metadata, history } = await deps.open(row)
await authorize()
if (metadata.execution_id !== operationId || metadata.connection_id !== connectionId || metadata.owner_id !== ownerId
|| history.planId !== row.plan_id || history.gcNotBefore !== canonicalPollArchiveTimestamp(row.gc_not_before)) fail()
const scope = z.array(z.union([sid, z.string().regex(/^[1-9]\d{0,15}$/).transform(Number).pipe(sid)])).min(1).max(1000).parse(row.target_server_ids)
if (scope.length !== history.affectedServerIds.length || new Set(scope).size !== scope.length
|| scope.some(id => !history.affectedServerIds.includes(id))) fail()
const fresh = await deps.query(RETIRED_QUERY, values, options)
await authorize()
if (fresh.length !== 1 || fingerprint(fresh[0]) !== fingerprint(row)) fail()
return { affectedServerIds: [...history.affectedServerIds] }
} catch {
if (authFailure) throw authFailure.error
return undefined
}
}
/** The caller owns current authority and independently checks returned scope. */
export const readRetiredServerPollHistory = (identity: RetiredSourceIdentity, authorize: () => Promise<unknown>, options: ServerPollHistoryReadWindow) =>
readRetired(identity, authorize, options, { query: production.query, now: Date.now, open: openServerPollRetiredArchive })
export const readRetiredServerPollHistoryForTests = readRetired
@@ -0,0 +1,183 @@
/** Owned-only historical fixture. Synthetically seeds a historical timeline
* before signing. The real V1 compactor, V2 retirement, read and GC run with
* production crypto and all database guards enabled. */
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { assertOwnedBandwidthSql } from "./bandwidth-admission-process.sql-helper.js"
assertOwnedBandwidthSql()
assert.equal(process.env.MMAPP_OWN_MCP_OPERATION_ORIGIN,"true")
const { pool,closePool } = await import("../db/index.js")
const { applySqlMigrations } = await import("../db/migrate.js")
const { createMcpConnection,authenticateMcpToken } = await import("../modules/mcp/store.js")
const { mcpConnectionCreateSchema } = await import("@mmapp/contracts/mcp")
const { invokeMcpTool } = await import("../modules/mcp/runtime.js")
const { encryptSecret,privateSecretFingerprint,fingerprint,redact } = await import("../modules/mcp/security.js")
const { encodeServerPollJournalAnchor } = await import("./server-poll-journal-anchor.js")
const { compactCompletedServerPollStorage,SERVER_POLL_ARCHIVE_READ_SQL } = await import("./server-poll-terminal-compaction.js")
const { openServerPollTerminalArchive } = await import("./server-poll-terminal-archive.js")
const { retireCompletedServerPollOperation } = await import("./server-poll-operation-retirement.js")
const { collectExpiredServerPollStorage } = await import("./server-poll-storage-retention.js")
const { PollStorageUnknown } = await import("./server-poll-storage.js")
const { tickMcpWorkers } = await import("../modules/mcp/workers.js")
const { Client } = await import("pg")
const proof=(domain:string,value:unknown)=>privateSecretFingerprint(fingerprint(value),domain)
const encrypt=(value:unknown)=>encryptSecret(JSON.stringify(value))
async function historical(days:number,serverId:number) {
const base=Date.now()-days*86400000, stamp=(offset:number)=>new Date(base+offset).toISOString()
const ownerId=randomUUID(),executionId=randomUUID(),receiptId=randomUUID(),planId=randomUUID()
const actor={id:ownerId,email:"[email protected]",name:"Owned historical poll",apps:["mm"],
permissions:["mm:servers:write","mm:servers: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:[serverId],operationIds:["servers.poll","operations.global.reconcile"],allowGlobal:true},
limits:{callsPerMinute:100,mutationsPerHour:10}}),actor)
const current=await authenticateMcpToken(issued.token),connectionId=current.connectionId,credentialId=randomUUID()
const core={version:1 as const,requiresHuman:true as const,scope:"global" as const,request:{serverId,receiptId},around:stamp(0),
beforeProof:"a".repeat(64),authorityProof:"b".repeat(64),maintenance:[],affectedServerIds:[serverId],mutationCost:8}
const plan={...core,bindingProof:await proof("server-poll-production-v1",core)}
const context={identity:{kind:"mcp" as const,ownerId,executionId,connectionId,credentialId,operationName:"servers.poll",
issuedAt:base,expiresAt:base+180000},plan}
const contextProof=await proof("server-poll-journal-v1:context",context)
const anchor=await encodeServerPollJournalAnchor(context,undefined,{proof:(domain,value)=>proof("server-poll-journal-v1:"+domain,value)})
const binding={version:1,operation:"servers.poll",principal:{connectionId,credentialId,credentialExpiresAt:stamp(240000)},
ownerId,issuedAt:base,expiresAt:base+180000,plan}
const input={serverId,receiptId,inputDigest:await proof("server-poll-mcp-binding-v1",binding),bindingCiphertext:await encrypt(binding)}
const step={kind:"service",service:"servers.poll",input}
const evidence={target:{scope:"global"},objects:[{kind:"server",id:String(serverId),serverId}],snapshots:[],steps:[step],
before:{proof:plan.beforeProof},mutationCost:8,requiresHuman:true,verificationScope:"configuration",validUntil:stamp(180000),
warnings:["Полный опрос читает устройство и изменяет общие снимки и разделы истории; требуется подтверждение человеком. Автоматический повтор запрещён."]}
const snapshot={id:42,serverId,polledAt:stamp(1000),status:"online" as const,latencyMs:0,rosVersion:null,boardName:null,
uptime:null,cpuLoad:null,freeMemory:null,totalMemory:null,identityName:null}
const result={operationId:executionId,receiptId,nativeReads:4,confirmedPublications:2,completedPublications:2,complete:true,
maintenanceComplete:true,configurationVerified:true,automaticReplay:false,snapshot}
const receipt={receiptId,nativeReads:4,confirmedPublications:2,completedPublications:2,complete:true,
maintenanceComplete:true,configurationVerified:true,afterProof:"f".repeat(64),observation:"online",snapshot}
const db=await pool.connect()
try{
await db.query("BEGIN")
await db.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,'servers.poll',1,1,'owned-historical',$3,$4,'completed',$5,$6,$7,$8)`,
[planId,connectionId,{sealed:await encrypt({serverId})},{sealed:await encrypt(evidence)},stamp(190000),ownerId,stamp(50),stamp(25)])
await db.query(`INSERT INTO mcp_operations(id,plan_id,connection_id,idempotency_key,state,verification,result,created_at,finished_at)
VALUES($1,$2,$3,$4,'applied','passed',$5,$6,$7)`,
[executionId,planId,connectionId,"owned-historical-"+executionId,{confirmedWrites:1,verificationScope:"configuration"},stamp(100),stamp(1600)])
await db.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)`,[executionId,redact(step),result,stamp(110),stamp(1500)])
// Historical journal admission is impossible via live writes by design.
// Only this private fixture transaction seeds the signed, pre-existing rows.
await db.query("SET LOCAL session_replication_role='replica'")
await db.query(`INSERT INTO server_poll_storage_admissions(receipt_id,identity_kind,execution_id,connection_id,owner_id,binding_proof,
context_proof,authority_issued_at,authority_expires_at,max_stages,claim_attempted_at,created_at,retention_days,gc_not_before,used_rows,used_bytes)
VALUES($1,'mcp',$2,$3,$4,$5,$6,$7,$8,8,$9,$10,365,$11,14,4000)`,
[receiptId,executionId,connectionId,ownerId,plan.bindingProof,contextProof,stamp(0),stamp(180000),stamp(300),stamp(200),stamp(180000+365*86400000+60000)])
await db.query(`INSERT INTO server_poll_executions(receipt_id,identity_kind,execution_id,binding_proof,context_ciphertext,context_proof,created_at)
VALUES($1,'mcp',$2,$3,$4,$5,$6)`,[receiptId,executionId,plan.bindingProof,await encryptSecret(anchor),contextProof,stamp(400)])
const stages=["native.identity","native.resource","native.interfaces","native.addresses","publish.0","publish.1"]
for(const [index,stage] of stages.entries()){
const beforeProof=index>4?"e".repeat(64):plan.beforeProof,effectProof="c".repeat(64)
const resultProof=index===stages.length-1?receipt.afterProof:"e".repeat(64),outcome="confirmed"
await db.query("INSERT INTO server_poll_intents VALUES($1,$2,$3,$4,$5,$6)",
[receiptId,stage,beforeProof,effectProof,await proof("server-poll-journal-v1:intent",{receiptId,stage,beforeProof,effectProof}),stamp(500+index*100)])
await db.query("INSERT INTO server_poll_acks VALUES($1,$2,$3,$4,$5,$6)",
[receiptId,stage,resultProof,outcome,await proof("server-poll-journal-v1:ack",{receiptId,stage,resultProof,outcome}),stamp(550+index*100)])
}
const receiptText=JSON.stringify(receipt)
await db.query("INSERT INTO server_poll_receipts VALUES($1,$2,$3,$4)",
[receiptId,await encryptSecret(receiptText),await proof("server-poll-journal-v1:receipt",receiptText),stamp(1400)])
await db.query("INSERT INTO server_poll_storage_usage(scope,admissions,reserved_rows,reserved_bytes) VALUES($1,1,4019,8421376)",["owner:"+ownerId])
await db.query("UPDATE server_poll_storage_usage SET admissions=admissions+1,reserved_rows=reserved_rows+4019,reserved_bytes=reserved_bytes+8421376 WHERE scope='global'")
await db.query("SET LOCAL session_replication_role='origin'")
assert.equal((await db.query("SELECT server_poll_storage_accounting_valid() AS valid")).rows[0].valid,true)
await db.query("COMMIT")
}catch(error){await db.query("ROLLBACK");throw error}finally{db.release()}
return {receiptId,executionId,planId,ownerId,connectionId,current}
}
try{
await applySqlMigrations(pool)
await pool.query("UPDATE mcp_settings SET enabled=true,requests_retention_days=365 WHERE id=1")
const {rows:[server]}=await pool.query("INSERT INTO servers(name,host,enabled) VALUES('owned historical poll','127.0.0.1',false) RETURNING id")
const retained=await historical(181,Number(server.id)), expired=await historical(366,Number(server.id))
for(const fixture of [expired,retained]){
assert.equal((await compactCompletedServerPollStorage()).compacted,1)
const {rows:[v1]}=await pool.query(SERVER_POLL_ARCHIVE_READ_SQL,[fixture.receiptId])
await openServerPollTerminalArchive(v1,Date.now())
const before=(await pool.query("SELECT gc_not_before,authority_expires_at,retention_days,original_activity_at FROM server_poll_storage_admissions WHERE receipt_id=$1",[fixture.receiptId])).rows[0]
await assert.rejects(retireCompletedServerPollOperation({receiptId:fixture.receiptId,oldArchiveProof:"0".repeat(64)}))
const hold=randomUUID()
await pool.query("INSERT INTO mcp_drift(id,operation_id,expected,observed) VALUES($1,$2,'{}','{}')",[hold,fixture.executionId])
await assert.rejects(retireCompletedServerPollOperation({receiptId:fixture.receiptId,oldArchiveProof:v1.archive_proof}))
assert.equal((await pool.query(SERVER_POLL_ARCHIVE_READ_SQL,[fixture.receiptId])).rows[0].archive_proof,v1.archive_proof)
assert.equal((await pool.query("SELECT server_poll_storage_accounting_valid() AS valid")).rows[0].valid,true)
assert.equal((await pool.query("SELECT 1 FROM server_poll_retirement_claims WHERE receipt_id=$1",[fixture.receiptId])).rows.length,0)
await pool.query("DELETE FROM mcp_drift WHERE id=$1",[hold])
assert.equal((await retireCompletedServerPollOperation({receiptId:fixture.receiptId,oldArchiveProof:v1.archive_proof})).retired,true)
const {rows:[v2]}=await pool.query(SERVER_POLL_ARCHIVE_READ_SQL,[fixture.receiptId])
assert.equal(v2.version,2);assert(v2.ciphertext_bytes<v1.ciphertext_bytes)
assert.deepEqual((await pool.query("SELECT gc_not_before,authority_expires_at,retention_days,original_activity_at FROM server_poll_storage_admissions WHERE receipt_id=$1",[fixture.receiptId])).rows[0],before)
const {rows:[removed]}=await pool.query(`SELECT
NOT EXISTS(SELECT 1 FROM mcp_operations WHERE id=$1) AS operation,
NOT EXISTS(SELECT 1 FROM mcp_operation_steps WHERE operation_id=$1) AS step,
NOT EXISTS(SELECT 1 FROM mcp_plans WHERE id=$2) AS plan,
EXISTS(SELECT 1 FROM mcp_operation_replay_tombstones WHERE operation_id=$1 AND plan_id=$2) AS tombstone,
server_poll_storage_accounting_valid() AS accounting`,[fixture.executionId,fixture.planId])
assert.deepEqual(removed,{operation:true,step:true,plan:true,tombstone:true,accounting:true})
const read=await invokeMcpTool(fixture.current,"mm_global_operation_reconcile",{operationId:fixture.executionId}) as {results:{matchesDesiredState:boolean}[];automaticRetry:boolean}
assert.equal(read.results[0].matchesDesiredState,true);assert.equal(read.automaticRetry,false)
const foreign=fixture===retained?expired.current:retained.current
await assert.rejects(invokeMcpTool(foreign,"mm_global_operation_reconcile",{operationId:fixture.executionId}),
(error:unknown)=>(error as {code?:string}).code==="NOT_FOUND")
}
assert.equal((await collectExpiredServerPollStorage()).collected,1)
assert.equal((await collectExpiredServerPollStorage()).collected,0)
const {rows}=await pool.query("SELECT r.receipt_id,r.ciphertext_bytes,u.reserved_bytes,u.reserved_rows FROM server_poll_archives r CROSS JOIN server_poll_storage_usage u WHERE u.scope='global'")
assert.equal(rows.length,1);assert.equal(rows[0].receipt_id,retained.receiptId)
assert.equal(Number(rows[0].reserved_bytes),32768+rows[0].ciphertext_bytes);assert.equal(Number(rows[0].reserved_rows),2)
assert.equal((await pool.query("SELECT 1 FROM server_poll_retirement_claims WHERE receipt_id=$1",[expired.receiptId])).rows.length,0)
const held=await historical(190,Number(server.id)), spent=await historical(189,Number(server.id))
const ready=await historical(188,Number(server.id))
// The first two are already compacted. The actual worker has to compact the
// next eligible archive, skip older held/spent V1 rows, then retire it.
assert.equal((await compactCompletedServerPollStorage()).compacted,1)
assert.equal((await compactCompletedServerPollStorage()).compacted,1)
await pool.query("INSERT INTO mcp_drift(id,operation_id,expected,observed) VALUES($1,$2,'{}','{}')",[randomUUID(),held.executionId])
const {rows:[spentV1]}=await pool.query(SERVER_POLL_ARCHIVE_READ_SQL,[spent.receiptId])
const originalQuery=Client.prototype.query
let commits=0
try{
Client.prototype.query=function(this:InstanceType<typeof Client>,...args:unknown[]){
if((args[0] as {text?:string})?.text==="COMMIT" && ++commits===2) {
// No COMMIT ACK can reach the owner. Closing the real checked-out
// backend rolls back uncommitted work, leaving the earlier claim spent.
return Promise.reject(Error("owned poll retirement COMMIT transport loss"))
}
return Reflect.apply(originalQuery,this,args)
} as typeof Client.prototype.query
await assert.rejects(retireCompletedServerPollOperation({receiptId:spent.receiptId,oldArchiveProof:spentV1.archive_proof}),
(error:unknown)=>error instanceof PollStorageUnknown && error.automaticReplay===false)
}finally{Client.prototype.query=originalQuery}
assert.equal(commits,2)
assert.equal((await pool.query(SERVER_POLL_ARCHIVE_READ_SQL,[spent.receiptId])).rows[0].version,1)
assert.equal((await pool.query("SELECT 1 FROM server_poll_retirement_claims WHERE receipt_id=$1",[spent.receiptId])).rows.length,1)
const workerProjection=async()=>(await pool.query(`SELECT a.receipt_id,r.version,r.archive_proof,a.archive_bytes,
o.id AS live_operation,t.operation_id AS tombstone,c.claim_token
FROM server_poll_storage_admissions a LEFT JOIN server_poll_archives r USING(receipt_id)
LEFT JOIN mcp_operations o ON o.id=a.execution_id
LEFT JOIN mcp_operation_replay_tombstones t ON t.operation_id=a.execution_id
LEFT JOIN server_poll_retirement_claims c USING(receipt_id)
WHERE a.receipt_id=ANY($1::uuid[]) ORDER BY a.receipt_id`,[[held.receiptId,spent.receiptId,ready.receiptId]])).rows
const beforeWorker=await workerProjection()
await tickMcpWorkers()
const afterWorker=await workerProjection()
assert.deepEqual(afterWorker.filter(row=>row.receipt_id!==ready.receiptId),beforeWorker.filter(row=>row.receipt_id!==ready.receiptId))
const retiredByWorker=afterWorker.find(row=>row.receipt_id===ready.receiptId)
assert.equal(retiredByWorker?.version,2);assert.equal(retiredByWorker?.live_operation,null)
assert.equal(retiredByWorker?.tombstone,ready.executionId);assert(retiredByWorker?.claim_token)
await tickMcpWorkers()
assert.deepEqual(await workerProjection(),afterWorker)
assert.equal((await pool.query("SELECT server_poll_storage_accounting_valid() AS valid")).rows[0].valid,true)
console.log("Owned PostgreSQL: authenticated V1/V2 transition, retained history, once-only refund, and actual worker skips held/spent rows")
}finally{await closePool()}
@@ -0,0 +1,162 @@
/** Scheduled/internal maintenance only. No public HTTP or MCP registration. */
import { randomUUID } from "node:crypto"
import { z } from "zod"
import { operationIdempotencyDigest } from "../modules/mcp/operation-replay-tombstones.js"
import { fingerprint } from "../modules/mcp/security.js"
import { assertServerPollArchiveAccounting } from "./server-poll-archive-accounting.js"
import { SERVER_POLL_ARCHIVE_READ_SQL } from "./server-poll-terminal-compaction.js"
import { openServerPollRetiredArchive, projectServerPollRetiredArchive, type LockedServerPollRetirementSource } from "./server-poll-retired-archive.js"
import { PollStorageRefused, runPollStorageTransaction } from "./server-poll-storage.js"
import type { ProbeStorageSql } from "./server-probe-journal-maintenance.js"
import { connectProbeStorage } from "./server-probe-storage-connection.js"
import { sharedDatabaseGate } from "./shared-database-gate.js"
const request = z.object({ receiptId: z.string().uuid(), oldArchiveProof: z.string().regex(/^[a-f0-9]{64}$/) }).strict()
const requireThat: (value: unknown) => asserts value = value => {
if (!value) throw new PollStorageRefused()
}
/** The storage owner already holds its global xact advisory lock. Do not wait
* for a connection writer or runner that might need the storage lock. */
const ELIGIBLE = `FROM public.server_poll_storage_admissions a JOIN public.server_poll_archives r USING(receipt_id)
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
WHERE a.identity_kind='mcp' AND a.storage_state='compacted'
AND r.version=1 AND r.execution_id=a.execution_id AND r.connection_id=a.connection_id AND r.owner_id=a.owner_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='servers.poll' AND p.operation_version=1 AND p.state='completed' AND p.approved_by IS NOT NULL
AND s.state='verified' AND s.server_id IS NULL AND s.finished_at IS NOT NULL
AND (SELECT count(*) FROM public.mcp_operation_steps WHERE operation_id=o.id)=1
AND NOT EXISTS(SELECT 1 FROM public.mcp_operation_checkpoints WHERE operation_id=o.id)
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.vrf_production_tickets WHERE plan_id=p.id)`
const SOURCE = `SELECT CASE WHEN octet_length(to_jsonb(a)::text)<=1048576 THEN to_jsonb(a) END AS admission,
CASE WHEN octet_length(to_jsonb(o)::text)+octet_length(to_jsonb(p)::text)+octet_length(to_jsonb(s)::text)<=16777216
THEN jsonb_build_object('operation',to_jsonb(o),'plan',to_jsonb(p),'step',to_jsonb(s)) END AS terminal,
o.id AS operation_id,p.id AS plan_id,o.idempotency_key,clock_timestamp() AS observed_at
${ELIGIBLE} AND a.receipt_id=$1 AND r.archive_proof=$2 FOR UPDATE OF a,r,o,p,s NOWAIT`
const CANDIDATES = `SELECT a.receipt_id,r.archive_proof,o.finished_at::text AS finished_at ${ELIGIBLE}
AND NOT EXISTS(SELECT 1 FROM public.server_poll_retirement_claims c WHERE c.receipt_id=a.receipt_id)
AND ($1::timestamptz IS NULL OR (o.finished_at,a.receipt_id)>($1::timestamptz,$2::uuid))
ORDER BY o.finished_at,a.receipt_id LIMIT 8`
const deps = { connect:connectProbeStorage,now:Date.now }
const transaction = <T>(deadlineAt:number, work:(sql:ProbeStorageSql)=>Promise<T>) =>
runPollStorageTransaction(deps,{deadlineAt},async()=>{},work)
type Target = z.infer<typeof request>
async function prepare(sql: ProbeStorageSql,{receiptId,oldArchiveProof}:Target) {
const identity = (await sql.query(`SELECT a.connection_id,a.execution_id,a.owner_id FROM public.server_poll_storage_admissions a
JOIN public.server_poll_archives r USING(receipt_id) WHERE a.receipt_id=$1 AND a.storage_state='compacted'
AND a.identity_kind='mcp' AND r.version=1 AND r.archive_proof=$2`, [receiptId,oldArchiveProof])).rows
requireThat(identity.length===1)
const { connection_id: connectionId,execution_id: operationId,owner_id: ownerId } = identity[0]
requireThat((await sql.query("SELECT id FROM public.mcp_connections WHERE id=$1 AND owner_id=$2 FOR UPDATE NOWAIT",
[connectionId,ownerId])).rows.length===1)
for (const key of ["mcp-operation:"+operationId,"server-poll-execution-v1:mcp:"+operationId,"server-poll-receipt-v1:"+receiptId]) {
requireThat((await sql.query("SELECT pg_try_advisory_xact_lock(hashtextextended($1,0)) AS locked",[key])).rows[0]?.locked===true)
}
const rows = (await sql.query(SOURCE,[receiptId,oldArchiveProof])).rows
requireThat(rows.length===1 && rows[0].operation_id===operationId && rows[0].admission && rows[0].terminal)
const { admission,terminal,plan_id: planId,idempotency_key: key,observed_at: observedAt } = rows[0]
requireThat(typeof key==="string" && typeof connectionId==="string" && typeof planId==="string")
await assertServerPollArchiveAccounting(sql)
const oldArchive = (await sql.query(SERVER_POLL_ARCHIVE_READ_SQL,[receiptId])).rows
requireThat(oldArchive.length===1 && oldArchive[0].archive_proof===oldArchiveProof)
const archived = await projectServerPollRetiredArchive({ archive:oldArchive[0],admission,terminal } as LockedServerPollRetirementSource,
new Date(observedAt as string | Date).getTime())
requireThat(archived)
const projection = await openServerPollRetiredArchive(archived)
requireThat(projection.metadata.receipt_id===receiptId && projection.metadata.execution_id===operationId
&& projection.metadata.connection_id===connectionId && projection.metadata.owner_id===ownerId && projection.history.planId===planId)
return {connectionId,operationId,planId,key,archived,projection,previousBytes:oldArchive[0].ciphertext_bytes}
}
async function retire(target:Target,deadlineAt:number) {
const {receiptId,oldArchiveProof}=target
const token=randomUUID()
await transaction(deadlineAt,async sql=>{
// Check the live hold predicates, all locks and both archive proofs before
// spending the irreversible one-shot claim.
await prepare(sql,target)
const claim=await sql.query(`INSERT INTO public.server_poll_retirement_claims(receipt_id,old_archive_proof,claim_token)
VALUES($1,$2,$3) ON CONFLICT DO NOTHING RETURNING receipt_id`,[receiptId,oldArchiveProof,token])
requireThat(claim.rows.length===1&&claim.rows[0].receipt_id===receiptId)
})
// No destructive transaction after an unacknowledged claim. A claim is
// never cleared, even when a later checkout/hold/COMMIT is inconclusive.
return transaction(deadlineAt,async sql=>{
const claimed=await sql.query(`SELECT receipt_id FROM public.server_poll_retirement_claims
WHERE receipt_id=$1 AND old_archive_proof=$2 AND claim_token=$3 AND claim_xid<>pg_current_xact_id()`,
[receiptId,oldArchiveProof,token])
requireThat(claimed.rows.length===1&&claimed.rows[0].receipt_id===receiptId)
const {connectionId,operationId,planId,key,archived,projection,previousBytes}=await prepare(sql,target)
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,'servers.poll',$5::bigint[],true) RETURNING operation_id`,
[operationId,planId,connectionId,operationIdempotencyDigest(connectionId,key),projection.history.affectedServerIds])
requireThat(tombstone.rows.length===1 && tombstone.rows[0].operation_id===operationId)
await sql.query("SELECT set_config('mm.poll_retire_receipt',$1,true),set_config('mm.poll_retire_claim',$2,true)",[receiptId,token])
const updated = await sql.query(`UPDATE public.server_poll_archives SET version=2,source_digest=$3,source_sha256=$4,
decoded_bytes=$5,compressed_bytes=$6,ciphertext=$7,ciphertext_bytes=$8,archive_proof=$9
WHERE receipt_id=$1 AND version=1 AND archive_proof=$2 RETURNING receipt_id`,
[receiptId,oldArchiveProof,archived.source_digest,archived.source_sha256,archived.decoded_bytes,archived.compressed_bytes,
archived.ciphertext,archived.ciphertext_bytes,archived.archive_proof])
requireThat(updated.rows.length===1 && updated.rows[0].receipt_id===receiptId)
const charged = await sql.query(`UPDATE public.server_poll_storage_admissions SET archive_bytes=$2
WHERE receipt_id=$1 AND storage_state='compacted' AND archive_bytes=$3 RETURNING receipt_id`,
[receiptId,archived.ciphertext_bytes,previousBytes])
requireThat(charged.rows.length===1 && charged.rows[0].receipt_id===receiptId)
const readback = (await sql.query(SERVER_POLL_ARCHIVE_READ_SQL,[receiptId])).rows
requireThat(readback.length===1 && readback[0].archive_proof===archived.archive_proof
&& fingerprint(await openServerPollRetiredArchive(readback[0]))===fingerprint(projection))
const step = await sql.query("DELETE FROM public.mcp_operation_steps WHERE operation_id=$1 AND step_index=0 RETURNING operation_id",[operationId])
requireThat(step.rows.length===1 && step.rows[0].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",
[operationId,connectionId,planId])
requireThat(operation.rows.length===1 && operation.rows[0].id===operationId)
const plan = await sql.query("DELETE FROM public.mcp_plans WHERE id=$1 AND connection_id=$2 RETURNING id",[planId,connectionId])
requireThat(plan.rows.length===1 && plan.rows[0].id===planId)
await assertServerPollArchiveAccounting(sql)
return { retired:true as const, automaticReplay:false as const }
})
}
/** Exact internal entry used by owned fixtures. It cannot bypass the claim. */
export function retireCompletedServerPollOperation(raw:Target) {
const target=request.parse(raw)
return sharedDatabaseGate.request(()=>retire(target,Date.now()+20000))
}
let cursor:{finishedAt:string;receiptId:string}|undefined
export function retireCompletedServerPollOperations() {
return sharedDatabaseGate.request(async()=>{
const deadlineAt=Date.now()+20000
const rows=await transaction(deadlineAt,async sql=>(await sql.query(CANDIDATES,
[cursor?.finishedAt??null,cursor?.receiptId??null])).rows)
requireThat(rows.length<=8)
let retired=0,scanned=0
for(const row of rows){
if(Date.now()>=deadlineAt-2000||retired>=2)break
const target=request.parse({receiptId:row.receipt_id,oldArchiveProof:row.archive_proof})
requireThat(typeof row.finished_at==="string"&&Number.isFinite(Date.parse(row.finished_at)))
cursor={finishedAt:row.finished_at,receiptId:target.receiptId}
scanned++
try{await retire(target,deadlineAt);retired++}
catch(error){if(!(error instanceof PollStorageRefused))throw error}
}
// Keep the cursor for any unvisited rows. On exhaustion the next tick
// starts at the beginning, revisiting formerly held or busy candidates.
if(scanned===rows.length&&rows.length<8)cursor=undefined
return {scanned,retired,automaticReplay:false as const}
})
}
@@ -0,0 +1,125 @@
import assert from "node:assert/strict"
import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "node:crypto"
import { test } from "node:test"
import { fingerprint } from "../modules/mcp/security.js"
import { createServerPollRetiredArchiveForTests } from "./server-poll-retired-archive.js"
import { createServerPollTerminalArchiveForTests, ServerPollArchiveRefused } from "./server-poll-terminal-archive.js"
import type { VerifiedServerPollHistory } from "./server-poll-history-verification.js"
const receiptId = "11111111-1111-4111-8111-111111111111"
const executionId = "22222222-2222-4222-8222-222222222222"
const connectionId = "33333333-3333-4333-8333-333333333333"
const planId = "44444444-4444-4444-8444-444444444444"
const ownerId = "owned-retired-archive"
const secret = "ORIGINAL-CREDENTIAL-OR-PAYLOAD-MUST-DISAPPEAR"
const expiredAt = "2026-10-29T03:20:00.123456Z"
const terminalAt = "2026-10-29T03:21:00.123457Z"
const completedAt = "2026-10-29T03:18:00.123458Z"
const createdAt = "2026-10-29T03:17:00.123459Z"
const gcNotBefore = "2026-11-29T03:20:00.123460Z"
const now = Date.parse("2026-11-30T00:00:00Z")
const key = randomBytes(32), mac = randomBytes(32)
const proof = async (purpose: string, value: unknown) => createHmac("sha256", mac).update(purpose).update(fingerprint(value)).digest("hex")
const encrypt = async (plain: Buffer) => {
const iv = randomBytes(12), cipher = createCipheriv("aes-256-gcm", key, iv)
const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()])
return Buffer.concat([iv, cipher.getAuthTag(), ciphertext])
}
const decrypt = async (bytes: Buffer) => {
const cipher = createDecipheriv("aes-256-gcm", key, bytes.subarray(0, 12))
cipher.setAuthTag(bytes.subarray(12, 28))
return Buffer.concat([cipher.update(bytes.subarray(28)), cipher.final()])
}
async function fixture(v2Encrypt = encrypt) {
const admission = { storage_state: "full", gc_not_before: gcNotBefore, opaque: secret }
const terminal = { operation: { finished_at: terminalAt, result: secret }, plan: { id: planId, expires_at: expiredAt, evidence: secret },
step: { intended_state: secret, result: secret } }
const journal = { execution: { created_at: createdAt, context_ciphertext: secret }, receipt: { created_at: completedAt, receipt_ciphertext: secret },
intents: [{ opaque: randomBytes(2048).toString("hex") }], acks: [{ opaque: secret }], quarantines: [] }
// Tests use genuine V1 codec authentication with a controlled source callback.
// Full production V1 source parity is tested by its existing verifier suite.
const v1 = createServerPollTerminalArchiveForTests({ encrypt, decrypt, proof, verifySource: async () => ({
receiptId, executionId, connectionId, ownerId, bindingProof: "a".repeat(64), contextProof: "b".repeat(64), terminalDigest: "c".repeat(64),
originalActivityAt: terminalAt, originalExpiresAt: expiredAt, retentionDays: 30, safetyMs: 60000,
}) })
const archive = await v1.create(JSON.stringify({ admission, terminal, journal }), now)
assert(archive)
const history = {
context: { identity: { kind: "mcp", executionId, connectionId, ownerId, credentialId: secret },
plan: { request: { receiptId, serverId: 23 }, affectedServerIds: [12, 23], maintenance: [secret] } },
receipt: { receiptId, complete: true, maintenanceComplete: true, configurationVerified: true, observation: "online",
nativeReads: 4, confirmedPublications: 8, completedPublications: 8, snapshot: { identityName: secret } },
source: { createdAt, completedAt, stageCount: 12, publications: 8,
nativeOutcomes: Array.from({ length: 4 }, (_, index) => ({ stage: "native." + index, outcome: "confirmed", resultProof: "d".repeat(64) })) },
} as unknown as VerifiedServerPollHistory
const calls = { decrypt: 0, history: 0 }
const codec = createServerPollRetiredArchiveForTests({ openV1: v1.open, verifyHistory: async () => { calls.history++; return history },
encrypt: v2Encrypt, proof, decrypt: async bytes => { calls.decrypt++; return decrypt(bytes) } })
return { codec, history, calls, input: { archive, terminal, admission: { ...admission, storage_state: "compacted",
archive_bytes: archive.ciphertext_bytes, original_activity_at: terminalAt, compact_at: "2026-10-29T03:23:00.123456Z" } } }
}
const refusal = (work: Promise<unknown>) => assert.rejects(work, (error: unknown) => {
assert(error instanceof ServerPollArchiveRefused && error.automaticReplay === false)
assert(!String(error).includes(secret))
return true
})
test("retirement authenticates V1 and projects only completed history, scope, and original lifetime", async () => {
const { codec, calls, input } = await fixture()
const retired = await codec.project(input, now)
assert(retired)
assert.equal(calls.history, 1)
assert(retired.ciphertext_bytes < input.archive.ciphertext_bytes)
const { history, metadata } = await codec.open(retired)
assert.deepEqual(history, { version: 2, operation: "servers.poll", operationVersion: 1, planId, serverId: 23, affectedServerIds: [12, 23],
gcNotBefore, terminalAt, previous: { archiveProof: input.archive.archive_proof, sourceDigest: input.archive.source_digest, sourceSha256: input.archive.source_sha256 },
history: { createdAt, completedAt, stageCount: 12, publications: 8, nativeReads: 4, observation: "online", verification: "passed" } })
assert.equal(metadata.terminal_digest, input.archive.terminal_digest)
assert.equal(metadata.original_activity_at, terminalAt)
assert.equal(metadata.original_expires_at, expiredAt)
assert.equal(metadata.retention_days, input.archive.retention_days)
assert(!JSON.stringify(history).includes(secret))
assert.equal((await codec.verifyCharge(retired)).bytes, retired.ciphertext_bytes + 32768)
})
test("retirement refuses unauthenticated V1 and any current admission or original terminal drift", async () => {
const { codec, input, calls } = await fixture()
const changed = [
{ ...input, archive: { ...input.archive, archive_proof: "e".repeat(64) } },
{ ...input, admission: { ...input.admission, gc_not_before: terminalAt } },
{ ...input, admission: { ...input.admission, original_activity_at: "2026-10-29T03:21:00.123458Z" } },
{ ...input, admission: { ...input.admission, archive_bytes: 1 } },
{ ...input, terminal: { ...input.terminal, step: { result: "changed" } } },
]
for (const row of changed) await refusal(codec.project(row, now))
assert.equal(calls.history, 0)
})
test("V2 metadata or ciphertext tamper never reaches V2 decrypt", async () => {
const { codec, input, calls } = await fixture()
const retired = await codec.project(input, now)
assert(retired)
for (const row of [
{ ...retired, version: 1 }, { ...retired, owner_id: "another-owner" },
{ ...retired, original_expires_at: "2026-10-29T03:20:00.123457Z" },
{ ...retired, terminal_digest: "e".repeat(64) },
{ ...retired, ciphertext: (retired.ciphertext[0] === "A" ? "B" : "A") + retired.ciphertext.slice(1) },
]) await refusal(codec.open(row))
assert.equal(calls.decrypt, 0)
await refusal(codec.verifyCharge(input.archive))
await refusal(codec.verifyCharge({ ...retired, ciphertext_bytes: retired.ciphertext_bytes - 1 }))
})
test("retirement refuses unknown history or a scope that omits its original target", async () => {
const { codec, history, input } = await fixture()
history.source.nativeOutcomes[0].outcome = "read-failed"
await refusal(codec.project(input, now))
history.source.nativeOutcomes[0].outcome = "confirmed"
history.context.plan.affectedServerIds = [12]
await refusal(codec.project(input, now))
})
test("a larger V2 ciphertext keeps the authenticated V1 instead of increasing the charge", async () => {
const { codec, input } = await fixture(async value => encrypt(Buffer.concat([Buffer.alloc(4096), value])))
assert.equal(await codec.project(input, now), undefined)
})
@@ -0,0 +1,168 @@
/** Private retirement projection. Publication still requires the SQL owner's
* locks, exact compare-and-swap, dependency checks and atomic quota adjustment. */
import { createHash, timingSafeEqual } from "node:crypto"
import { gzipSync, gunzipSync } from "node:zlib"
import { z } from "zod"
import { canonicalPollArchiveTimestamp, openServerPollTerminalArchive, ServerPollArchiveRefused } from "./server-poll-terminal-archive.js"
import { verifyServerPollHistory } from "./server-poll-history-verification.js"
import { decryptBinary, encryptBinary, fingerprint, privateSecretFingerprint } from "../modules/mcp/security.js"
const MAX_PAYLOAD_BYTES = 24576
const MAX_COMPRESSED_BYTES = MAX_PAYLOAD_BYTES
const MAX_CIPHERTEXT_BYTES = 33000
const RECORD_PURPOSE = "server-poll-retired-archive-record-v2"
const SOURCE_PURPOSE = "server-poll-retired-archive-source-v2"
const hash = z.string().regex(/^[a-f0-9]{64}$/), uuid = z.string().uuid()
const integer = z.number().int().positive().safe()
const owner = z.string().min(1).max(200).refine(value => value.trim() === value && !/[\x00-\x1f\x7f]/.test(value))
const utc = z.string().transform(canonicalPollArchiveTimestamp)
const metadataSchema = z.object({
version: z.literal(2), receipt_id: uuid, execution_id: uuid, connection_id: uuid, owner_id: owner,
binding_proof: hash, context_proof: hash, terminal_digest: hash,
original_activity_at: utc, original_expires_at: utc, retention_days: integer.max(365), safety_ms: z.literal(60000),
source_digest: hash, source_sha256: hash, decoded_bytes: integer.max(MAX_PAYLOAD_BYTES),
compressed_bytes: integer.max(MAX_COMPRESSED_BYTES), ciphertext_bytes: integer.max(MAX_CIPHERTEXT_BYTES), ciphertext_sha256: hash,
})
const projectionSchema = z.object({
version: z.literal(2), operation: z.literal("servers.poll"), operationVersion: z.literal(1),
planId: uuid, serverId: integer, affectedServerIds: z.array(integer).min(1).max(1000),
gcNotBefore: utc, terminalAt: utc,
previous: z.object({ archiveProof: hash, sourceDigest: hash, sourceSha256: hash }).strict(),
history: z.object({
createdAt: utc, completedAt: utc, stageCount: integer.max(2008),
publications: integer.max(2008), nativeReads: z.literal(4),
observation: z.literal("online"), verification: z.literal("passed"),
}).strict(),
}).strict().refine(value => value.affectedServerIds.includes(value.serverId)
&& value.affectedServerIds.length === new Set(value.affectedServerIds).size
&& value.history.createdAt <= value.history.completedAt
&& value.history.completedAt <= value.terminalAt)
export type ServerPollRetiredArchiveMetadata = z.infer<typeof metadataSchema>
export type ServerPollRetiredHistory = z.infer<typeof projectionSchema>
export type ServerPollRetiredArchive = ServerPollRetiredArchiveMetadata & { ciphertext: string; archive_proof: string }
export interface LockedServerPollRetirementSource {
/** Existing V1 reader projection, including retention and actual ciphertext digest. */
archive: Record<string, unknown>
/** Current compacted admission JSON and exact operation, plan and step JSON. */
admission: Record<string, unknown>
terminal: { operation: Record<string, unknown>; plan: Record<string, unknown>; step: Record<string, unknown> }
}
interface Dependencies {
openV1: typeof openServerPollTerminalArchive
verifyHistory: typeof verifyServerPollHistory
encrypt(value: Buffer): Promise<Buffer>
decrypt(value: Buffer): Promise<Buffer>
proof(purpose: string, value: unknown): Promise<string>
}
const sha = (value: Buffer | string) => createHash("sha256").update(value).digest("hex")
const equal = (left: string, right: string) => timingSafeEqual(Buffer.from(hash.parse(left), "hex"), Buffer.from(hash.parse(right), "hex"))
const refuse = (): never => { throw new ServerPollArchiveRefused() }
const requireThat: (value: unknown) => asserts value = value => { if (!value) refuse() }
function immutableAdmission(raw: Record<string, unknown>) {
const result = { ...raw }
for (const name of ["storage_state", "compact_at", "archive_bytes", "original_activity_at"]) delete result[name]
return result
}
function codec(deps: Dependencies) {
const proof = async (purpose: string, value: unknown) => hash.parse(await deps.proof(purpose, value))
const recordProof = (metadata: ServerPollRetiredArchiveMetadata) => proof(RECORD_PURPOSE, {
...metadata, identity_kind: "mcp", storage_state: "compacted", admission_bytes: 32768, charged_rows: 2,
})
async function readMetadata(raw: Record<string, unknown>) {
const metadata = metadataSchema.parse(raw)
requireThat(equal(hash.parse(raw.archive_proof), await recordProof(metadata)))
return metadata
}
async function project(raw: LockedServerPollRetirementSource, now: number): Promise<ServerPollRetiredArchive | undefined> {
try {
requireThat(Number.isSafeInteger(now) && now > 0)
const input = structuredClone(raw)
const opened = await deps.openV1(input.archive, now)
const { source, metadata: previous } = opened
const current = input.admission
requireThat(source.admission.storage_state === "full" && current.storage_state === "compacted"
&& current.archive_bytes === previous.ciphertext_bytes
&& canonicalPollArchiveTimestamp(current.original_activity_at) === previous.original_activity_at
&& fingerprint(immutableAdmission(source.admission)) === fingerprint(immutableAdmission(current))
&& fingerprint(source.terminal) === fingerprint(input.terminal))
canonicalPollArchiveTimestamp(current.compact_at)
// The existing codec authenticates the entire original. The shared
// verifier supplies its typed history rather than reading opaque fields
// from unverified context or preserving original authority in V2.
const { context, receipt, source: journal } = await deps.verifyHistory(source.journal)
requireThat(context.identity.kind === "mcp" && context.identity.executionId === previous.execution_id
&& context.identity.connectionId === previous.connection_id && context.identity.ownerId === previous.owner_id
&& context.plan.request.receiptId === previous.receipt_id && receipt.receiptId === previous.receipt_id
&& receipt.complete && receipt.maintenanceComplete && receipt.configurationVerified && receipt.observation === "online"
&& receipt.confirmedPublications === receipt.completedPublications && journal.publications === receipt.completedPublications
&& journal.nativeOutcomes.length === 4 && journal.nativeOutcomes.every(item => item.outcome === "confirmed"))
const terminalAt = [source.terminal.operation.finished_at, source.terminal.plan.expires_at].map(canonicalPollArchiveTimestamp).sort()[1]
const projection = projectionSchema.parse({
version: 2, operation: "servers.poll", operationVersion: 1, planId: source.terminal.plan.id,
serverId: context.plan.request.serverId, affectedServerIds: context.plan.affectedServerIds,
gcNotBefore: source.admission.gc_not_before, terminalAt,
previous: { archiveProof: input.archive.archive_proof, sourceDigest: previous.source_digest, sourceSha256: previous.source_sha256 },
history: { createdAt: source.journal.execution.created_at, completedAt: source.journal.receipt.created_at,
stageCount: journal.stageCount, publications: journal.publications, nativeReads: receipt.nativeReads,
observation: receipt.observation, verification: "passed" },
})
requireThat(projection.terminalAt <= previous.original_activity_at && previous.original_expires_at <= projection.gcNotBefore)
const plaintext = Buffer.from(JSON.stringify(projection))
requireThat(plaintext.length <= MAX_PAYLOAD_BYTES)
const compressed = gzipSync(plaintext, { level: 6, maxOutputLength: MAX_COMPRESSED_BYTES })
const ciphertext = (await deps.encrypt(compressed)).toString("base64")
const bytes = Buffer.byteLength(ciphertext)
if (bytes > previous.ciphertext_bytes || bytes > MAX_CIPHERTEXT_BYTES) return undefined
const sourceSha = sha(plaintext)
const metadata = metadataSchema.parse({ ...previous, version: 2, source_sha256: sourceSha, source_digest: await proof(SOURCE_PURPOSE, sourceSha),
decoded_bytes: plaintext.length, compressed_bytes: compressed.length, ciphertext_bytes: bytes, ciphertext_sha256: sha(ciphertext) })
return { ...metadata, ciphertext, archive_proof: await recordProof(metadata) }
} catch { return refuse() }
}
async function verifyCharge(raw: Record<string, unknown>) {
try {
const metadata = await readMetadata(structuredClone(raw))
return { bytes: metadata.ciphertext_bytes + 32768, rows: 2 as const, metadata }
} catch { return refuse() }
}
async function open(raw: Record<string, unknown>) {
try {
const input = structuredClone(raw)
const metadata = await readMetadata(input), ciphertext = input.ciphertext
requireThat(typeof ciphertext === "string" && Buffer.byteLength(ciphertext) === metadata.ciphertext_bytes
&& sha(ciphertext) === metadata.ciphertext_sha256)
const binary = Buffer.from(ciphertext, "base64")
requireThat(binary.toString("base64") === ciphertext)
const compressed = await deps.decrypt(binary)
requireThat(compressed.length === metadata.compressed_bytes)
const plaintext = gunzipSync(compressed, { maxOutputLength: MAX_PAYLOAD_BYTES })
const text = plaintext.toString("utf8")
requireThat(plaintext.length === metadata.decoded_bytes && Buffer.from(text).equals(plaintext)
&& sha(plaintext) === metadata.source_sha256 && equal(metadata.source_digest, await proof(SOURCE_PURPOSE, metadata.source_sha256)))
const history = projectionSchema.parse(JSON.parse(text))
requireThat(history.terminalAt <= metadata.original_activity_at && metadata.original_expires_at <= history.gcNotBefore)
return { metadata, history }
} catch { return refuse() }
}
return Object.freeze({ project, verifyCharge, open })
}
const production = codec({
openV1: openServerPollTerminalArchive, verifyHistory: verifyServerPollHistory,
encrypt: encryptBinary, decrypt: decryptBinary,
proof: (purpose, value) => privateSecretFingerprint(fingerprint(value), purpose),
})
export const projectServerPollRetiredArchive = production.project
export const verifyServerPollRetiredArchiveCharge = production.verifyCharge
export const openServerPollRetiredArchive = production.open
/** Isolated component crypto composition. No public/runtime caller selects it. */
export const createServerPollRetiredArchiveForTests = (deps: Dependencies) => codec(Object.freeze({ ...deps }))
@@ -34,8 +34,40 @@ try{
}finally{await other.end();await pool.query(`DROP DATABASE "${name}"`)}
const {tickMcpWorkers}=await import("../modules/mcp/workers.js")
assert.equal((await pool.query("SELECT enabled FROM mcp_settings WHERE id=1")).rows[0].enabled,false)
const expiredRequestIds = Array.from({ length: 1001 }, () => randomUUID()).sort()
const [runningId, freshId] = [randomUUID(), randomUUID()].sort()
await pool.query(`INSERT INTO mcp_requests(id,tool,status,created_at)
SELECT id,'owned-retention-test','completed',now()-interval '31 days' FROM unnest($1::uuid[]) AS ids(id)
UNION ALL SELECT $2::uuid,'owned-retention-test','running',now()-interval '32 days'
UNION ALL SELECT $3::uuid,'owned-retention-test','completed',now()-interval '29 days'`, [expiredRequestIds, runningId, freshId])
const expiredAudit = (await pool.query(`INSERT INTO mcp_audit(actor_id,action,created_at)
SELECT 'owned-retention-test','expired',now()-interval '181 days' FROM generate_series(1,1001)
RETURNING id`)).rows.map(row => String(row.id))
const lastExpiredAuditId = expiredAudit.reduce((last, id) => BigInt(id) > BigInt(last) ? id : last)
const freshAuditId = (await pool.query(`INSERT INTO mcp_audit(actor_id,action,created_at)
VALUES ('owned-retention-test','fresh',now()-interval '179 days') RETURNING id`)).rows[0].id
const artifactConnectionId = randomUUID()
const expiredArtifactIds = Array.from({ length: 1001 }, () => randomUUID()).sort()
const freshArtifactId = randomUUID()
await pool.query(`INSERT INTO mcp_connections(id,name,owner_id,owner_permissions,policy,generation)
VALUES ($1,'owned-retention-test','owned-retention-test','[]','{}','owned-retention-test')`, [artifactConnectionId])
await pool.query(`INSERT INTO mcp_artifacts(id,connection_id,filename,media_type,ciphertext,expires_at)
SELECT id,$1::uuid,'test','application/octet-stream','test',now()-interval '1 day' FROM unnest($2::uuid[]) AS ids(id)
UNION ALL SELECT $3::uuid,$1::uuid,'test','application/octet-stream','test',now()+interval '1 day'`, [artifactConnectionId, expiredArtifactIds, freshArtifactId])
const requestIds = async () => (await pool.query("SELECT id FROM mcp_requests WHERE tool='owned-retention-test' ORDER BY id")).rows.map(row => row.id)
const auditIds = async () => (await pool.query("SELECT id FROM mcp_audit WHERE actor_id='owned-retention-test' ORDER BY id")).rows.map(row => String(row.id))
const artifactIds = async () => (await pool.query("SELECT id FROM mcp_artifacts WHERE connection_id=$1 ORDER BY id", [artifactConnectionId])).rows.map(row => row.id)
await tickMcpWorkers()
console.log("Owned fresh poll storage:",JSON.stringify({normalStartupTwice:true,migrations:MIGRATION_MANIFEST.length,globalZeroSeed:true,legacyPrefix:38,nonemptyJournalRefusedBeforeDdl:true,oldRowsPreserved:true,noForgedMarker:true,actualWorkerTickWithMcpDisabled:true}))
assert.deepEqual(await requestIds(), [...expiredRequestIds.slice(-1), runningId, freshId].sort())
assert.deepEqual(await auditIds(), [lastExpiredAuditId, String(freshAuditId)])
assert.deepEqual(await artifactIds(), [...expiredArtifactIds.slice(-1), freshArtifactId].sort())
await tickMcpWorkers()
assert.deepEqual(await requestIds(), [runningId, freshId].sort())
assert.deepEqual(await auditIds(), [String(freshAuditId)])
assert.deepEqual(await artifactIds(), [freshArtifactId])
await pool.query("DELETE FROM mcp_artifacts WHERE id=$1", [freshArtifactId])
await pool.query("DELETE FROM mcp_connections WHERE id=$1", [artifactConnectionId])
console.log("Owned fresh poll storage:",JSON.stringify({normalStartupTwice:true,migrations:MIGRATION_MANIFEST.length,globalZeroSeed:true,legacyPrefix:38,nonemptyJournalRefusedBeforeDdl:true,oldRowsPreserved:true,noForgedMarker:true,actualWorkerTickWithMcpDisabled:true,boundedRequestAuditAndArtifactRetention:true}))
if (process.env.MMAPP_OWN_SERVER_POLL_SETTINGS_EPOCH === "true") {
await (await import("../db/server-poll-settings-epoch.owned-helper.js")).acceptServerPollSettingsEpochMigrationSql(pool)
} else if (process.env.MMAPP_OWN_SERVER_POLL_ATOMIC_MAINTENANCE === "true") {
@@ -1,6 +1,8 @@
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { createHash, createHmac, randomUUID } from "node:crypto"
import { test } from "node:test"
import { gzipSync } from "node:zlib"
import { fingerprint } from "../modules/mcp/security.js"
import { collectExpiredServerPollStorageForTests } from "./server-poll-storage-retention.js"
import { PollStorageRefused, PollStorageUnknown } from "./server-poll-storage.js"
import type { ProbeStorageOptions } from "./server-probe-journal-maintenance.js"
@@ -8,8 +10,10 @@ import { sharedDatabaseGate } from "./shared-database-gate.js"
const tables = ["server_poll_acks", "server_poll_intents", "server_poll_receipts", "server_poll_quarantines", "server_poll_executions", "server_poll_archives", "server_poll_storage_admissions"]
const deferred = () => { let resolve!: () => void; const promise = new Promise<void>(done => { resolve = done }); return { promise, resolve } }
function fixture() {
const now = Date.now(), candidates: Record<string, unknown>[] = [{ receipt_id: randomUUID(), owner_id: "owned-gc" }, { receipt_id: randomUUID(), owner_id: "owned-gc" }]
function fixture(openRetired?: Parameters<typeof collectExpiredServerPollStorageForTests>[1]) {
const now = Date.now(), candidates: Record<string, unknown>[] = [{ receipt_id: randomUUID(), owner_id: "owned-gc", archive_version: null },
{ receipt_id: randomUUID(), owner_id: "owned-gc", archive_version: 1 }]
const retiredRows: Record<string, unknown>[] = []
const commands: { text: string; values: unknown[]; timeout: number }[] = [], releases: boolean[] = [], options: ProbeStorageOptions[] = []
let settings: Record<string, unknown>[] = [{ requests_retention_days: 30 }], fail = "", residual = "", removed: Record<string, unknown>[] | undefined, hold: ReturnType<typeof deferred> | undefined
const entered = deferred()
@@ -25,12 +29,13 @@ function fixture() {
return { rows: [{ scope: "global", admissions: 0, compacted_admissions: 0, archive_bytes: 0, reserved_rows: 0, reserved_bytes: 0 }] }
if (text.includes("requests_retention_days")) { entered.resolve(); if (hold) await hold.promise; return { rows: settings } }
if (text.startsWith("SELECT receipt_id,owner_id")) return { rows: structuredClone(candidates) }
if (text.startsWith("SELECT r.version,r.receipt_id")) return { rows: structuredClone(retiredRows) }
if (text.startsWith("DELETE FROM server_poll_storage_admissions")) return { rows: removed ?? candidates.map(row => ({ receipt_id: row.receipt_id })) }
if (text.startsWith("SELECT receipt_id FROM ") && text.includes("FROM " + residual + " ")) return { rows: [{ receipt_id: candidates[0]?.receipt_id }] }
return { rows: [] }
}, release(unsafe = false) { releases.push(unsafe) } }
} })
return { run, now, candidates, commands, releases, options, entered: entered.promise, block: () => { hold = deferred(); return hold },
} }, openRetired)
return { run, now, candidates, retiredRows, commands, releases, options, entered: entered.promise, block: () => { hold = deferred(); return hold },
setting: (rows: Record<string, unknown>[]) => { settings = rows }, fail: (text: string) => { fail = text }, residual: (table: string) => { residual = table }, removed: (rows: Record<string, unknown>[]) => { removed = rows } }
}
@@ -61,6 +66,67 @@ test("selected GC owns one bounded backend, locks current retention, deletes all
assert.deepEqual(f.releases, [false])
})
async function retiredFixture() {
const { createServerPollRetiredArchiveForTests } = await import("./server-poll-retired-archive.js")
const proof = async (purpose: string, value: unknown) =>
createHmac("sha256", "gc-fixture-only").update(purpose).update(fingerprint(value)).digest("hex")
const sha = (value: string | Buffer) => createHash("sha256").update(value).digest("hex")
let opened = 0
const codec = createServerPollRetiredArchiveForTests({
proof,
decrypt: async value => { opened++; return value },
encrypt: async () => { throw new Error("Unexpected archive write") },
openV1: async () => { throw new Error("Unexpected archive fallback") },
verifyHistory: async () => { throw new Error("Unexpected historical fallback") },
})
const f = fixture(codec.open)
const selected = f.candidates[1]
selected.archive_version = 2
const timestamp = "2026-10-01T00:00:00.123456Z", gcNotBefore = "2026-11-01T00:00:00.123456Z"
const history = { version: 2, operation: "servers.poll", operationVersion: 1, planId: randomUUID(), serverId: 3,
affectedServerIds: [3], gcNotBefore, terminalAt: timestamp,
previous: { archiveProof: "1".repeat(64), sourceDigest: "2".repeat(64), sourceSha256: "3".repeat(64) },
history: { createdAt: timestamp, completedAt: timestamp, stageCount: 12, publications: 8,
nativeReads: 4, observation: "online", verification: "passed" } }
const plaintext = Buffer.from(JSON.stringify(history)), compressed = gzipSync(plaintext), ciphertext = compressed.toString("base64")
const metadata = { version: 2, receipt_id: selected.receipt_id, execution_id: randomUUID(), connection_id: randomUUID(), owner_id: selected.owner_id,
binding_proof: "a".repeat(64), context_proof: "b".repeat(64), terminal_digest: "c".repeat(64),
original_activity_at: timestamp, original_expires_at: timestamp, retention_days: 30, safety_ms: 60000,
source_digest: await proof("server-poll-retired-archive-source-v2", sha(plaintext)), source_sha256: sha(plaintext),
decoded_bytes: plaintext.length, compressed_bytes: compressed.length, ciphertext_bytes: ciphertext.length, ciphertext_sha256: sha(ciphertext) }
const archiveProof = await proof("server-poll-retired-archive-record-v2", { ...metadata, identity_kind: "mcp",
storage_state: "compacted", admission_bytes: 32768, charged_rows: 2 })
f.retiredRows.push({ ...metadata, ciphertext, archive_proof: archiveProof, admission_gc_not_before: gcNotBefore })
return { ...f, opened: () => opened }
}
test("GC opens only selected V2 under a row lock and authenticates the original lifetime before any DELETE", async () => {
const f = await retiredFixture()
assert.deepEqual(await f.run(), { collected: 2, automaticReplay: false })
assert.equal(f.opened(), 1)
const index = f.commands.findIndex(command => command.text.startsWith("SELECT r.version,r.receipt_id"))
assert.deepEqual(f.commands[index].values, [f.candidates[1].receipt_id])
assert(f.commands[index].text.endsWith("FOR UPDATE OF r"))
assert(index < f.commands.findIndex(command => command.text.startsWith("DELETE")))
})
test("missing V2, forged ciphertext or proof, and microsecond lifetime drift refuse before any DELETE", async () => {
for (const fault of ["missing", "ciphertext", "actual-hash", "proof", "lifetime", "expiry", "version"] as const) {
const f = await retiredFixture(), row = f.retiredRows[0]
if (fault === "missing") f.retiredRows.length = 0
if (fault === "ciphertext") row.ciphertext = "A" + String(row.ciphertext).slice(1)
if (fault === "actual-hash") row.ciphertext_sha256 = "a".repeat(64)
if (fault === "proof") row.archive_proof = "a".repeat(64)
if (fault === "lifetime") row.admission_gc_not_before = "2026-11-01T00:00:00.123455Z"
if (fault === "expiry") row.original_expires_at = "2026-10-01T00:00:00.123455Z"
if (fault === "version") f.candidates[1].archive_version = 3
await assert.rejects(f.run(), PollStorageRefused)
assert.equal(f.opened(), fault === "lifetime" ? 1 : 0)
assert(!f.commands.some(command => command.text.startsWith("DELETE") || command.text === "COMMIT"))
assert.equal(f.commands.filter(command => command.text === "ROLLBACK").length, 1)
}
})
test("empty candidate set returns zero without deleting or refunding", async () => {
const f = fixture(); f.candidates.length = 0
assert.deepEqual(await f.run(), { collected: 0, automaticReplay: false })
@@ -4,21 +4,50 @@ import { connectProbeStorage } from "./server-probe-storage-connection.js"
import { runPollStorageTransaction, type PollStorageDependencies } from "./server-poll-storage.js"
import { PollStorageRefused } from "./server-poll-storage-model.js"
import { assertServerPollArchiveAccounting } from "./server-poll-archive-accounting.js"
import type { openServerPollRetiredArchive } from "./server-poll-retired-archive.js"
const uuid=/^[a-f\d]{8}(?:-[a-f\d]{4}){3}-[a-f\d]{12}$/i
const fail=():never=>{throw new PollStorageRefused()}
const production:PollStorageDependencies={connect:connectProbeStorage,now:Date.now}
async function collect(deps:PollStorageDependencies){
const openRetiredArchive:typeof openServerPollRetiredArchive=async row=>
(await import("./server-poll-retired-archive.js")).openServerPollRetiredArchive(row)
const retiredCandidate = `SELECT r.version,r.receipt_id,r.execution_id,r.connection_id,r.owner_id,
r.binding_proof,r.context_proof,r.terminal_digest,r.source_digest,r.source_sha256,
r.decoded_bytes,r.compressed_bytes,r.ciphertext_bytes,r.ciphertext,r.archive_proof,a.retention_days,a.safety_ms,
to_char(r.original_activity_at AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS original_activity_at,
to_char(r.original_expires_at AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS original_expires_at,
to_char(a.gc_not_before AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS admission_gc_not_before,
pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(r.ciphertext,'UTF8')),'hex') AS ciphertext_sha256
FROM server_poll_archives r JOIN server_poll_storage_admissions a USING(receipt_id)
WHERE r.receipt_id=$1 AND r.version=2 AND a.storage_state='compacted' AND a.identity_kind='mcp'
AND r.execution_id=a.execution_id AND r.connection_id=a.connection_id AND r.owner_id=a.owner_id
AND r.binding_proof=a.binding_proof AND r.context_proof=a.context_proof
AND r.original_activity_at=a.original_activity_at AND r.original_expires_at=a.authority_expires_at
AND r.ciphertext_bytes=a.archive_bytes AND r.ciphertext_bytes=octet_length(r.ciphertext)
FOR UPDATE OF r`
async function collect(deps:PollStorageDependencies, openRetired:typeof openServerPollRetiredArchive){
return runPollStorageTransaction(deps,{deadlineAt:deps.now()+20000},async()=>{},async sql=>{
// Hold the authoritative setting against concurrent updates until COMMIT.
// Each DELETE guard additionally checks its current value independently.
const setting=(await sql.query("SELECT requests_retention_days FROM mcp_settings WHERE id=1 FOR SHARE")).rows
if(setting.length!==1||!Number.isSafeInteger(setting[0].requests_retention_days)||Number(setting[0].requests_retention_days)<1||Number(setting[0].requests_retention_days)>365)fail()
await sql.query("SET LOCAL mm.poll_storage_gc='on'")
const rows=(await sql.query("SELECT receipt_id,owner_id FROM server_poll_storage_admissions WHERE gc_not_before<clock_timestamp() AND server_poll_storage_gc_allowed(receipt_id) ORDER BY gc_not_before,receipt_id LIMIT 8 FOR UPDATE")).rows
if(rows.length>8||rows.some(row=>typeof row.receipt_id!=="string"||!uuid.test(row.receipt_id)||typeof row.owner_id!=="string")||new Set(rows.map(row=>row.receipt_id)).size!==rows.length)fail()
const rows=(await sql.query(`SELECT receipt_id,owner_id,
(SELECT version FROM server_poll_archives WHERE receipt_id=a.receipt_id) AS archive_version
FROM server_poll_storage_admissions a WHERE gc_not_before<clock_timestamp()
AND server_poll_storage_gc_allowed(receipt_id) ORDER BY gc_not_before,receipt_id LIMIT 8 FOR UPDATE`)).rows
if(rows.length>8||rows.some(row=>typeof row.receipt_id!=="string"||!uuid.test(row.receipt_id)||typeof row.owner_id!=="string"
|| (row.archive_version!==null&&row.archive_version!==1&&row.archive_version!==2))||new Set(rows.map(row=>row.receipt_id)).size!==rows.length)fail()
if(!rows.length)return {collected:0,automaticReplay:false as const}
await assertServerPollArchiveAccounting(sql)
for(const candidate of rows){
if(candidate.archive_version!==2)continue
const locked=(await sql.query(retiredCandidate,[candidate.receipt_id])).rows
if(locked.length!==1||locked[0].version!==2||locked[0].receipt_id!==candidate.receipt_id||locked[0].owner_id!==candidate.owner_id)fail()
const {history}=await openRetired(locked[0])
if(history.gcNotBefore!==locked[0].admission_gc_not_before)fail()
}
const ids=rows.map(row=>row.receipt_id),owners=[...new Set(rows.map(row=>row.owner_id))]
// No payload-only shortcut: all dependent rows, admission and its exact
// trigger-owned refund commit together. Any missing guard/refund aborts.
@@ -35,6 +64,7 @@ async function collect(deps:PollStorageDependencies){
return {collected:ids.length,automaticReplay:false as const}
})
}
export const collectExpiredServerPollStorage=()=>sharedDatabaseGate.request(()=>collect(production))
export const collectExpiredServerPollStorage=()=>sharedDatabaseGate.request(()=>collect(production,openRetiredArchive))
/** Explicit owned SQL composition; does not start a worker. */
export const collectExpiredServerPollStorageForTests=(deps:PollStorageDependencies)=>sharedDatabaseGate.request(()=>collect(deps))
export const collectExpiredServerPollStorageForTests=(deps:PollStorageDependencies,openRetired=openRetiredArchive)=>
sharedDatabaseGate.request(()=>collect(deps,openRetired))
@@ -77,7 +77,7 @@ async function main() {
}
try {
await applySqlMigrations(pool)
assert.equal(MIGRATION_MANIFEST.at(-1)?.file, "0044_mcp_routing_manual_inverse_charges.sql")
assert.equal(MIGRATION_MANIFEST.at(-1)?.file, "0053_server_probe_terminal_proofs.sql")
assert.equal(MIGRATION_MANIFEST.find(pin=>pin.file==="0041_server_poll_terminal_archives.sql")?.sha256,"fbb8935ce977cfc507801f01beb8f303d4817ed63c6bdd4c05799768dfaac9d4")
assert.equal((await pool.query("SELECT count(*)::int AS n FROM schema_migrations")).rows[0].n, MIGRATION_MANIFEST.length)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM server_poll_storage_admissions")).rows[0].n, 0)
@@ -242,6 +242,12 @@ async function main() {
const archivedRows = (await pool.query(SERVER_POLL_ARCHIVE_READ_SQL, [receiptId])).rows
assert.equal(archivedRows.length, 1)
const archive = archivedRows[0]
const { retireCompletedServerPollOperation } = await import("./server-poll-operation-retirement.js")
const beforeRetirement = await ledger()
await assert.rejects(retireCompletedServerPollOperation({ receiptId, oldArchiveProof: archive.archive_proof }))
assert.equal(await ledger(), beforeRetirement, "Authenticated V1 younger than 180 days retains exact charge")
assert.equal((await pool.query("SELECT version FROM server_poll_archives WHERE receipt_id=$1", [receiptId])).rows[0].version, 1)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_operation_replay_tombstones WHERE operation_id=$1", [a.execution_id])).rows[0].n, 0)
assert.equal(Number(archive.actual_ciphertext_bytes), Number(archive.ciphertext_bytes))
assert(Number(archive.ciphertext_bytes) > 0 && Number(archive.ciphertext_bytes) <= SERVER_POLL_ARCHIVE_MAX_BYTES)
assert.equal(Number(archive.ciphertext_bytes), Buffer.byteLength(archive.ciphertext))
@@ -0,0 +1,213 @@
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { assertOwnedBandwidthSql, prepareOwnedBandwidthState } from "./bandwidth-admission-process.sql-helper.js"
assertOwnedBandwidthSql()
assert.equal(process.env.MMAPP_OWN_PROBE_OPERATION_RETIREMENT, "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 { authenticateMcpToken, createMcpConnection } = await import("../modules/mcp/store.js")
const { applyPlan, invokeMcpTool } = await import("../modules/mcp/runtime.js")
const { encryptSecret, McpError } = await import("../modules/mcp/security.js")
const { serverProbeRetiredFixture } = await import("./server-probe-retired-source.fixture.js")
const { captureServerProbeTerminalProof, collectExpiredRetiredServerProbe, retireCompletedServerProbeOperation,
retireExpiredServerProbeOperation } = await import("./server-probe-operation-retirement.js")
const { createProbeJournalStorage } = await import("./server-probe-journal-maintenance.js")
const { connectProbeStorage } = await import("./server-probe-storage-connection.js")
const { PROBE_STORAGE_LIMITS } = await import("../modules/mcp/diagnostic-cancellation-storage.js")
const { retainServerProbeStorage } = await import("./server-probe-retention.js")
const { tickMcpWorkers } = await import("../modules/mcp/workers.js")
try {
await applySqlMigrations(pool)
await pool.query("UPDATE mcp_settings SET enabled=true WHERE id=1")
await pool.query("INSERT INTO servers(id,name,host,enabled) VALUES(9,'owned retirement source','127.0.0.1',true)")
async function access() {
const actor = { id: randomUUID(), name: "Owned diagnostic retention", email: "[email protected]", apps: ["mm"],
permissions: ["mm:servers:read", "mm:servers:write", "mm:mcp:approve"], 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: [9],
operationIds: ["servers.probes.run", "servers.probes.receipt.read", "operations.read", "changes.apply"], allowGlobal: false },
limits: { callsPerMinute: 100, mutationsPerHour: 10 } }), actor)
return { actor, principal: await authenticateMcpToken(issued.token) }
}
async function verifyLifecycle(mode: "retained" | "purged" | "default-old" | "default-young" | "default-unproven" | "default-spent" | "scheduled-retained") {
const { actor, principal } = await access(), foreign = (await access()).principal
const historical = mode === "retained" ? undefined : {
base: Date.now()-(mode === "default-young" ? 60 : 200)*86400000, retentionDays:mode === "scheduled-retained" ? 365 : 30,
}
const { raw } = await serverProbeRetiredFixture(false, { connectionId: principal.connectionId, credentialId: principal.credentialId, ownerId: actor.id }, historical)
const { original: o, admission: a } = raw, key = randomUUID(), empty = await encryptSecret("[]")
const target = { receiptId: a.receipt_id, operationId: o.operation_id, connectionId: o.connection_id }
const request = { operationId: o.operation_id, receiptId: a.receipt_id, page: 0 }
const own = await pool.connect()
try {
await own.query("BEGIN")
// Only this disposable database gets synthetic historical SQL rows. The
// actual codec signs the historical originals with its owned state key.
await own.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,$3,1,1,'owned historical test','{}',$4,'completed',$5,$6,$7,$8)`,
[o.plan_id,o.connection_id,o.operation_name,o.evidence,o.plan_expires_at,o.owner_id,o.plan_approved_at,o.plan_created_at])
await own.query(`INSERT INTO mcp_operations(id,plan_id,connection_id,idempotency_key,state,verification,result,created_at,finished_at)
VALUES($1,$2,$3,$4,'applied','inconclusive',$5,$6,$7)`, [o.operation_id,o.plan_id,o.connection_id,key,o.operation_result,o.operation_created_at,o.operation_finished_at])
await own.query(`INSERT INTO mcp_operation_steps(operation_id,step_index,state,intended_state,result,created_at,finished_at)
VALUES($1,0,'acknowledged',$2,$3,$4,$5)`, [o.operation_id,o.intended_state,o.step_result,o.step_created_at,o.step_finished_at])
await own.query("INSERT INTO mcp_operation_checkpoints(operation_id,step_index,before_sealed,after_sealed) VALUES($1,0,$2,$2)", [o.operation_id,empty])
await own.query("SET LOCAL session_replication_role='replica'")
await own.query("INSERT INTO server_probe_storage_admissions SELECT * FROM jsonb_populate_record(NULL::server_probe_storage_admissions,$1)", [a])
await own.query("INSERT INTO server_probe_executions SELECT * FROM jsonb_populate_record(NULL::server_probe_executions,$1)", [raw.anchor])
await own.query("INSERT INTO server_probe_execution_payloads SELECT * FROM jsonb_populate_record(NULL::server_probe_execution_payloads,$1)", [raw.payload])
await own.query("INSERT INTO server_probe_execution_stages SELECT * FROM jsonb_populate_record(NULL::server_probe_execution_stages,$1)", [raw.stages[0]])
await own.query("UPDATE server_probe_storage_usage SET admissions=1,reserved_files=1,reserved_bytes=294912 WHERE scope='global'")
await own.query("INSERT INTO server_probe_storage_usage VALUES($1,1,1,294912)", ["owner:"+actor.id])
await own.query("SET LOCAL session_replication_role='origin'")
await own.query("COMMIT")
} finally { own.release(true) }
const live = await invokeMcpTool(principal, "mm_server_probe_receipt_get", request)
const storage = createProbeJournalStorage({ connect:connectProbeStorage, policy:{ ...PROBE_STORAGE_LIMITS,retentionDays:30 }, safetyWindowMs:60000,
async authorize(ownerId,operation,scope) {
assert.equal(ownerId,actor.id); assert.equal(operation,"purge"); assert.equal(scope,undefined)
return "a".repeat(64)
} })
if (mode === "default-young" || mode === "default-old" || mode === "default-unproven" || mode === "default-spent") {
await assert.rejects(retireExpiredServerProbeOperation(target))
if (mode === "default-unproven") {
// This original still qualifies for normal payload expiry, but cannot
// be authenticated as a dependency-free retirement source.
await pool.query("UPDATE mcp_operation_checkpoints SET after_sealed=$2 WHERE operation_id=$1",
[o.operation_id,await encryptSecret('["held"]')])
}
assert.deepEqual(await retainServerProbeStorage(), { skipped:false,owners:1,collected:1 })
assert.equal((await pool.query("SELECT reserved_bytes::int AS bytes FROM server_probe_storage_usage WHERE scope='global'")).rows[0].bytes,0)
assert.equal((await pool.query("SELECT 1 FROM server_probe_terminal_proofs WHERE operation_id=$1",[o.operation_id])).rows.length,
mode === "default-unproven" ? 0 : 1)
await assert.rejects(invokeMcpTool(principal,"mm_server_probe_receipt_get",request))
await assert.rejects(captureServerProbeTerminalProof(target))
await assert.rejects(retireCompletedServerProbeOperation(target))
if (mode === "default-spent") {
const spent = (await pool.query(`INSERT INTO server_probe_retirement_claims
(receipt_id,operation_id,plan_id,connection_id,source_proof,claim_token)
SELECT receipt_id,operation_id,$2,$3,source_proof,$4 FROM server_probe_terminal_proofs WHERE operation_id=$1
RETURNING claim_token`,[o.operation_id,o.plan_id,o.connection_id,randomUUID()])).rows[0]
assert.ok(spent)
await tickMcpWorkers()
await tickMcpWorkers()
assert.deepEqual((await pool.query("SELECT claim_token FROM server_probe_retirement_claims WHERE receipt_id=$1",[a.receipt_id])).rows,[spent])
assert.equal((await pool.query("SELECT 1 FROM mcp_operations WHERE id=$1",[o.operation_id])).rows.length,1)
assert.equal((await pool.query("SELECT 1 FROM mcp_operation_replay_tombstones WHERE operation_id=$1",[o.operation_id])).rows.length,0)
return
}
if (mode !== "default-old") {
await tickMcpWorkers()
await assert.rejects(retireExpiredServerProbeOperation(target))
assert.equal((await pool.query("SELECT 1 FROM server_probe_retirement_claims WHERE receipt_id=$1",[a.receipt_id])).rows.length,0)
assert.equal((await pool.query("SELECT 1 FROM mcp_operations WHERE id=$1",[o.operation_id])).rows.length,1)
return
}
await pool.query("UPDATE mcp_operations SET result=jsonb_set(result,'{confirmedWrites}','1') WHERE id=$1",[o.operation_id])
await tickMcpWorkers()
assert.equal((await pool.query("SELECT 1 FROM server_probe_retirement_claims WHERE receipt_id=$1",[a.receipt_id])).rows.length,0)
await pool.query("UPDATE mcp_operations SET result=$2 WHERE id=$1",[o.operation_id,o.operation_result])
await tickMcpWorkers()
const absence = (await pool.query(`SELECT
NOT EXISTS(SELECT 1 FROM mcp_operations WHERE id=$1) AS op,
NOT EXISTS(SELECT 1 FROM mcp_plans WHERE id=$2) AS plan,
NOT EXISTS(SELECT 1 FROM mcp_operation_steps WHERE operation_id=$1) AS step,
NOT EXISTS(SELECT 1 FROM mcp_operation_checkpoints WHERE operation_id=$1) AS checkpoint,
NOT EXISTS(SELECT 1 FROM server_probe_terminal_proofs WHERE operation_id=$1) AS proof,
NOT EXISTS(SELECT 1 FROM server_probe_retired_sources WHERE operation_id=$1) AS archive`,[o.operation_id,o.plan_id])).rows[0]
assert.deepEqual(absence,{op:true,plan:true,step:true,checkpoint:true,proof:true,archive:true})
await assert.rejects(applyPlan(principal,o.plan_id,key),error=>error instanceof McpError&&error.code==="OPERATION_HISTORY_EXPIRED")
await assert.rejects(invokeMcpTool(foreign,"mm_operation_get",{operationId:o.operation_id}),error=>error instanceof McpError&&error.code==="NOT_FOUND")
return
}
await pool.query("UPDATE mcp_operation_checkpoints SET after_sealed=$2 WHERE operation_id=$1", [o.operation_id,await encryptSecret('["held"]')])
await assert.rejects(retireCompletedServerProbeOperation(target))
assert.equal((await pool.query("SELECT 1 FROM server_probe_retirement_claims WHERE receipt_id=$1", [a.receipt_id])).rows.length, 0)
await pool.query("UPDATE mcp_operation_checkpoints SET after_sealed=$2 WHERE operation_id=$1", [o.operation_id,empty])
if (mode === "scheduled-retained") {
await tickMcpWorkers()
await tickMcpWorkers()
assert.equal((await pool.query("SELECT 1 FROM mcp_operations WHERE id=$1",[o.operation_id])).rows.length,0)
assert.equal((await pool.query("SELECT 1 FROM server_probe_retired_sources WHERE receipt_id=$1",[a.receipt_id])).rows.length,1)
assert.deepEqual(await invokeMcpTool(principal,"mm_server_probe_receipt_get",request),live)
await assert.rejects(applyPlan(principal,o.plan_id,key),error=>error instanceof McpError&&error.code==="OPERATION_HISTORY_EXPIRED")
return
}
assert.deepEqual(await retireCompletedServerProbeOperation(target), { retired:true,automaticReplay:false })
const proof = (await pool.query(`SELECT
NOT EXISTS(SELECT 1 FROM mcp_operations WHERE id=$1) AS op,
NOT EXISTS(SELECT 1 FROM mcp_plans WHERE id=$2) AS plan,
NOT EXISTS(SELECT 1 FROM mcp_operation_steps WHERE operation_id=$1) AS step,
NOT EXISTS(SELECT 1 FROM mcp_operation_checkpoints WHERE operation_id=$1) AS checkpoint,
(SELECT receipt_ciphertext=$4 FROM server_probe_execution_payloads WHERE receipt_id=$3) AS payload,
(SELECT reserved_bytes=294912 FROM server_probe_storage_usage WHERE scope='global') AS quota`,
[o.operation_id,o.plan_id,a.receipt_id,raw.payload.receipt_ciphertext])).rows[0]
assert.deepEqual(proof, { op:true,plan:true,step:true,checkpoint:true,payload:true,quota:true })
assert.deepEqual(await invokeMcpTool(principal, "mm_server_probe_receipt_get", request), live)
await assert.rejects(invokeMcpTool(foreign, "mm_server_probe_receipt_get", request))
await assert.rejects(applyPlan(principal,o.plan_id,key), error => error instanceof McpError && error.code==="OPERATION_HISTORY_EXPIRED" && error.statusCode===410)
await assert.rejects(applyPlan(principal,randomUUID(),key), error => error instanceof McpError && error.code==="STATE_CONFLICT" && error.statusCode===409)
await assert.rejects(retireCompletedServerProbeOperation(target))
const gc = await pool.connect()
try {
await gc.query("BEGIN")
await gc.query("SET LOCAL mm.probe_storage_gc='on'")
assert.equal((await gc.query("SELECT server_probe_storage_gc_allowed($1) AS allowed", [a.receipt_id])).rows[0].allowed, false)
await gc.query("ROLLBACK")
} finally { gc.release(true) }
const protectedWrite = await pool.connect()
try {
await protectedWrite.query("BEGIN")
const {rows:[archive]} = await protectedWrite.query("SELECT proof FROM server_probe_retired_sources WHERE receipt_id=$1",[a.receipt_id])
await protectedWrite.query(`SELECT set_config('mm.probe_storage_gc','on',true),set_config('mm.probe_retired_gc_receipt',$1,true),
set_config('mm.probe_retired_gc_proof',$2,true)`, [a.receipt_id,archive.proof])
await assert.rejects(protectedWrite.query("DELETE FROM server_probe_retired_sources WHERE receipt_id=$1",[a.receipt_id]),/immutable/)
await protectedWrite.query("ROLLBACK")
} finally { protectedWrite.release(true) }
const raced = await pool.connect()
try {
await raced.query("BEGIN")
await raced.query("SELECT receipt_id FROM server_probe_retired_sources WHERE receipt_id=$1 FOR UPDATE",[a.receipt_id])
await assert.rejects(collectExpiredRetiredServerProbe(target))
await raced.query("ROLLBACK")
} finally { raced.release(true) }
assert.deepEqual(await invokeMcpTool(principal, "mm_server_probe_receipt_get", request),live)
if (mode === "purged") {
await pool.query("UPDATE mcp_settings SET requests_retention_days=365 WHERE id=1")
await assert.rejects(collectExpiredRetiredServerProbe(target))
assert.deepEqual(await invokeMcpTool(principal,"mm_server_probe_receipt_get",request),live)
await pool.query("UPDATE mcp_settings SET requests_retention_days=30 WHERE id=1")
assert.deepEqual(await storage.purge(actor.id,1,{ deadlineAt:Date.now()+10000 }),{ purged:1,automaticReplay:false })
await assert.rejects(invokeMcpTool(principal,"mm_server_probe_receipt_get",request))
assert.equal((await pool.query("SELECT reserved_bytes::int AS bytes FROM server_probe_storage_usage WHERE scope='global'")).rows[0].bytes,32768)
}
if (mode === "retained") await tickMcpWorkers()
else assert.deepEqual(await collectExpiredRetiredServerProbe(target), {collected:true,automaticReplay:false})
const collected = (await pool.query(`SELECT
(SELECT count(*)::int FROM server_probe_retired_sources WHERE receipt_id=$1) AS archive,
(SELECT count(*)::int FROM server_probe_storage_admissions WHERE receipt_id=$1) AS admission,
(SELECT count(*)::int FROM server_probe_executions WHERE receipt_id=$1) AS anchor,
(SELECT count(*)::int FROM server_probe_execution_stages WHERE receipt_id=$1) AS stages,
(SELECT count(*)::int FROM server_probe_execution_payloads WHERE receipt_id=$1) AS payload,
(SELECT reserved_bytes::int FROM server_probe_storage_usage WHERE scope='global') AS quota,
(SELECT count(*)::int FROM server_probe_storage_usage WHERE scope=$2) AS owner_counter`,[a.receipt_id,"owner:"+actor.id])).rows[0]
assert.deepEqual(collected,{archive:0,admission:0,anchor:0,stages:0,payload:0,quota:0,owner_counter:0})
await assert.rejects(collectExpiredRetiredServerProbe(target))
await assert.rejects(invokeMcpTool(principal,"mm_server_probe_receipt_get",request))
await assert.rejects(applyPlan(principal,o.plan_id,key),error=>error instanceof McpError&&error.code==="OPERATION_HISTORY_EXPIRED")
await pool.query("UPDATE mcp_connections SET policy=jsonb_set(policy,'{scope,serverIds}','[]') WHERE id=$1", [principal.connectionId])
await assert.rejects(invokeMcpTool(principal, "mm_server_probe_receipt_get", request))
await assert.rejects(applyPlan(principal,o.plan_id,key), error => error instanceof McpError && error.code==="NOT_FOUND" && error.statusCode===404)
}
await verifyLifecycle("retained")
await verifyLifecycle("purged")
await verifyLifecycle("default-young")
await verifyLifecycle("default-old")
await verifyLifecycle("default-unproven")
await verifyLifecycle("default-spent")
await verifyLifecycle("scheduled-retained")
console.log("Owned diagnostic retirement: real worker, retained/expired paths, spent claims and holds, public replay/read and exact GC refunds")
} finally { await closePool() }
@@ -0,0 +1,370 @@
/** Exact internal maintenance entry. No scheduler, public registration, or native work. */
import { randomUUID } from "node:crypto"
import { z } from "zod"
import { operationIdempotencyDigest } from "../modules/mcp/operation-replay-tombstones.js"
import { constantEqual, decryptSecretExisting, fingerprint, privateFingerprinterExisting } from "../modules/mcp/security.js"
import { PROBE_PAYLOAD_MAX_BYTES, PROBE_PERMANENT_RESERVATION_BYTES, ProbeStorageRefused, ProbeStorageUnknown, type ProbeStorageSql } from "./server-probe-journal-maintenance.js"
import { findRetiredServerProbeHistory } from "./server-probe-retired-history.js"
import { matchesServerProbeRetiredTerminal, openServerProbeRetiredSource, projectServerProbeRetiredSource, ServerProbeRetirementSourceRefused } from "./server-probe-retired-source.js"
import { connectProbeStorage } from "./server-probe-storage-connection.js"
import { sharedDatabaseGate } from "./shared-database-gate.js"
const targetSchema = z.object({ receiptId: z.string().uuid(), operationId: z.string().uuid(), connectionId: z.string().uuid() }).strict()
type Target = z.infer<typeof targetSchema>
function requireThat(value: unknown): asserts value { if (!value) throw new ProbeStorageRefused() }
const ORIGINAL = `jsonb_build_object(
'operation_id',o.id,'connection_id',o.connection_id,'owner_id',c.owner_id,'plan_id',p.id,
'operation_name',p.operation_id,'operation_version',p.operation_version,'operation_state',o.state,
'operation_verification',o.verification,'operation_error_code',o.error_code,'operation_result',o.result,
'plan_state',p.state,'approved_by',p.approved_by,'evidence',p.evidence,'step_index',s.step_index,
'step_state',s.state,'intended_state',s.intended_state,'step_result',s.result,
'operation_created_at',o.created_at,'operation_finished_at',o.finished_at,'plan_created_at',p.created_at,
'plan_approved_at',p.approved_at,'plan_expires_at',p.expires_at,'step_created_at',s.created_at,'step_finished_at',s.finished_at
) AS original,o.idempotency_key,k.before_sealed,k.after_sealed`
const ORIGINAL_JOINS = `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.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=s.step_index`
const ORIGINAL_ELIGIBLE = ` 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()
AND p.operation_id='servers.probes.run' AND p.operation_version=1 AND p.state='completed'
AND isfinite(p.expires_at)
AND s.state='acknowledged' AND s.server_id IS NULL AND s.before_state IS NULL
AND octet_length(k.before_sealed)<=1024 AND octet_length(k.after_sealed)<=1024
AND octet_length(p.evidence::text)+octet_length(s.intended_state::text)+octet_length(s.result::text)+octet_length(o.result::text)<=2097152
AND NOT EXISTS(SELECT 1 FROM public.mcp_operation_steps WHERE operation_id=o.id AND step_index<>0)
AND NOT EXISTS(SELECT 1 FROM public.mcp_operation_checkpoints WHERE operation_id=o.id AND step_index<>0)
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_execution_quarantines WHERE receipt_id=$1 OR(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 SOURCE = `SELECT ${ORIGINAL},to_jsonb(a) AS admission,to_jsonb(j) AS anchor,to_jsonb(b) AS payload
FROM public.server_probe_storage_admissions a
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
JOIN public.mcp_operations o ON o.id=a.execution_id
${ORIGINAL_JOINS}
WHERE a.receipt_id=$1 AND o.id=$2 AND c.id=$3 AND c.owner_id=a.owner_id AND a.identity_kind='mcp'
AND a.claim_token IS NULL AND a.purged_at IS NULL AND a.cancelled_at IS NULL
AND (($5::timestamptz IS NULL AND a.authority_expires_at<transaction_timestamp() AND p.expires_at<transaction_timestamp())
OR o.finished_at=$5::timestamptz)
AND (NOT $4::boolean OR o.finished_at<transaction_timestamp()-interval '180 days')
${ORIGINAL_ELIGIBLE} FOR UPDATE OF a,j,b,o,p,s,k NOWAIT`
const CLOSED_SOURCE = `SELECT ${ORIGINAL},f.source,f.source_proof,f.idempotency_digest,f.capture_proof
FROM public.mcp_operations o ${ORIGINAL_JOINS}
JOIN public.server_probe_terminal_proofs f ON f.operation_id=o.id AND f.receipt_id=$1
WHERE o.id=$2 AND c.id=$3 AND o.finished_at<transaction_timestamp()-interval '180 days' AND p.expires_at<transaction_timestamp()
AND NOT EXISTS(SELECT 1 FROM public.server_probe_storage_admissions WHERE receipt_id=$1 OR(identity_kind='mcp' AND execution_id=o.id))
AND NOT EXISTS(SELECT 1 FROM public.server_probe_executions WHERE receipt_id=$1 OR(identity_kind='mcp' AND execution_id=o.id))
AND NOT EXISTS(SELECT 1 FROM public.server_probe_execution_payloads WHERE receipt_id=$1)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_execution_stages WHERE receipt_id=$1)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_retired_sources WHERE receipt_id=$1 OR operation_id=o.id)
${ORIGINAL_ELIGIBLE} FOR UPDATE OF f,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
let timer: ReturnType<typeof setTimeout> | undefined
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)")
// The same order as admission, publication, cancellation and payload GC.
requireThat((await sql.query("SELECT pg_try_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0)) AS locked")).rows[0]?.locked === true)
// Bound non-SQL proof work as well. Any late continuation retains only
// this permanently released checkout and cannot issue further SQL.
const expired = new Promise<never>((_resolve,reject) => {
timer = setTimeout(() => {
sql.release(true)
reject(new ProbeStorageRefused())
}, Math.max(1,deadlineAt-Date.now()))
})
const result = await Promise.race([work(sql),expired])
clearTimeout(timer)
requireThat(Date.now()<deadlineAt)
committing = true
await sql.query("COMMIT")
committed = true
return result
} catch (error) {
if (committing) throw new ProbeStorageUnknown()
try { await sql.query("ROLLBACK") } catch { /* Checkout is discarded. */ }
throw error
} finally {
clearTimeout(timer)
sql.release(!committed)
}
}
async function lockOriginal(sql: ProbeStorageSql, { operationId, connectionId }: Target) {
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)
}
async function assertEmptyCheckpoints(row: Record<string, unknown>) {
for (const sealed of [row.before_sealed, row.after_sealed]) {
requireThat(typeof sealed === "string")
let decoded: unknown
try { decoded = JSON.parse(await decryptSecretExisting(sealed)) }
catch { throw new ProbeStorageRefused() }
requireThat(Array.isArray(decoded) && decoded.length === 0)
}
}
async function prepare(sql: ProbeStorageSql, target: Target, requireOld = true, expectedFinishedAt?: string) {
const { receiptId, operationId, connectionId } = target
await lockOriginal(sql, target)
const rows = (await sql.query(SOURCE, [receiptId, operationId, connectionId, requireOld, expectedFinishedAt ?? null])).rows
requireThat(rows.length === 1)
const { original, admission, anchor, payload, idempotency_key: key } = rows[0]
requireThat(typeof key === "string")
await assertEmptyCheckpoints(rows[0])
const stages = (await sql.query(`SELECT to_jsonb(s) AS stage FROM public.server_probe_execution_stages s
WHERE s.receipt_id=$1 ORDER BY s.stage_index LIMIT 12 FOR UPDATE NOWAIT`, [receiptId])).rows.map(row => row.stage)
const signed = await projectServerProbeRetiredSource({ original, admission, anchor, payload, stages, quarantineCount: 0, cancellationCount: 0 })
requireThat(signed.source.receiptId === receiptId && signed.source.executionId === operationId && signed.source.connectionId === connectionId)
return { signed, key }
}
const captureHash = z.string().regex(/^[a-f0-9]{64}$/)
async function terminalCaptureProof(sourceProof: string, idempotencyDigest: string) {
const sign = await privateFingerprinterExisting("server-probe-terminal-capture-v1")
return sign(fingerprint({ sourceProof, idempotencyDigest }))
}
function captureTerminalProof(target: Target, deadlineAt: number, expectedFinishedAt?: string) {
return sharedDatabaseGate.request(() => transaction(deadlineAt, async sql => {
const { signed, key } = await prepare(sql, target, false, expectedFinishedAt)
const digest = operationIdempotencyDigest(target.connectionId, key)
const proof = await terminalCaptureProof(signed.proof, digest)
const saved = (await sql.query(`INSERT INTO public.server_probe_terminal_proofs
(operation_id,receipt_id,source,source_proof,idempotency_digest,capture_proof)
VALUES($1,$2,$3::jsonb,$4,$5,$6) ON CONFLICT DO NOTHING RETURNING receipt_id`,
[target.operationId,target.receiptId,JSON.stringify(signed.source),signed.proof,digest,proof])).rows
requireThat(saved.length === 1 && saved[0].receipt_id === target.receiptId)
return { captured:true as const, automaticReplay:false as const }
}))
}
/** Captures evidence only while the normal retention owner can still read the original. */
export function captureServerProbeTerminalProof(raw: Target, deadlineAt = Date.now()+20000) {
return captureTerminalProof(targetSchema.parse(raw), deadlineAt)
}
/** Only the operation runner supplies the positively acknowledged terminal SQL timestamp. */
export function capturePublishedServerProbeTerminalProof(raw: Target & { finishedAt: string }) {
const { finishedAt, ...identity } = raw
const stamp = z.string().regex(/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{6}Z$/).parse(finishedAt)
return captureTerminalProof(targetSchema.parse(identity), Date.now()+1200, stamp)
}
async function prepareClosed(sql: ProbeStorageSql, target: Target) {
const { receiptId, operationId, connectionId } = target
await lockOriginal(sql, target)
const rows = (await sql.query(CLOSED_SOURCE, [receiptId, operationId, connectionId])).rows
requireThat(rows.length === 1)
const row = rows[0], key = z.string().parse(row.idempotency_key)
await assertEmptyCheckpoints(row)
const proof = captureHash.parse(row.source_proof), digest = operationIdempotencyDigest(connectionId, key)
requireThat(digest === row.idempotency_digest
&& constantEqual(captureHash.parse(row.capture_proof), await terminalCaptureProof(proof, digest)))
const source = await openServerProbeRetiredSource({ source:row.source, proof })
requireThat(source.receiptId === receiptId && source.executionId === operationId && source.connectionId === connectionId
&& matchesServerProbeRetiredTerminal(source, row.original))
return { signed:{source,proof},key }
}
function retireServerProbeOperation(raw: Target, kind: "retained" | "expired", deadlineAt = Date.now()+20000) {
const target = targetSchema.parse(raw)
const { receiptId, operationId, connectionId } = target
return sharedDatabaseGate.request(async () => {
const token = randomUUID()
const read = kind === "retained" ? prepare : prepareClosed
const claim = await transaction(deadlineAt, async sql => {
const { signed, key } = await read(sql, target)
const digest = operationIdempotencyDigest(connectionId, key)
const result = await sql.query(`INSERT INTO public.server_probe_retirement_claims
(receipt_id,operation_id,plan_id,connection_id,source_proof,claim_token)
VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT DO NOTHING RETURNING receipt_id`,
[receiptId, operationId, signed.source.planId, connectionId, signed.proof, token])
requireThat(result.rows.length === 1 && result.rows[0].receipt_id === receiptId)
return { proof: signed.proof, planId: signed.source.planId, digest }
})
return transaction(deadlineAt, async sql => {
const { signed, key } = await read(sql, target), { planId, proof, digest } = claim
requireThat(signed.proof === proof && signed.source.planId === planId && operationIdempotencyDigest(connectionId, key) === digest)
const consumed = await sql.query(`SELECT receipt_id FROM public.server_probe_retirement_claims
WHERE receipt_id=$1 AND operation_id=$2 AND plan_id=$3 AND connection_id=$4 AND source_proof=$5
AND claim_token=$6 AND claim_xid<>pg_current_xact_id()`, [receiptId, operationId, planId, connectionId, proof, token])
requireThat(consumed.rows.length === 1 && consumed.rows[0].receipt_id === receiptId)
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,'servers.probes.run',$5::bigint[],false) RETURNING operation_id`,
[operationId, planId, connectionId, digest, signed.source.serverIds])
requireThat(tombstone.rows.length === 1 && tombstone.rows[0].operation_id === operationId)
await sql.query("SELECT set_config('mm.probe_retirement_claim',$1,true)", [token])
if (kind === "retained") {
const archive = await sql.query(`INSERT INTO public.server_probe_retired_sources(receipt_id,operation_id,source,proof)
VALUES($1,$2,$3::jsonb,$4) RETURNING receipt_id`, [receiptId, operationId, JSON.stringify(signed.source), proof])
requireThat(archive.rows.length === 1 && archive.rows[0].receipt_id === receiptId)
}
const released = await sql.query("DELETE FROM public.server_probe_terminal_proofs WHERE operation_id=$1 RETURNING receipt_id", [operationId])
requireThat(released.rows.length <= 1 && released.rows.every(row => row.receipt_id === receiptId)
&& (kind !== "expired" || released.rows.length === 1))
const checkpoint = await sql.query("DELETE FROM public.mcp_operation_checkpoints WHERE operation_id=$1 AND step_index=0 RETURNING operation_id", [operationId])
requireThat(checkpoint.rows.length === 1 && checkpoint.rows[0].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])
requireThat(step.rows.length === 1 && step.rows[0].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", [operationId, connectionId, planId])
requireThat(operation.rows.length === 1 && operation.rows[0].id === operationId)
const plan = await sql.query("DELETE FROM public.mcp_plans WHERE id=$1 AND connection_id=$2 RETURNING id", [planId, connectionId])
requireThat(plan.rows.length === 1 && plan.rows[0].id === planId)
if (kind === "retained") {
const observed = await findRetiredServerProbeHistory(async (text, values) => (await sql.query(text, values)).rows,
{ ...target, ownerId: signed.source.ownerId, scopeServerIds: signed.source.serverIds })
requireThat(observed && !observed.payloadPurged && fingerprint(observed.source) === fingerprint(signed.source))
}
return { retired: true as const, automaticReplay: false as const }
})
})
}
export function retireCompletedServerProbeOperation(raw: Target) {
return retireServerProbeOperation(raw, "retained")
}
export function retireExpiredServerProbeOperation(raw: Target) {
return retireServerProbeOperation(raw, "expired")
}
/** Exact internal GC. This entry has no worker or public registration. */
export function collectExpiredRetiredServerProbe(raw: Target, deadlineAt = Date.now()+20000) {
const target = targetSchema.parse(raw)
const { receiptId, operationId, connectionId } = target
return sharedDatabaseGate.request(() => transaction(deadlineAt, async sql => {
const settings = (await sql.query("SELECT requests_retention_days FROM public.mcp_settings WHERE id=1 FOR SHARE NOWAIT")).rows
const retentionDays = settings[0]?.requests_retention_days
requireThat(settings.length === 1 && typeof retentionDays === "number" && Number.isInteger(retentionDays)
&& retentionDays >= 1 && retentionDays <= 365)
const pinned = (await sql.query(`SELECT c.owner_id,r.proof,t.target_server_ids::text[] AS server_ids FROM public.server_probe_retired_sources r
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
JOIN public.server_probe_storage_admissions a ON a.receipt_id=r.receipt_id AND a.owner_id=c.owner_id
JOIN public.server_probe_executions j ON j.receipt_id=r.receipt_id
WHERE r.receipt_id=$1 AND r.operation_id=$2 AND c.id=$3 FOR UPDATE OF c,r,t,a,j NOWAIT`,
[receiptId, operationId, connectionId])).rows
const identity = z.object({ owner_id: z.string().min(1).max(200), proof: z.string().regex(/^[a-f0-9]{64}$/),
server_ids: z.array(z.string().regex(/^[1-9]\d{0,15}$/).transform(Number).pipe(z.number().int().positive().safe())).min(1).max(2) }).strict().parse(pinned[0])
requireThat(pinned.length === 1)
const observed = await findRetiredServerProbeHistory(async (text, values) => (await sql.query(text, values)).rows,
{ ...target, ownerId: identity.owner_id, scopeServerIds: identity.server_ids })
requireThat(observed)
const stageRows = (await sql.query(`SELECT to_jsonb(s) AS stage FROM public.server_probe_execution_stages s
WHERE s.receipt_id=$1 ORDER BY s.stage_index LIMIT 12 FOR UPDATE NOWAIT`, [receiptId])).rows
requireThat(stageRows.length >= 1 && stageRows.length <= 11
&& fingerprint(stageRows.map(row => row.stage)) === observed.source.stageSnapshotDigest)
await sql.query(`SELECT set_config('mm.probe_storage_gc','on',true),set_config('mm.probe_retired_gc_receipt',$1,true),
set_config('mm.probe_retired_gc_proof',$2,true)`, [receiptId, identity.proof])
requireThat((await sql.query("SELECT public.server_probe_storage_gc_allowed($1) AS allowed", [receiptId])).rows[0]?.allowed === true)
const deletedPayload = (await sql.query("DELETE FROM public.server_probe_execution_payloads WHERE receipt_id=$1 RETURNING receipt_id", [receiptId])).rows
const files = observed.payloadPurged ? 0 : 1
requireThat(deletedPayload.length === files && deletedPayload.every(row => row.receipt_id === receiptId))
const deletedStages = (await sql.query("DELETE FROM public.server_probe_execution_stages WHERE receipt_id=$1 RETURNING receipt_id", [receiptId])).rows
requireThat(deletedStages.length === stageRows.length && deletedStages.every(row => row.receipt_id === receiptId))
const deletedAnchor = (await sql.query("DELETE FROM public.server_probe_executions WHERE receipt_id=$1 RETURNING receipt_id", [receiptId])).rows
requireThat(deletedAnchor.length === 1 && deletedAnchor[0].receipt_id === receiptId)
// Admission deletion cascades to the exact authenticated archive only.
const deletedAdmission = (await sql.query("DELETE FROM public.server_probe_storage_admissions WHERE receipt_id=$1 AND owner_id=$2 RETURNING receipt_id",
[receiptId, identity.owner_id])).rows
requireThat(deletedAdmission.length === 1 && deletedAdmission[0].receipt_id === receiptId)
const scopes = ["global", "owner:" + identity.owner_id], bytes = PROBE_PERMANENT_RESERVATION_BYTES + files * PROBE_PAYLOAD_MAX_BYTES
const released = (await sql.query(`UPDATE public.server_probe_storage_usage SET admissions=admissions-1,
reserved_files=reserved_files-$2,reserved_bytes=reserved_bytes-$3 WHERE scope=ANY($1::text[])
AND admissions>=1 AND reserved_files>=$2 AND reserved_bytes>=$3 RETURNING scope`, [scopes, files, bytes])).rows
requireThat(released.length === 2 && scopes.every(scope => released.some(row => row.scope === scope)))
await sql.query("DELETE FROM public.server_probe_storage_usage WHERE scope=$1 AND admissions=0 AND reserved_files=0 AND reserved_bytes=0", [scopes[1]])
const state = (await sql.query(`SELECT
NOT EXISTS(SELECT 1 FROM public.server_probe_retired_sources WHERE receipt_id=$1) AS archive_gone,
EXISTS(SELECT 1 FROM public.mcp_operation_replay_tombstones WHERE operation_id=$2 AND connection_id=$3) AS replay_retained,
EXISTS(SELECT 1 FROM public.server_probe_retirement_claims WHERE receipt_id=$1 AND operation_id=$2 AND connection_id=$3) AS claim_retained`,
[receiptId, operationId, connectionId])).rows[0]
requireThat(state?.archive_gone === true && state.replay_retained === true && state.claim_retained === true)
return { collected: true as const, automaticReplay: false as const }
}))
}
const RETIRE_CANDIDATES = `SELECT COALESCE(a.receipt_id,f.receipt_id) AS receipt_id,o.id AS operation_id,o.connection_id,
o.finished_at::text AS ordered_at,CASE WHEN a.receipt_id IS NULL THEN 'expired' ELSE 'retained' END AS kind
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
LEFT JOIN public.server_probe_storage_admissions a ON a.identity_kind='mcp' AND a.execution_id=o.id AND a.owner_id=c.owner_id
LEFT JOIN public.server_probe_terminal_proofs f ON f.operation_id=o.id
WHERE 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 ((a.receipt_id IS NULL AND f.receipt_id IS NOT NULL)
OR (a.claim_token IS NULL AND a.claim_attempted_at IS NOT NULL AND a.purged_at IS NULL AND a.cancelled_at IS NULL
AND EXISTS(SELECT 1 FROM public.server_probe_execution_payloads b WHERE b.receipt_id=a.receipt_id)))
AND NOT EXISTS(SELECT 1 FROM public.server_probe_retirement_claims x WHERE x.operation_id=o.id)
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_diagnostic_cancellations WHERE 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.vrf_production_tickets WHERE plan_id=p.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 GC_CANDIDATES = `SELECT r.receipt_id,r.operation_id,t.connection_id,a.gc_not_before::text AS ordered_at,'gc' AS kind
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 isfinite(a.gc_not_before) AND a.gc_not_before<transaction_timestamp()
AND t.action_id='servers.probes.run' AND NOT t.target_global
AND ($1::timestamptz IS NULL OR (a.gc_not_before,r.operation_id)>($1::timestamptz,$2::uuid))
ORDER BY a.gc_not_before,r.operation_id LIMIT 8`
type Cursor = { orderedAt: string; operationId: string }
let retireCursor: Cursor | undefined, gcCursor: Cursor | undefined
const workerRow = z.object({ receipt_id:z.string().uuid(),operation_id:z.string().uuid(),connection_id:z.string().uuid(),
ordered_at:z.string().refine(value=>Number.isFinite(Date.parse(value))),kind:z.enum(["retained","expired","gc"]) }).strict()
/** Invoked only under the existing scheduler owner. Keep expiry and one-shot
* retirement independent; a claimed operation is never selected again. */
export function maintainServerProbeOperationRetirement() {
return sharedDatabaseGate.request(async () => {
const deadlineAt = Date.now()+20000
const scan = async (lane: "retire" | "gc") => {
const cursor = lane === "retire" ? retireCursor : gcCursor
const setCursor = (value: Cursor | undefined) => { if (lane === "retire") retireCursor=value; else gcCursor=value }
const rows = await transaction(deadlineAt,async sql => (await sql.query(lane === "retire" ? RETIRE_CANDIDATES : GC_CANDIDATES,
[cursor?.orderedAt ?? null,cursor?.operationId ?? null])).rows)
requireThat(rows.length<=8)
let scanned=0, completed=0
for (const raw of rows) {
if (Date.now()>=deadlineAt-2000 || completed>=2) break
const row=workerRow.parse(raw)
requireThat((row.kind === "gc") === (lane === "gc"))
const target={receiptId:row.receipt_id,operationId:row.operation_id,connectionId:row.connection_id}
setCursor({orderedAt:row.ordered_at,operationId:row.operation_id}); scanned++
try {
if (row.kind === "gc") await collectExpiredRetiredServerProbe(target,deadlineAt)
else await retireServerProbeOperation(target,row.kind,deadlineAt)
completed++
} catch (error) {
if (!(error instanceof ProbeStorageRefused || error instanceof ServerProbeRetirementSourceRefused || error instanceof z.ZodError)) throw error
}
}
if (scanned === rows.length && rows.length < 8) setCursor(undefined)
return completed
}
const collected=await scan("gc")
const retired=Date.now()<deadlineAt-2000 ? await scan("retire") : 0
return {collected,retired,automaticReplay:false as const}
})
}
+43 -2
View File
@@ -5,14 +5,55 @@ import { PROBE_STORAGE_LIMITS } from "../modules/mcp/diagnostic-cancellation-sto
import { connectProbeStorage } from "./server-probe-storage-connection.js"
import { createProbeJournalStorage, ProbeStorageRefused, type ProbeStorageConnection, type ProbeStorageOptions } from "./server-probe-journal-maintenance.js"
import { sharedDatabaseGate } from "./shared-database-gate.js"
import { captureServerProbeTerminalProof } from "./server-probe-operation-retirement.js"
function createRetention(connect: typeof connectProbeStorage) {
/** Independent from the expiry transaction. A busy, malformed, or uncertain
* source cannot extend retention of its ciphertext or reuse a checkout. */
async function captureBeforeExpiry() {
const deadlineAt = Date.now() + 2200
let candidates: Record<string, unknown>[]
try {
const sql = await connectProbeStorage({ deadlineAt: Math.min(deadlineAt, Date.now()+500) })
let read = false
try {
candidates = (await sql.query(`/* probe-retention:capture-candidates */
SELECT a.receipt_id,o.id AS operation_id,o.connection_id
FROM public.server_probe_storage_admissions a
JOIN public.server_probe_execution_payloads b USING(receipt_id)
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.identity_kind='mcp' 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 a.authority_expires_at<clock_timestamp()
AND a.gc_not_before<clock_timestamp()+interval '1 day'
AND o.state='applied' AND o.verification='inconclusive' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<clock_timestamp()
AND p.operation_id='servers.probes.run' AND p.operation_version=1 AND p.state='completed'
AND isfinite(p.expires_at) AND p.expires_at<clock_timestamp()
AND NOT EXISTS(SELECT 1 FROM public.server_probe_terminal_proofs f WHERE f.operation_id=o.id OR f.receipt_id=a.receipt_id)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_retired_sources r WHERE r.operation_id=o.id OR r.receipt_id=a.receipt_id)
ORDER BY a.gc_not_before,a.receipt_id LIMIT 8`, [], {timeoutMs:450})).rows
read = true
} finally { sql.release(!read) }
} catch { return }
for (const row of candidates) {
if (Date.now() >= deadlineAt || candidates.length > 8) break
if (typeof row.receipt_id !== "string" || typeof row.operation_id !== "string" || typeof row.connection_id !== "string") continue
try {
await captureServerProbeTerminalProof({ receiptId:row.receipt_id, operationId:row.operation_id, connectionId:row.connection_id },
Math.min(deadlineAt, Date.now()+550))
} catch { /* Never hold the payload or retry a capture in this window. */ }
}
}
function createRetention(connect: typeof connectProbeStorage, preCapture?: () => Promise<void>) {
let running = false, cursor = ""
return async () => {
if (running) return { skipped: true, owners: 0, collected: 0 }
running = true
try {
return await sharedDatabaseGate.request(async () => {
await preCapture?.()
const options: ProbeStorageOptions = { deadlineAt: Date.now() + 20000 }
const physical = await connect(options)
let active = true, broken = false, released = false, currentOwner = ""
@@ -69,6 +110,6 @@ function createRetention(connect: typeof connectProbeStorage) {
}
}
export const retainServerProbeStorage = createRetention(connectProbeStorage)
export const retainServerProbeStorage = createRetention(connectProbeStorage, captureBeforeExpiry)
/** Trusted owned-test composition only; never an HTTP/MCP parameter. */
export const createServerProbeRetentionForTests = createRetention
@@ -0,0 +1,67 @@
/** Fixed private source lookup. The action owns the current authority and
* protection fence, and the existing history reader authenticates payload. */
import { z } from "zod"
import { fingerprint } from "../modules/mcp/security.js"
import { openServerProbeRetiredSource, ServerProbeRetirementSourceRefused } from "./server-probe-retired-source.js"
type Query=(sql:string,values:unknown[])=>Promise<Record<string,unknown>[]>
export interface ServerProbeRetiredLookup {
receiptId:string
operationId:string
connectionId:string
ownerId:string
scopeServerIds:readonly number[]
}
const SQL=`/* probe-history:retired-source */ SELECT r.source,r.proof,t.plan_id,t.target_server_ids::text[] AS tombstone_server_ids,
j.source_server_id,j.destination_server_id,j.tool,j.mcp_credential_id,j.authority_issued_at,
j.receipt_proof,a.binding_proof,a.retention_days,a.safety_ms,
to_char(a.authority_expires_at AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS authority_expires_at,
to_char(a.gc_not_before AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS gc_not_before,
a.purged_at IS NOT NULL AS payload_purged,p.receipt_proof AS payload_proof,
CASE WHEN p.receipt_ciphertext IS NOT NULL THEN pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(p.receipt_ciphertext,'UTF8')),'hex') END AS payload_sha256
FROM public.server_probe_retired_sources r
JOIN public.mcp_operation_replay_tombstones t ON t.operation_id=r.operation_id AND t.connection_id=$3
JOIN public.mcp_connections c ON c.id=t.connection_id AND c.owner_id=$4
JOIN public.server_probe_storage_admissions a ON a.receipt_id=r.receipt_id
JOIN public.server_probe_executions j ON j.receipt_id=a.receipt_id AND j.execution_id=a.execution_id AND j.identity_kind=a.identity_kind
LEFT JOIN public.server_probe_execution_payloads p ON p.receipt_id=a.receipt_id
WHERE r.receipt_id=$1 AND r.operation_id=$2 AND a.execution_id=r.operation_id AND a.identity_kind='mcp'
AND a.owner_id=c.owner_id AND a.claim_token IS NULL AND isfinite(a.claim_attempted_at) AND a.cancelled_at IS NULL
AND (a.purged_at IS NULL OR isfinite(a.purged_at))
AND j.actor_id=c.owner_id AND j.mcp_connection_id=c.id AND j.authentication='mcp' AND j.request_id IS NULL
AND j.binding_proof=a.binding_proof AND j.authority_expires_at=a.authority_expires_at AND j.state='completed'
AND t.action_id='servers.probes.run' AND NOT t.target_global AND cardinality(t.target_server_ids) BETWEEN 1 AND 2
AND j.source_server_id=ANY($5::int[]) AND (j.destination_server_id IS NULL OR j.destination_server_id=ANY($5::int[]))
AND NOT EXISTS(SELECT 1 FROM public.mcp_operations WHERE id=r.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=r.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))
LIMIT 2`
const sid=z.number().int().positive().safe()
const ids=z.array(z.string().regex(/^[1-9]\d{0,15}$/).transform(Number).pipe(sid)).min(1).max(2)
export async function findRetiredServerProbeHistory(query:Query,input:ServerProbeRetiredLookup) {
const rows=await query(SQL,[input.receiptId,input.operationId,input.connectionId,input.ownerId,input.scopeServerIds])
if(rows.length!==1)return
const row=rows[0]
const source=await openServerProbeRetiredSource({source:row.source,proof:row.proof}).catch(error=>{
if(error instanceof ServerProbeRetirementSourceRefused)return
throw error
})
const parsedScope=ids.safeParse(row.tombstone_server_ids)
if(!source||!parsedScope.success)return
const scope=parsedScope.data.sort((a,b)=>a-b)
if(source.receiptId!==input.receiptId||source.executionId!==input.operationId||source.connectionId!==input.connectionId||
source.ownerId!==input.ownerId||source.planId!==row.plan_id||fingerprint(source.serverIds)!==fingerprint(scope)||
source.serverIds.some(id=>!input.scopeServerIds.includes(id))||source.sourceServerId!==row.source_server_id||
(source.destinationServerId??null)!==row.destination_server_id||source.tool!==row.tool||
source.originalCredentialId!==row.mcp_credential_id||String(source.issuedAt)!==String(row.authority_issued_at)||
source.bindingProof!==row.binding_proof||source.receiptProof!==row.receipt_proof||source.retentionDays!==row.retention_days||
source.safetyMs!==row.safety_ms||source.authorityExpiresAt!==row.authority_expires_at||source.gcNotBefore!==row.gc_not_before)return
if(row.payload_purged===true){
if(row.payload_proof!==null||row.payload_sha256!==null)return
}else if(row.payload_purged!==false||row.payload_proof!==source.receiptProof||row.payload_sha256!==source.ciphertextSha256)return
return {source,payloadPurged:row.payload_purged}
}
@@ -0,0 +1,50 @@
import { randomUUID } from "node:crypto"
import { encryptSecret,fingerprint,privateSecretFingerprint,redact } from "../modules/mcp/security.js"
import { serverProbeReceiptMetadata } from "../modules/mcp/server-probe-actions.js"
import type { ServerProbeReceipt } from "./server-probe-execution.js"
import type { ServerProbeHistoryBinding } from "./server-probe-journal-history.js"
export async function serverProbeRetiredFixture(twoSides=false, identity?:{connectionId:string;credentialId:string;ownerId:string},
historical={base:Date.parse("2025-01-02T00:00:00Z"),retentionDays:365}){
const operationId=randomUUID(),receiptId=randomUUID(),connectionId=identity?.connectionId??randomUUID(),credentialId=identity?.credentialId??randomUUID(),ownerId=identity?.ownerId??randomUUID()
const {base,retentionDays}=historical,stamp=(ms:number)=>new Date(base+ms).toISOString()
const originalTimes={issuedAt:base,authorityExpiresAt:base+15000}
const tool=twoSides?"bandwidth":"ping",sourceServerId=9,destination=twoSides?{destinationServerId:11}:{}
const prepared={receiptId,tool,serverId:sourceServerId,...destination,expiresAt:stamp(15000),bindingCiphertext:"inert-private-inner-binding"}
const binding={version:1,operation:"servers.probes.run",principal:{connectionId,credentialId,credentialExpiresAt:stamp(30000)},
ownerId,...originalTimes,prepared,bindingProof:"a".repeat(64),stageBudget:twoSides?1:2}
const proof=(domain:string,value:unknown)=>privateSecretFingerprint(fingerprint(value),domain)
const input={serverId:sourceServerId,receiptId,inputDigest:await proof("server-probe-mcp-binding-v1",binding),
bindingCiphertext:await encryptSecret(JSON.stringify(binding))}
const step={kind:"service",service:binding.operation,input}
const serverIds=twoSides?[9,11]:[9]
const evidence={steps:[step],requiresHuman:true,verificationScope:"acknowledgement",mutationCost:binding.stageBudget,
target:{scope:"servers",serverIds},validUntil:prepared.expiresAt}
const context:ServerProbeHistoryBinding["context"]={identity:{kind:"mcp",operationName:"servers.probes.run",ownerId,
connectionId,credentialId,executionId:operationId,issuedAt:base,expiresAt:base+15000},sourceServerId,...destination,tool}
const receipt:ServerProbeReceipt={receiptId,serverId:sourceServerId,...destination,tool,state:"completed",httpStatus:200,
attemptedStages:1,completedStages:1,output:"INERT_RESULT_ONLY_IN_ORIGINAL_PAYLOAD",data:{kind:tool},automaticReplay:false,forwardingVerified:false}
const metadata=serverProbeReceiptMetadata(receipt,operationId,binding.stageBudget)
const plaintext=JSON.stringify({version:1,context,bindingProof:binding.bindingProof,receipt})
const payload={receipt_id:receiptId,receipt_proof:await proof("server-probe-journal-v1:receipt",plaintext),receipt_ciphertext:await encryptSecret(plaintext)}
const anchor={receipt_id:receiptId,binding_proof:binding.bindingProof,identity_kind:"mcp",execution_id:operationId,
actor_id:ownerId,authentication:"mcp",mcp_connection_id:connectionId,mcp_credential_id:credentialId,request_id:null,
authority_issued_at:String(base),authority_expires_at:stamp(15000),source_server_id:sourceServerId,
destination_server_id:destination.destinationServerId??null,tool,state:"completed",receipt_proof:payload.receipt_proof,
attempted_stages:1,completed_stages:1,created_at:stamp(500),finished_at:stamp(1000)}
const raw={
original:{operation_id:operationId,connection_id:connectionId,owner_id:ownerId,plan_id:randomUUID(),
operation_name:binding.operation,operation_version:1,operation_state:"applied",operation_verification:"inconclusive",operation_error_code:null,
operation_result:{confirmedWrites:0,verificationScope:"acknowledgement",completedServiceSteps:1,diagnosticReceipt:metadata},
plan_state:"completed",approved_by:ownerId,evidence:{sealed:await encryptSecret(JSON.stringify(evidence))},
step_index:0,step_state:"acknowledged",intended_state:redact(step),step_result:metadata,
operation_created_at:stamp(100),operation_finished_at:stamp(1500),plan_created_at:stamp(10),plan_approved_at:stamp(50),
plan_expires_at:stamp(16000),step_created_at:stamp(200),step_finished_at:stamp(1400)},
admission:{receipt_id:receiptId,owner_id:ownerId,identity_kind:"mcp",execution_id:operationId,binding_proof:binding.bindingProof,
authority_expires_at:stamp(15000),claim_token:null,claim_attempted_at:stamp(400),purged_at:null,cancelled_at:null,created_at:stamp(300),
retention_days:retentionDays,safety_ms:60000,gc_not_before:stamp(15000+retentionDays*86400000+60000)},
anchor,payload,stages:[{receipt_id:receiptId,stage_index:1,kind:twoSides?"native-bandwidth":"native-ping",timeout_ms:1000,
state:"received",outcome:"received",created_at:stamp(600),received_at:stamp(900)}],quarantineCount:0,cancellationCount:0,
}
return {raw,context,receipt,serverIds,stamp}
}
@@ -0,0 +1,77 @@
import assert from "node:assert/strict"
import { findRetiredServerProbeHistory } from "./server-probe-retired-history.js"
import { randomUUID } from "node:crypto"
import { mkdtemp,rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"
import { test } from "node:test"
import { serverProbeRetiredFixture as fixture } from "./server-probe-retired-source.fixture.js"
import { createServerProbeJournalHistoryReader } from "./server-probe-journal-history.js"
import { openServerProbeRetiredSource,projectServerProbeRetiredSource,ServerProbeRetirementSourceRefused } from "./server-probe-retired-source.js"
test("production proofs authenticate the minimal projection and keep the original payload readable only with the current fence",async()=>{
const directory=await mkdtemp(path.join(tmpdir(),"owned-probe-retirement-"))
const previous=process.env.MCP_STATE_DIR
process.env.MCP_STATE_DIR=directory
try{
for(const twoSides of [false,true]){
const {raw,context,receipt,serverIds}=await fixture(twoSides)
const exact=structuredClone(raw),signed=await projectServerProbeRetiredSource(raw)
assert.deepEqual(raw,exact)
const opened=await openServerProbeRetiredSource(JSON.parse(JSON.stringify(signed)))
assert.deepEqual(opened,signed.source)
assert.deepEqual(opened.serverIds,serverIds)
assert.equal(opened.authorityExpiresAt,"2025-01-02T00:00:15.000000Z")
assert.equal(opened.terminalAt,"2025-01-02T00:00:16.000000Z")
assert.equal(opened.activityAt,opened.terminalAt)
assert.equal(opened.retentionDays,365)
assert.equal(opened.gcNotBefore,"2026-01-02T00:01:15.000000Z")
const lookup={operationId:opened.executionId,receiptId:opened.receiptId,connectionId:opened.connectionId,ownerId:opened.ownerId,scopeServerIds:serverIds}
const stored={...signed,...raw.anchor,plan_id:raw.original.plan_id,tombstone_server_ids:serverIds.map(String),
retention_days:raw.admission.retention_days,safety_ms:raw.admission.safety_ms,
gc_not_before:raw.admission.gc_not_before.replace("Z","000Z"),authority_expires_at:raw.anchor.authority_expires_at.replace("Z","000Z"),
payload_purged:false,payload_proof:raw.payload.receipt_proof,payload_sha256:opened.ciphertextSha256}
const read=(row:Record<string,unknown>)=>findRetiredServerProbeHistory(async (_sql,values)=>{
assert.deepEqual(values,[lookup.receiptId,lookup.operationId,lookup.connectionId,lookup.ownerId,lookup.scopeServerIds])
return [row]
},lookup)
assert.deepEqual(await read(stored),{source:opened,payloadPurged:false})
assert.deepEqual(await read({...stored,payload_purged:true,payload_proof:null,payload_sha256:null}),{source:opened,payloadPurged:true})
for(const alteration of [{tombstone_server_ids:["800"]},{plan_id:randomUUID()},{gc_not_before:raw.admission.authority_expires_at},
{mcp_credential_id:randomUUID()},{payload_sha256:"d".repeat(64)},{payload_purged:true}])
assert.equal(await read({...stored,...alteration}),undefined)
assert.equal(await read({...stored,proof:"d".repeat(64)}),undefined)
const envelope=JSON.stringify(signed)
for(const secret of [raw.payload.receipt_ciphertext,raw.original.evidence.sealed,receipt.output!,"inert-private-inner-binding"])
assert(!envelope.includes(secret))
let permitted=true
const reader=createServerProbeJournalHistoryReader({
assertReadAuthority:async()=>{if(!permitted)throw Error("current scope or protection denied")},
loadArchive:async()=>[{...raw.anchor,receipt_ciphertext:raw.payload.receipt_ciphertext,quarantine_count:0}],
})
const binding={context,receiptId:opened.receiptId,bindingProof:opened.bindingProof}
assert.deepEqual(await reader.read(binding),receipt)
permitted=false
await assert.rejects(reader.read(binding),/current scope or protection denied/)
for(const alteration of [{retentionDays:364},{activityAt:"2025-01-02T00:00:17.000000Z"},{ownerId:randomUUID()}])
await assert.rejects(openServerProbeRetiredSource({...signed,source:{...signed.source,...alteration}}),ServerProbeRetirementSourceRefused)
}
const {raw}=await fixture()
for(const alter of [
(copy:typeof raw)=>{copy.original.evidence.sealed="broken"},
(copy:typeof raw)=>{copy.payload.receipt_proof="b".repeat(64)},
(copy:typeof raw)=>{copy.original.step_result.sha256="b".repeat(64)},
(copy:typeof raw)=>{copy.stages[0].received_at="2025-01-02T00:00:00.000001Z"},
(copy:typeof raw)=>{copy.admission.authority_expires_at="2025-01-02T00:00:15.000001Z"},
(copy:typeof raw)=>{copy.quarantineCount=1},
(copy:typeof raw)=>{copy.cancellationCount=1},
]){
const copy=structuredClone(raw);alter(copy)
await assert.rejects(projectServerProbeRetiredSource(copy),ServerProbeRetirementSourceRefused)
}
}finally{
if(previous===undefined)delete process.env.MCP_STATE_DIR
else process.env.MCP_STATE_DIR=previous
await rm(directory,{recursive:true,force:true})
}
})
@@ -0,0 +1,147 @@
/** Private codec only. A future caller must own a bounded, locked SQL
* snapshot, and readers must still prove current scope and protections. */
import { createHash } from "node:crypto"
import { z } from "zod"
import { authenticateServerProbeExecutionSourceRow } from "../modules/mcp/diagnostic-cancellation-source.js"
import { serverProbeReceiptMetadata } from "../modules/mcp/server-probe-actions.js"
import { serviceDiagnosticReceipt } from "../modules/mcp/service-diagnostic-progress.js"
import { constantEqual, decryptSecretExisting, fingerprint, privateFingerprinterExisting } from "../modules/mcp/security.js"
import { createLegacyServerProbeVerifier } from "./server-probe-legacy-verifier.js"
import { validateServerProbeJournalReceipt } from "./server-probe-journal.js"
import type { ServerProbeReceipt } from "./server-probe-execution.js"
import { canonicalPollArchiveTimestamp } from "./server-poll-terminal-archive.js"
const purpose="server-probe-retired-source-v1"
const uuid=z.string().uuid(),hash=z.string().regex(/^[0-9a-f]{64}$/),sid=z.number().int().positive().safe()
const time=z.string().transform(canonicalPollArchiveTimestamp)
const tool=z.enum(["ping","traceroute","bandwidth","dns","route","mtu"])
const record=z.record(z.string(),z.unknown())
const originalSchema=z.object({
operation_id:uuid,connection_id:uuid,owner_id:z.string().min(1).max(200),plan_id:uuid,
operation_name:z.literal("servers.probes.run"),operation_version:z.literal(1),operation_state:z.literal("applied"),
operation_verification:z.literal("inconclusive"),operation_error_code:z.null(),operation_result:z.unknown(),
plan_state:z.literal("completed"),approved_by:z.string().min(1).max(200),evidence:record,
step_index:z.literal(0),step_state:z.literal("acknowledged"),intended_state:record,step_result:record,
operation_created_at:time,operation_finished_at:time,plan_created_at:time,plan_approved_at:time,
plan_expires_at:time,step_created_at:time,step_finished_at:time,
}).strict()
const admissionSchema=z.object({
receipt_id:uuid,owner_id:z.string().min(1).max(200),identity_kind:z.literal("mcp"),execution_id:uuid,binding_proof:hash,
authority_expires_at:time,claim_token:z.null(),claim_attempted_at:time,purged_at:z.null(),cancelled_at:z.null(),created_at:time,
retention_days:z.number().int().min(1).max(365),safety_ms:z.number().int().min(60000).max(86400000),gc_not_before:time,
}).strict()
const sourceSchema=z.object({
original:originalSchema,admission:admissionSchema,anchor:record,
payload:z.object({receipt_id:uuid,receipt_proof:hash,receipt_ciphertext:z.string().min(1).max(262144)}).strict(),
stages:z.array(record).min(1).max(11),quarantineCount:z.literal(0),cancellationCount:z.literal(0),
}).strict()
const signedSchema=z.object({
version:z.literal(1),receiptId:uuid,executionId:uuid,connectionId:uuid,ownerId:z.string().min(1).max(200),planId:uuid,
operation:z.literal("servers.probes.run"),operationVersion:z.literal(1),originalCredentialId:uuid,
sourceServerId:sid,destinationServerId:sid.optional(),tool,serverIds:z.array(sid).min(1).max(2),
issuedAt:z.number().int().nonnegative().safe(),authorityExpiresAt:time,terminalAt:time,activityAt:time,gcNotBefore:time,
retentionDays:z.number().int().min(1).max(365),safetyMs:z.number().int().min(60000).max(86400000),
bindingProof:hash,sourceDigest:hash,terminalDigest:hash,stageSnapshotDigest:hash,receiptProof:hash,ciphertextSha256:hash,
}).strict().refine(value=>{
const expected=[...new Set([value.sourceServerId,...(value.destinationServerId===undefined?[]:[value.destinationServerId])])].sort((a,b)=>a-b)
return fingerprint(expected)===fingerprint(value.serverIds)
&& (value.tool==="bandwidth" ? value.destinationServerId!==undefined&&value.destinationServerId!==value.sourceServerId : value.destinationServerId===undefined)
&& value.issuedAt<Date.parse(value.authorityExpiresAt) && value.terminalAt<=value.activityAt
&& value.authorityExpiresAt<=value.activityAt
&& micros(value.gcNotBefore)>=micros(value.authorityExpiresAt)+BigInt(value.retentionDays*86400000+value.safetyMs)*1000n
})
export type ServerProbeRetiredSource = z.output<typeof signedSchema>
/** Match the exact canonical live terminal projection after payload expiry. */
export function matchesServerProbeRetiredTerminal(source: ServerProbeRetiredSource, raw: unknown): boolean {
const parsed = originalSchema.safeParse(raw)
return parsed.success && parsed.data.operation_id === source.executionId && parsed.data.plan_id === source.planId
&& parsed.data.connection_id === source.connectionId && parsed.data.owner_id === source.ownerId
&& fingerprint(parsed.data) === source.terminalDigest
}
export interface SignedServerProbeRetiredSource { source: ServerProbeRetiredSource; proof: string }
export class ServerProbeRetirementSourceRefused extends Error {
readonly automaticReplay=false
constructor(){super("Diagnostic retirement source cannot be authenticated");this.name="ServerProbeRetirementSourceRefused"}
}
function requireThat(value:unknown):asserts value {if(!value)throw new ServerProbeRetirementSourceRefused()}
function micros(value:string){return BigInt(Date.parse(value))*1000n+BigInt(value.slice(23,26))}
const maximum=(...values:string[])=>values.sort().at(-1)!
/** Produces no operation/payload/cancellation mutation or public authorization.
* Only settled successful sources with the original retained payload qualify. */
export async function projectServerProbeRetiredSource(raw:unknown):Promise<SignedServerProbeRetiredSource>{
try{
requireThat(Buffer.byteLength(JSON.stringify(raw))<=3*1048576)
const source=sourceSchema.parse(structuredClone(raw)),{original,admission,anchor,payload,stages}=source
const authenticated=await authenticateServerProbeExecutionSourceRow(original,original.operation_id,
{connectionId:original.connection_id,ownerId:original.owner_id})
requireThat(authenticated.terminal&&authenticated.sourceState==="applied")
const context=authenticated.originalContext,id=context.identity
requireThat(admission.receipt_id===authenticated.scope.receiptId&&admission.execution_id===id.executionId&&
admission.owner_id===id.ownerId&&admission.binding_proof===authenticated.scope.bindingProof&&
micros(admission.authority_expires_at)===BigInt(id.expiresAt)*1000n&&time.parse(anchor.authority_expires_at)===admission.authority_expires_at&&
payload.receipt_id===admission.receipt_id&&
payload.receipt_proof===anchor.receipt_proof&&anchor.state==="completed")
const verify=createLegacyServerProbeVerifier({loadMcpSource:async operationId=>{
requireThat(operationId===id.executionId)
return authenticated
}})
const verified=await verify({identityKind:"mcp",executionId:id.executionId,
anchor:{...anchor,receipt_ciphertext:payload.receipt_ciphertext},stages})
requireThat(verified.status==="verified"&&verified.authentication==="receipt"&&verified.bindingAuthenticated&&
verified.progressAuthenticated&&verified.terminal&&!verified.quarantined&&!verified.cancelled)
// The same ciphertext has already passed the existing receipt MAC and
// original-context checks above. Match its actual public effect as well.
const envelope=JSON.parse(await decryptSecretExisting(payload.receipt_ciphertext)) as {receipt:ServerProbeReceipt}
const receipt=validateServerProbeJournalReceipt(context,admission.receipt_id,envelope.receipt)
requireThat(receipt.state==="completed")
const metadata=serverProbeReceiptMetadata(receipt,id.executionId,authenticated.stageBudget)
const confirmed=serviceDiagnosticReceipt(metadata,true,id.executionId,authenticated.stageBudget)
requireThat(confirmed&&fingerprint(original.step_result)===fingerprint(metadata)&&fingerprint(original.operation_result)===fingerprint({
confirmedWrites:0,verificationScope:"acknowledgement",completedServiceSteps:1,diagnosticReceipt:confirmed,
}))
// The original receipt authenticates context and progress, not SQL row
// timestamps. Sign the exact observed locked snapshot and its chronology,
// without upgrading the historical journal's stage or timestamp proofs.
const anchorCreated=time.parse(anchor.created_at),anchorFinished=time.parse(anchor.finished_at)
requireThat(original.plan_created_at<=original.plan_approved_at&&original.plan_approved_at<=original.operation_created_at&&
original.operation_created_at<original.plan_expires_at&&original.operation_created_at<=original.step_created_at&&
original.step_created_at<=admission.created_at&&admission.created_at<=admission.claim_attempted_at&&
admission.claim_attempted_at<=anchorCreated&&anchorCreated<=anchorFinished&&anchorFinished<admission.authority_expires_at&&
anchorFinished<=original.step_finished_at&&original.step_finished_at<=original.operation_finished_at)
let stageActivity=anchorCreated
for(const stage of stages){
const created=time.parse(stage.created_at),received=time.parse(stage.received_at)
requireThat(stage.state==="received"&&stageActivity<=created&&created<=received&&received<=anchorFinished)
stageActivity=received
}
const terminalAt=maximum(original.operation_finished_at,original.plan_expires_at)
const result=signedSchema.parse({
version:1,receiptId:admission.receipt_id,executionId:id.executionId,connectionId:id.connectionId,ownerId:id.ownerId,
planId:original.plan_id,operation:"servers.probes.run",operationVersion:1,originalCredentialId:id.credentialId,
sourceServerId:context.sourceServerId,...(context.destinationServerId===undefined?{}:{destinationServerId:context.destinationServerId}),
tool:context.tool,serverIds:authenticated.serverIds,
issuedAt:id.issuedAt,authorityExpiresAt:admission.authority_expires_at,terminalAt,
activityAt:maximum(terminalAt,admission.authority_expires_at,admission.created_at,anchorFinished,stageActivity),
gcNotBefore:admission.gc_not_before,retentionDays:admission.retention_days,safetyMs:admission.safety_ms,
bindingProof:authenticated.scope.bindingProof,sourceDigest:authenticated.sourceDigest,terminalDigest:fingerprint(original),
stageSnapshotDigest:fingerprint(stages),receiptProof:payload.receipt_proof,
ciphertextSha256:createHash("sha256").update(payload.receipt_ciphertext).digest("hex"),
})
const sign=await privateFingerprinterExisting(purpose)
return {source:result,proof:sign(fingerprint(result))}
}catch{throw new ServerProbeRetirementSourceRefused()}
}
/** Authenticates the private projection. It never grants current access or
* authorizes the retained ciphertext without a fresh SQL identity read. */
export async function openServerProbeRetiredSource(raw:unknown):Promise<ServerProbeRetiredSource>{
try{
requireThat(Buffer.byteLength(JSON.stringify(raw))<=8192)
const {source,proof}=z.object({source:signedSchema,proof:hash}).strict().parse(structuredClone(raw))
const sign=await privateFingerprinterExisting(purpose)
requireThat(constantEqual(proof,sign(fingerprint(source))))
return source
}catch{throw new ServerProbeRetirementSourceRefused()}
}
@@ -4,7 +4,7 @@ import { pool } from "../db/index.js"
import { getServerRowById, type ServerRow } from "../modules/servers/repository/servers-repository.js"
import { fingerprint, McpError, privateFingerprinter } from "../modules/mcp/security.js"
import { readWireguardPeerDeletionState, wireguardPeerDeletionTarget, wireguardConfigurationRows, type WireguardPeerDeletionState } from "./wireguard-peer-delete.js"
import { greDependencyPaths, greStableRows, assertGreDeleteDependencies } from "./gre-mutation-state.js"
import { greStableRows, assertGreDeleteDependencies } from "./gre-mutation-state.js"
import { observedBoolean } from "./ospf-observed-values.js"
import { MikrotikClient } from "./mikrotik.js"
import { withApplicationMutationLock, withConfigMutationLock } from "./config-mutation-lock.js"
@@ -12,6 +12,8 @@ import { executeGuardedApplicationWrite } from "./config-mutation-policy.js"
import { captureAndAppendRevision } from "./config-revisions.js"
import { captureWireguardSnapshot } from "./wireguard-live.js"
import { sameWireguardEndpoint } from "./wireguard-source.js"
import { wireguardInterfaceDependencyPaths, wireguardInterfaceRuntimeDependencyPaths } from "./routeros-native-paths.js"
export { wireguardInterfaceDependencyPaths } from "./routeros-native-paths.js"
type Row = Record<string, string>
export interface WireguardInterfaceDeleteState {
@@ -19,8 +21,7 @@ export interface WireguardInterfaceDeleteState {
filters: { id: number; gatewayTunnelId: string; gateway: string }[]; proof: string;
}
export interface WireguardInterfaceDeleteCommand { method: "PATCH" | "DELETE"; path: string; resource: "interfaces" | "peers" | "ipv4" | "ipv6"; id: string; body?: Record<string, string> }
const runtimePaths = new Set(["/routing/bgp/session", "/routing/ospf/neighbor", "/routing/ospf/interface", "/routing/bfd/session"])
export const wireguardInterfaceDependencyPaths = [...new Set<string>([...greDependencyPaths, "/interface/vxlan", "/interface/vrrp", "/interface/bonding", "/interface/eoip", "/interface/gre", "/interface/ipip", "/ip/dhcp-server", "/ip/dhcp-client", "/ipv6/dhcp-server", "/ipv6/dhcp-client", "/ip/ipsec/peer", "/ip/ipsec/policy", ...runtimePaths])]
const runtimePaths = new Set<string>(wireguardInterfaceRuntimeDependencyPaths)
const fail = (code: string, message: string) => new McpError(code, message, ["STATE_UNAVAILABLE", "MUTATION_OUTCOME_UNKNOWN", "VERIFICATION_FAILED"].includes(code) ? 502 : 409)
const automaticLinkLocal = (row: Row) => observedBoolean(row.dynamic) === true && /^fe80:/i.test(row.address ?? "")
const ordered = (path: string) => ["/ip/vrf", "/routing/rule", "/routing/filter/rule", "/routing/bfd/configuration"].includes(path) || /^\/(?:ip|ipv6)\/firewall\//.test(path)
@@ -66,3 +66,54 @@ test("same-tuple pre-refresh packet still decodes with previous template and ref
assert.throws(() => checkChrCapture(input), /reused the previous template/)
assert.equal(checkChrCapture(fixture()).captureReplayMatched, true)
})
function concatenateSets(version: number, first: string, second: string, reversed = false) {
const header = version === 9 ? 20 : 16
const left = Buffer.from(first, "hex"), right = Buffer.from(second, "hex")
const values = [left.subarray(header), right.subarray(header)]
if (reversed) values.reverse()
const packet = Buffer.concat([left.subarray(0, header), ...values])
packet.writeUInt16BE(version === 9 ? 2 : packet.length, 2)
return packet
}
function bundledFixture() {
const input = fixture()
input.frames = [0, 1, 4, 5].map((index, ordinal) => {
const first = input.frames[index], second = input.frames[index + 2]
return { ...first, ordinal: ordinal + 1, expected: second.expected,
payloadHex: concatenateSets(input.exporters[first.exporter].version, first.payloadHex, second.payloadHex).toString("hex") }
})
return input
}
test("v9 and IPFIX template plus data datagrams prove both phases with the current product parser", () => {
const result = checkChrCapture(bundledFixture())
assert.equal(result.frames, 4)
assert.deepEqual(result.exporters.map(row => [row.baselineRecords, row.refreshedRecords]), [[1, 1], [1, 1]])
const multipleData = fixture()
const row = multipleData.frames[3]
row.payloadHex = concatenateSets(10, row.payloadHex, row.payloadHex).toString("hex")
row.expected = [...row.expected, ...row.expected]
assert.equal(checkChrCapture(multipleData).exporters[1].baselineRecords, 2)
})
test("rejects stale data before a refresh, foreign sets, trailing bytes and v9 counts that hide a set", () => {
const original = fixture()
const reversed = bundledFixture()
reversed.frames[2].payloadHex = concatenateSets(9, original.frames[4].payloadHex, original.frames[6].payloadHex, true).toString("hex")
assert.throws(() => checkChrCapture(reversed), /template order/)
const foreign = bundledFixture()
const foreignPacket = Buffer.from(foreign.frames[1].payloadHex, "hex")
foreignPacket.writeUInt16BE(257, 16 + foreignPacket.readUInt16BE(18))
foreign.frames[1].payloadHex = foreignPacket.toString("hex")
assert.throws(() => checkChrCapture(foreign), /data set ID/)
const trailing = bundledFixture()
trailing.frames[0].payloadHex += "00"
assert.throws(() => checkChrCapture(trailing), /set count or header/)
const hidden = bundledFixture()
const hiddenPacket = Buffer.from(hidden.frames[0].payloadHex, "hex")
hiddenPacket.writeUInt16BE(1, 2)
hidden.frames[0].payloadHex = hiddenPacket.toString("hex")
assert.throws(() => checkChrCapture(hidden), /v9 set coverage/)
})
@@ -6,7 +6,7 @@ import { resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { isDeepStrictEqual } from "node:util"
import { z } from "zod"
import { clearFlowTemplateCache, FlowTemplateRefused, parseFlowPacket } from "../services/traffic-flow-parse.js"
import { clearFlowTemplateCache, FLOW_TEMPLATE_LIMITS, FlowTemplateRefused, parseFlowPacket } from "../services/traffic-flow-parse.js"
const maxInput = 512 * 1024
const ip = z.string().max(45).refine(value => isIP(value) !== 0)
@@ -28,20 +28,65 @@ const capture = z.object({
payloadHex: z.string().min(40).max(8192).regex(/^(?:[0-9a-f]{2})+$/),
kind: z.enum(["template", "data", "refresh", "pre-refresh-probe"]),
expected: z.array(flow).max(64),
}).strict()).min(8).max(64),
}).strict()).min(4).max(64),
}).strict()
function requireEvidence(condition: unknown, reason: string): asserts condition {
if (!condition) throw Error(reason)
}
type Exporter = z.infer<typeof exporter>
const zeroPadding = (bytes: Buffer) => bytes.length <= 3 && bytes.every(value => value === 0)
/** Check the entire datagram envelope without decoding record values. */
function inspectSets(packet: Buffer, source: Exporter, changesTemplate: boolean, previousWidth: number) {
const header = source.version === 9 ? 20 : 16
requireEvidence(packet.length >= header + 4 && packet.readUInt16BE(0) === source.version &&
packet.readUInt32BE(header - 4) === source.domain &&
(source.version === 9 || packet.readUInt16BE(2) === packet.length), "Packet does not match the declared exporter")
let width = previousWidth, templateSeen = false, records = 0, sets = 0
for (let offset = header; offset < packet.length;) {
requireEvidence(packet.length - offset >= 4 && ++sets <= 32, "Invalid set count or header")
const id = packet.readUInt16BE(offset), length = packet.readUInt16BE(offset + 2)
requireEvidence(length >= 4 && offset + length <= packet.length, "Invalid set length")
const body = packet.subarray(offset + 4, offset + length)
offset += length
if (id === (source.version === 9 ? 0 : 2)) {
requireEvidence(changesTemplate && !templateSeen && records === 0 && body.length >= 4 &&
body.readUInt16BE(0) === source.templateId, "Unexpected template identity or order")
const fieldCount = body.readUInt16BE(2)
requireEvidence(fieldCount > 0 && fieldCount <= FLOW_TEMPLATE_LIMITS.perTemplateFields, "Invalid template field count")
let at = 4
width = 0
for (let field = 0; field < fieldCount; field++) {
requireEvidence(body.length - at >= 4, "Truncated template field")
const type = body.readUInt16BE(at), size = body.readUInt16BE(at + 2)
requireEvidence(size > 0 && width + size <= 4096, "Variable or oversized replay template")
width += size
at += source.version === 10 && (type & 0x8000) !== 0 ? 8 : 4
requireEvidence(at <= body.length, "Truncated enterprise template field")
}
requireEvidence(zeroPadding(body.subarray(at)), "Unexpected additional template or padding")
templateSeen = true
} else {
requireEvidence(id === source.templateId && (!changesTemplate || templateSeen), "Unexpected data set ID or template order")
requireEvidence(width > 0 && body.length >= width && zeroPadding(body.subarray(Math.floor(body.length / width) * width)), "Invalid data set records or padding")
records += Math.floor(body.length / width)
requireEvidence(records <= 64, "Replay record budget exceeded")
}
}
requireEvidence(templateSeen === changesTemplate && (source.version !== 9 || packet.readUInt16BE(2) >= sets),
"Template transition or v9 set coverage differs")
return { width, records }
}
/** Synchronous, isolated replay. Call in an offline process with no live parser. */
export function checkChrCapture(input: unknown) {
const parsed = capture.safeParse(input)
requireEvidence(parsed.success, "Invalid capture projection")
const { destination, exporters, frames } = parsed.data
requireEvidence(exporters[0].address !== exporters[1].address && exporters.every(row => row.address !== destination), "Expected two distinct exporter addresses and one destination")
const state = exporters.map(() => ({ phase: 0, baselineRecords: 0, refreshedRecords: 0, missingTemplateObserved: false }))
const state = exporters.map(() => ({ phase: 0, width: 0, baselineRecords: 0, refreshedRecords: 0, missingTemplateObserved: false }))
let lastOrdinal = -1, lastReceivedMs = -1
clearFlowTemplateCache()
try {
@@ -51,19 +96,13 @@ export function checkChrCapture(input: unknown) {
lastReceivedMs = frame.receivedMs
const source = exporters[frame.exporter], current = state[frame.exporter]
const packet = Buffer.from(frame.payloadHex, "hex")
const header = source.version === 9 ? 20 : 16
requireEvidence(packet.length >= header + 8 && packet.readUInt16BE(0) === source.version &&
packet.readUInt32BE(header - 4) === source.domain &&
(source.version === 9 || packet.readUInt16BE(2) === packet.length) &&
packet.readUInt16BE(header + 2) === packet.length - header, "Packet does not match the declared exporter or single-set envelope")
const isTemplate = frame.kind === "template" || frame.kind === "refresh"
requireEvidence(packet.readUInt16BE(header) === (isTemplate ? source.version === 9 ? 0 : 2 : source.templateId), "Unexpected set ID")
if (isTemplate) {
requireEvidence(packet.readUInt16BE(header + 4) === source.templateId && frame.expected.length === 0, "Unexpected template ID or template records")
requireEvidence(frame.kind === "template" ? current.phase === 0 : current.phase === 1 && current.baselineRecords > 0, "Unexpected template transition")
} else {
requireEvidence(current.phase > 0, "Data preceding baseline template")
}
const envelope = inspectSets(packet, source, isTemplate, current.width)
const transport = { sourcePort: source.port, collectorSocketId: "offline_capture_socket" }
if (frame.kind === "pre-refresh-probe") {
requireEvidence(current.phase === 1 && current.baselineRecords > 0 && !current.missingTemplateObserved && frame.expected.length === 0, "Unexpected pre-refresh probe")
@@ -77,10 +116,13 @@ export function checkChrCapture(input: unknown) {
continue
}
const actual = parseFlowPacket(packet, source.address, transport).map(({ src, dst, bytes, packets, inIface, outIface }) => ({ src, dst, bytes, packets, inIface, outIface }))
requireEvidence(isDeepStrictEqual(actual, frame.expected), "Decoded flow records differ from the expected capture projection")
if (isTemplate) current.phase++
else {
requireEvidence(actual.length > 0 && actual.every(row => row.bytes > 0 && row.packets > 0), "Expected positive traffic after the template")
requireEvidence(actual.length === envelope.records && isDeepStrictEqual(actual, frame.expected), "Decoded flow records differ from the expected capture projection")
if (isTemplate) {
current.phase++
current.width = envelope.width
}
if (actual.length) {
requireEvidence(actual.every(row => row.bytes > 0 && row.packets > 0), "Expected positive traffic after the template")
if (current.phase === 1) current.baselineRecords += actual.length
else current.refreshedRecords += actual.length
}
@@ -0,0 +1,111 @@
import assert from "node:assert/strict"
import { EventEmitter } from "node:events"
import { PassThrough } from "node:stream"
import { test } from "node:test"
import { captureOwnedChrPacketsForTests } from "./traffic-flow-chr-capture-process.js"
const scope = { interfaceName: "owned0", sources: ["192.0.2.1", "192.0.2.2"] as const, destination: "192.0.2.100", port: 4739 }
function fixturePcap(linkType: "RAW" | "EN10MB" = "RAW") {
const link = linkType === "EN10MB" ? 14 : 0, frame = 60 + link
const bytes = Buffer.alloc(24 + 2 * frame)
bytes.writeUInt32LE(0xa1b2c3d4)
bytes.writeUInt16LE(2, 4); bytes.writeUInt16LE(4, 6)
bytes.writeUInt32LE(4124 + link, 16); bytes.writeUInt32LE(link ? 1 : 101, 20)
for (const [index, at] of [24, 24 + frame].entries()) {
bytes.writeUInt32LE(1700000000, at); bytes.writeUInt32LE(44 + link, at + 8); bytes.writeUInt32LE(44 + link, at + 12)
if (link) bytes.writeUInt16BE(0x0800, at + 16 + 12)
const ip = bytes.subarray(at + 16 + link, at + frame)
ip[0] = 0x45; ip.writeUInt16BE(44, 2); ip[9] = 17
Buffer.from([192, 0, 2, index + 1, 192, 0, 2, 100]).copy(ip, 12)
ip.writeUInt16BE(2055, 20); ip.writeUInt16BE(scope.port, 22); ip.writeUInt16BE(24, 24)
ip.writeUInt16BE(10, 28); ip.writeUInt16BE(16, 30)
}
return bytes
}
const ready = "tcpdump: listening on owned0, link-type RAW (Raw IP), snapshot length 4124 bytes\n"
type Link = "RAW" | "EN10MB"
function fixture({ mode = "normal", drop = 0, linkType = "RAW", reportedLink = linkType, fileLink = linkType }:
{ mode?: "normal" | "early" | "wrong-interface"; drop?: number; linkType?: Link; reportedLink?: Link; fileLink?: Link } = {}) {
const child = Object.assign(new EventEmitter(), { stdout: new PassThrough(), stderr: new PassThrough(),
kill(signal: "SIGINT" | "SIGKILL") {
events.push(signal)
if (signal === "SIGINT") {
child.stdout.end(fixturePcap(fileLink))
child.stderr.end("2 packets captured\n2 packets received by filter\n" + drop + " packets dropped by kernel\n")
}
queueMicrotask(() => child.emit("close", signal === "SIGINT" ? 0 : null, signal === "SIGINT" ? null : signal))
return true
},
})
const events: string[] = []
const launch = (args: string[]) => {
events.push("launch")
assert.deepEqual(args, ["-n", "-p", "-U", "-i", "owned0", "-y", linkType, "-s", linkType === "RAW" ? "4124" : "4138", "-c", "64", "-w", "-",
"ip and udp and dst host 192.0.2.100 and dst port 4739 and (src host 192.0.2.1 or src host 192.0.2.2)"])
queueMicrotask(() => {
if (mode === "early") child.emit("close", 0, null)
else {
const line = reportedLink === "RAW" ? ready : ready.replace("RAW (Raw IP)", "EN10MB (Ethernet)").replace("4124", "4138")
child.stderr.write(mode === "normal" ? line : line.replace("owned0", "foreign0"))
}
})
return child
}
return { child, launch, events }
}
test("confined capture waits for exact readiness, then checks complete acknowledged two-source pcap", async () => {
const f = fixture()
const result = await captureOwnedChrPacketsForTests(scope, {
deadlineAt: Date.now() + 1000, assertCurrent: async () => { f.events.push("current") },
exercise: async signal => { assert.equal(signal.aborted, false); f.events.push("exercise") },
}, f.launch)
assert.deepEqual(f.events, ["current", "launch", "current", "exercise", "current", "SIGINT"])
assert.deepEqual(result.counts, { captured: 2, received: 2, dropped: 0 })
assert.deepEqual(result.bytes, fixturePcap())
})
test("wrong-interface readiness and early exit never dispatch exercise", async () => {
for (const mode of ["wrong-interface", "early"] as const) {
const f = fixture({ mode })
await assert.rejects(captureOwnedChrPacketsForTests(scope, {
deadlineAt: Date.now() + 100, assertCurrent: async () => {},
exercise: async () => { f.events.push("exercise") },
}, f.launch))
assert.deepEqual(f.events, mode === "early" ? ["launch"] : ["launch", "SIGKILL"])
}
})
test("capture drops refuse evidence and scope revocation prevents exercise", async () => {
const dropped = fixture({ drop: 1 })
await assert.rejects(captureOwnedChrPacketsForTests(scope, {
deadlineAt: Date.now() + 1000, assertCurrent: async () => {}, exercise: async () => {},
}, dropped.launch), /completeness/)
const revoked = fixture()
let checks = 0
await assert.rejects(captureOwnedChrPacketsForTests(scope, {
deadlineAt: Date.now() + 1000, assertCurrent: async () => { if (++checks === 2) throw Error("revoked") },
exercise: async () => { revoked.events.push("exercise") },
}, revoked.launch), /revoked/)
assert.deepEqual(revoked.events, ["launch", "SIGKILL"])
})
test("EN10MB launch, readiness, and file must agree independently", async () => {
const ethScope = { ...scope, linkType: "EN10MB" } as const
const f = fixture({ linkType: "EN10MB" })
const result = await captureOwnedChrPacketsForTests(ethScope, {
deadlineAt: Date.now() + 1000, assertCurrent: async () => {}, exercise: async () => {},
}, f.launch)
assert.deepEqual(result.bytes, fixturePcap("EN10MB"))
const wrongReady = fixture({ linkType: "EN10MB", reportedLink: "RAW" })
await assert.rejects(captureOwnedChrPacketsForTests(ethScope, {
deadlineAt: Date.now() + 100, assertCurrent: async () => {}, exercise: async () => { wrongReady.events.push("exercise") },
}, wrongReady.launch))
assert.deepEqual(wrongReady.events, ["launch", "SIGKILL"])
const wrongFile = fixture({ linkType: "EN10MB", fileLink: "RAW" })
await assert.rejects(captureOwnedChrPacketsForTests(ethScope, {
deadlineAt: Date.now() + 1000, assertCurrent: async () => {}, exercise: async () => {},
}, wrongFile.launch), /pinned link type EN10MB/)
})
@@ -0,0 +1,135 @@
/** Owns a single confined tcpdump child. Used only by an admitted native runner. */
import { spawn } from "node:child_process"
import type { Readable } from "node:stream"
import { isIPv4 } from "node:net"
import { chrCaptureLayout, projectChrWire } from "./traffic-flow-chr-wire.js"
import { prepareChrReplay } from "./traffic-flow-chr-offline-replay.js"
type Scope = Parameters<typeof projectChrWire>[1] & { interfaceName: string }
type Child = {
stdout: Pick<Readable, "on">
stderr: Pick<Readable, "on">
once(event: "error", listener: (error: Error) => void): unknown
once(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown
kill(signal: "SIGINT" | "SIGKILL"): boolean
}
type Launch = (args: string[]) => Child
type Options = {
deadlineAt: number
assertCurrent: () => Promise<void>
exercise: (signal: AbortSignal) => Promise<void>
}
const maxCapture = 512 * 1024
const maxDiagnostics = 8192
const packetCap = 64
function must(value: unknown, reason: string): asserts value {
if (!value) throw Error(reason)
}
function argsFor(scope: Scope, layout: ReturnType<typeof chrCaptureLayout>) {
must(/^[A-Za-z0-9][A-Za-z0-9_.-]{0,14}$/.test(scope.interfaceName) && scope.interfaceName !== "any" && scope.sources.length === 2 &&
[...scope.sources, scope.destination].every(isIPv4) && new Set([...scope.sources, scope.destination]).size === 3 &&
Number.isInteger(scope.port) && scope.port >= 1 && scope.port <= 65535, "Invalid confined capture scope")
const [first, second] = scope.sources
return ["-n", "-p", "-U", "-i", scope.interfaceName, "-y", layout.linkType, "-s", String(layout.snapLength), "-c", String(packetCap), "-w", "-",
"ip and udp and dst host " + scope.destination + " and dst port " + scope.port + " and (src host " + first + " or src host " + second + ")"]
}
function counters(stderr: string) {
const exact = (label: string) => {
const matches = stderr.split(/\r?\n/).map(line => new RegExp("^(\\d+) packets? " + label + "$").exec(line)).filter(match => match !== null)
must(matches.length === 1, "Capture counters are missing or ambiguous")
const value = Number(matches[0][1])
must(Number.isSafeInteger(value) && value >= 0, "Capture counter exceeds integer range")
return value
}
return { captured: exact("captured"), received: exact("received by filter"), dropped: exact("dropped by kernel") }
}
async function run(scope: Scope, options: Options, launch: Launch) {
const pinned = structuredClone(scope), layout = chrCaptureLayout(pinned.linkType)
const args = argsFor(pinned, layout), deadline = options.deadlineAt
must(Number.isSafeInteger(deadline) && deadline > Date.now() && deadline - Date.now() <= 90000, "Invalid capture deadline")
const controller = new AbortController()
const live = () => must(!controller.signal.aborted && Date.now() < deadline, "Capture deadline or ownership ended")
await options.assertCurrent()
live()
const child = launch(args), chunks: Buffer[] = []
let bytes = 0, diagnosticBytes = 0, stderr = "", requestedStop = false, closed = false, failed = false
let ready!: () => void, terminate!: () => void, rejectWork!: (error: Error) => void
const readiness = new Promise<void>(resolve => { ready = resolve })
const workFailed = new Promise<never>((_resolve, reject) => { rejectWork = reject })
// The promise can reject during cleanup after an earlier error was observed.
void workFailed.catch(() => {})
const stopped = new Promise<void>(resolve => { terminate = resolve })
const fail = (reason: string) => {
if (failed) return
failed = true
controller.abort()
rejectWork(Error(reason))
}
child.stdout.on("data", (part: Buffer) => {
bytes += part.length
if (bytes > maxCapture) { fail("Capture exceeded byte limit"); return }
chunks.push(Buffer.from(part))
})
child.stderr.on("data", (part: Buffer) => {
diagnosticBytes += part.length
if (diagnosticBytes > maxDiagnostics) { fail("Capture diagnostics exceeded byte limit"); return }
stderr += part.toString("utf8")
const expected = "tcpdump: listening on " + pinned.interfaceName + ", link-type " + layout.linkType
if (stderr.split(/\r?\n/).some(line => line.startsWith(expected + " (") && line.endsWith("snapshot length " + layout.snapLength + " bytes"))) ready()
})
child.once("error", () => fail("Capture child failed to start or signal"))
child.once("close", (code, signal) => {
closed = true
if (!requestedStop || code !== 0 || signal !== null) fail("Capture closed outside its acknowledged stop")
terminate()
})
const timer = setTimeout(() => fail("Capture deadline expired"), Math.max(1, deadline - Date.now()))
let cleanupTimer: ReturnType<typeof setTimeout> | undefined
try {
await Promise.race([readiness, workFailed])
live()
await Promise.race([options.assertCurrent(), workFailed])
live()
await Promise.race([options.exercise(controller.signal), workFailed])
live()
await Promise.race([options.assertCurrent(), workFailed])
live()
requestedStop = true
must(child.kill("SIGINT"), "Capture stop signal was not delivered")
await Promise.race([stopped, workFailed])
live()
const counts = counters(stderr), capture = Buffer.concat(chunks, bytes)
const wire = projectChrWire(capture, { sources: pinned.sources, destination: pinned.destination, port: pinned.port, ...(pinned.linkType === undefined ? {} : { linkType: pinned.linkType }) })
must(counts.dropped === 0 && counts.captured === counts.received && counts.captured === wire.frames.length && counts.captured < packetCap,
"Capture completeness was not established")
return { bytes: capture, sha256: wire.captureSha256, dropped: counts.dropped, complete: true as const, counts, wire }
} finally {
clearTimeout(timer)
controller.abort()
if (!closed) {
child.kill("SIGKILL")
const exited = await Promise.race([stopped.then(() => true), new Promise<false>(resolve => { cleanupTimer = setTimeout(() => resolve(false), 2000) })])
if (cleanupTimer) clearTimeout(cleanupTimer)
must(exited, "Capture child exit could not be confirmed")
}
}
}
export const captureOwnedChrPackets = (scope: Scope, options: Options) => {
must(process.platform === "linux", "Confined tcpdump capture requires Linux")
return run(scope, options, args =>
spawn("tcpdump", args, { shell: false, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LC_ALL: "C" } }))
}
/** Adapter for the capture callback of executeOwnedChrCapture. */
export function createConfinedChrCapture(scope: Scope, options: Omit<Options, "exercise">) {
const pinned = structuredClone(scope)
return async (exercise: (signal?: AbortSignal) => Promise<void>) => {
const capture = await captureOwnedChrPackets(pinned, { ...options, exercise })
return { ...capture, ...prepareChrReplay(capture.wire) }
}
}
/** Explicit inert transport seam. */
export const captureOwnedChrPacketsForTests = run
@@ -0,0 +1,65 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { assessChrCaptureTiming, planChrMeasuredCaptureWindow } from "./traffic-flow-chr-capture-timing.js"
import { CHR_TOPOLOGY_CATALOG_PAIR_GETS, CHR_TOPOLOGY_CATALOG_READ_LIMIT } from "./traffic-flow-chr-topology-inventory.js"
import { CHR_TARGET_MINIMUM_CAPTURE_REMAINING_MS } from "./traffic-flow-chr-target-harness.js"
const current = { pairGets: CHR_TOPOLOGY_CATALOG_PAIR_GETS, catalogueReadLimit: CHR_TOPOLOGY_CATALOG_READ_LIMIT,
captureMs: 90000, targetMinimumRemainingMs: CHR_TARGET_MINIMUM_CAPTURE_REMAINING_MS }
test("reduced exact catalogue count fits the existing cap but does not authorize writes without measurement", () => {
assert.equal(current.pairGets, 86)
const result = assessChrCaptureTiming(current)
assert.equal(result.status, "measurement-required")
assert.equal(result.minimumNodeCatalogueGets, 9460)
const max = assessChrCaptureTiming({ ...current, forwardWrites: 6 })
assert.equal(max.minimumNodeCatalogueGets, 10492)
assert.equal(max.pairsBeforeGenerator, 22)
assert.equal(result.pairsBeforeFirstTargetWindow, 2)
assert.equal(result.setupBudgetMs, 26000)
assert.equal(assessChrCaptureTiming({ ...current, catalogueReadLimit: 8000 }).status, "catalogue-read-limit")
})
const measured = {
childPairMs: 12023, parentPairMs: 12023, directSetupMs: 4000,
collectorBeforeCaptureMs: 45000, collectorAfterCaptureMs: 55000,
beforeCaptureMs: 1400000, afterCaptureMs: 1500000, counterSpanMs: 120000, remainingAuthorityMs: 3600000,
}
test("proposes a finite measured capture window and reserves authority through inverse", () => {
assert.deepEqual(planChrMeasuredCaptureWindow(measured), {
status: "candidate-window", captureMs: 205000, requiredAtCaptureStartMs: 1810000, requiredBeforeFirstWriteMs: 3315000,
requiredCollectorMs: 515000,
})
assert.equal(planChrMeasuredCaptureWindow({ ...measured, remainingAuthorityMs: 3315000 }).status, "authority-window-exceeded")
assert.deepEqual(planChrMeasuredCaptureWindow({ ...measured, childPairMs: 100, parentPairMs: 100 }), {
status: "candidate-window", captureMs: 90000, requiredAtCaptureStartMs: 1695000, requiredBeforeFirstWriteMs: 3200000,
requiredCollectorMs: 400000,
})
})
test("refuses missing or excessive measurements and preserves the counter evidence horizon", () => {
assert.equal(planChrMeasuredCaptureWindow({ ...measured, parentPairMs: undefined }).status, "measurement-required")
assert.equal(planChrMeasuredCaptureWindow({ ...measured, directSetupMs: undefined }).status, "measurement-required")
assert.equal(planChrMeasuredCaptureWindow({ ...measured, childPairMs: 30000 }).status, "capture-window-exceeded")
assert.equal(planChrMeasuredCaptureWindow({ ...measured, counterSpanMs: 180001 }).status, "capture-window-exceeded")
assert.throws(() => planChrMeasuredCaptureWindow({ ...measured, childPairMs: Infinity }), /Invalid CHR measured capture inputs/)
assert.throws(() => planChrMeasuredCaptureWindow({ ...measured, remainingAuthorityMs: 3600001 }), /Invalid CHR measured capture inputs/)
})
test("a long fixture lease still requires measured work within the short MCP credential", () => {
assert.equal(planChrMeasuredCaptureWindow({ ...measured, collectorAfterCaptureMs: undefined }).status, "collector-measurement-required")
assert.deepEqual(planChrMeasuredCaptureWindow({ ...measured, collectorAfterCaptureMs: 140000 }), {
status: "collector-window-exceeded", requiredCollectorMs: 600000,
})
})
test("a larger catalogue budget still requires real representative measurement and refuses observed latency", () => {
const options = { ...current, catalogueReadLimit: 20000 }
assert.equal(assessChrCaptureTiming(options).status, "measurement-required")
// Minimum recorded GET on CHR A/B times 43 each. This is a projection,
// not an assertion about the next live relay transaction.
assert.equal(assessChrCaptureTiming({ ...options, pairedReadMs: 43 * (108 + 263) }).status, "capture-setup-window")
assert.equal(assessChrCaptureTiming({ ...options, pairedReadMs: 100 }).status, "parent-measurement-required")
assert.equal(assessChrCaptureTiming({ ...options, pairedReadMs: 100, parentPairedReadMs: 100 }).status, "budget-check-passed")
})
@@ -0,0 +1,88 @@
import { CHR_COLLECTOR_RESULT_POLL_MS, CHR_STAGED_COLLECTOR_APPROVAL_MS, CHR_STAGED_TARGET_CAPTURE_MAX_MS,
CHR_TARGET_MINIMUM_CAPTURE_REMAINING_MS } from "./traffic-flow-chr-target-timing-profile.js"
/** This model is a lower bound for the current serial read schedule. It can
* refuse admission. Its success never substitutes for live scope or authority. */
export function assessChrCaptureTiming(options: {
pairGets: number
catalogueReadLimit: number
captureMs: number
targetMinimumRemainingMs: number
forwardWrites?: number
pairedReadMs?: number
parentPairedReadMs?: number
}) {
const { pairGets, catalogueReadLimit, captureMs, targetMinimumRemainingMs, forwardWrites = 4, pairedReadMs, parentPairedReadMs } = options
if (![pairGets, catalogueReadLimit, captureMs, targetMinimumRemainingMs].every(value => Number.isSafeInteger(value) && value > 0) ||
![4, 5, 6].includes(forwardWrites) || [pairedReadMs, parentPairedReadMs].some(value =>
value !== undefined && (!Number.isFinite(value) || value <= 0))) {
throw Error("Invalid CHR timing inputs")
}
// Each flow setting/target write takes three fresh reads and a fresh
// topology proof after each. Eight outer pairs include the timing sample.
const pairsBeforeFirstTargetWindow = 2
const pairsBeforeGenerator = pairsBeforeFirstTargetWindow + 3 * forwardWrites + 2
const topologyAcquireAndRestorePairs = 2 * (1 + 2 * 18)
const minimumNodePairs = topologyAcquireAndRestorePairs + pairsBeforeGenerator + 3 * forwardWrites + 8
const minimumNodeCatalogueGets = minimumNodePairs * pairGets
const minimumParentPairsBeforeGenerator = 2 * forwardWrites
const minimumParentCatalogueGetsBeforeGenerator = minimumParentPairsBeforeGenerator * pairGets
const setupBudgetMs = captureMs - targetMinimumRemainingMs
// A measured pair omits parent identity/catalogue extras. Use it only to
// veto an already underbudget capture, with margin for local scheduling.
const projectedSetupMs = pairedReadMs === undefined ? undefined :
Math.ceil((pairsBeforeGenerator * pairedReadMs + minimumParentPairsBeforeGenerator * (parentPairedReadMs ?? 0)) * 1.25)
const status = minimumNodeCatalogueGets > catalogueReadLimit ? "catalogue-read-limit" :
setupBudgetMs <= 0 || (projectedSetupMs !== undefined && projectedSetupMs >= setupBudgetMs) ? "capture-setup-window" :
pairedReadMs === undefined ? "measurement-required" :
parentPairedReadMs === undefined ? "parent-measurement-required" : "budget-check-passed"
return { status, minimumNodeCatalogueGets, minimumParentCatalogueGetsBeforeGenerator,
pairsBeforeFirstTargetWindow, pairsBeforeGenerator, setupBudgetMs, projectedSetupMs } as const
}
/** Offline proposal for the staged disabled-baseline path. Before/after phase
* budgets cover the complete reviewed schedule outside capture, with one
* separate bounded human approval in each phase. */
export function planChrMeasuredCaptureWindow(options: {
childPairMs?: number
parentPairMs?: number
directSetupMs?: number
collectorBeforeCaptureMs?: number
collectorAfterCaptureMs?: number
beforeCaptureMs: number
afterCaptureMs: number
counterSpanMs: number
remainingAuthorityMs: number
}) {
const { childPairMs, parentPairMs, directSetupMs, collectorBeforeCaptureMs, collectorAfterCaptureMs, beforeCaptureMs, afterCaptureMs,
counterSpanMs, remainingAuthorityMs } = options
const valid = (value: number) => Number.isSafeInteger(value) && value > 0 && value <= 3600000
if (![beforeCaptureMs, afterCaptureMs, counterSpanMs, remainingAuthorityMs].every(valid) ||
[childPairMs, parentPairMs, directSetupMs, collectorBeforeCaptureMs, collectorAfterCaptureMs]
.some(value => value !== undefined && !valid(value))) {
throw Error("Invalid CHR measured capture inputs")
}
if (childPairMs === undefined || parentPairMs === undefined || directSetupMs === undefined) {
return { status: "measurement-required" } as const
}
// Two enables, each with full pre-dispatch and post-ACK snapshots on both
// independent paths, plus the child's complete pre-generation fence.
const setupMs = (5 * childPairMs + 4 * parentPairMs + directSetupMs) * 1.25
const captureMs = Math.max(90000,
(Math.floor((CHR_TARGET_MINIMUM_CAPTURE_REMAINING_MS + setupMs) / 1000) + 1) * 1000)
if (captureMs > CHR_STAGED_TARGET_CAPTURE_MAX_MS || counterSpanMs > 180000) {
return { status: "capture-window-exceeded", captureMs } as const
}
const operationMs = CHR_STAGED_COLLECTOR_APPROVAL_MS + CHR_COLLECTOR_RESULT_POLL_MS
if (collectorBeforeCaptureMs === undefined || collectorAfterCaptureMs === undefined) {
return { status: "collector-measurement-required" } as const
}
const requiredCollectorMs = captureMs + 2 * operationMs + collectorBeforeCaptureMs + collectorAfterCaptureMs
if (requiredCollectorMs >= 600000) return { status: "collector-window-exceeded", requiredCollectorMs } as const
const requiredAtCaptureStartMs = captureMs + afterCaptureMs + operationMs
const requiredBeforeFirstWriteMs = beforeCaptureMs + operationMs + requiredAtCaptureStartMs
if (requiredBeforeFirstWriteMs >= remainingAuthorityMs) {
return { status: "authority-window-exceeded", requiredBeforeFirstWriteMs } as const
}
return { status: "candidate-window", captureMs, requiredAtCaptureStartMs, requiredBeforeFirstWriteMs, requiredCollectorMs } as const
}
@@ -0,0 +1,74 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { createDisabledChrFlowHarness } from "./traffic-flow-chr-disabled-baseline.js"
function fixture(lose = false, idless = false) {
const baseline = {
deviceId: "fixture", flow: { ...(idless ? {} : { ".id": "*F" }), enabled: "false", interfaces: "all", untouched: "1m" },
ipfix: { ...(idless ? {} : { ".id": "*A" }), bytes: "false", packets: "true", "src-address": "true", "dst-address": "true",
protocol: "true", "src-port": "true", "dst-port": "true", "in-interface": "true", "out-interface": "true" },
targets: [] as Record<string, unknown>[],
}
const state = structuredClone(baseline)
const writes: Record<string, string>[] = []
const journal: string[] = []
const dispatches: string[] = []
async function change<T>(method: string, path: string, body: Record<string, string | string[]>,
fence?: () => Promise<unknown>): Promise<T> {
await fence?.()
dispatches.push(method + " " + path)
assert.ok(Object.values(body).every(value => typeof value === "string"))
writes.push(body as Record<string, string>)
const row = path.includes("/ipfix/") ? state.ipfix : state.flow
Object.assign(row, Object.fromEntries(Object.entries(body).map(([key, value]) =>
[key, key === "interfaces" ? value : value === "yes" ? "true" : "false"])))
if (lose) throw Error("lost ACK")
return undefined as T
}
const harness = createDisabledChrFlowHarness({
baseline, transitInterface: "transit-test", read: async () => state, authorize: async () => {},
checkpoint: async entry => { journal.push(entry.step + "." + entry.phase) },
native: {
patch: (path, body, _ms, fence) => change("PATCH", path, body, fence),
post: (path, body, _ms, _signal, fence) => change("POST", path, body, fence),
},
})
return { baseline, state, writes, journal, harness, dispatches }
}
test("changes only required fields and one transit interface, then restores the exact disabled inventory", async () => {
const { baseline, state, writes, journal, harness } = fixture()
assert.equal((await harness.enable()).flow.enabled, "true")
assert.deepEqual(await harness.restore(), { restored: true, automaticReplay: false })
assert.deepEqual(state, baseline)
assert.deepEqual(writes, [{ bytes: "yes" }, { enabled: "yes", interfaces: "transit-test" },
{ enabled: "no", interfaces: "all" }, { bytes: "no" }])
assert.deepEqual(journal.filter(value => value.endsWith(".verified")),
["ipfix.enable.verified", "flow.enable.verified", "flow.restore.verified", "ipfix.restore.verified"])
})
test("missing singleton IDs use only fixed POST /set and require ACK before inverse", async () => {
const { baseline, state, dispatches, harness } = fixture(false, true)
await harness.enable()
await harness.restore()
assert.deepEqual(state, baseline)
assert.deepEqual(dispatches, ["POST /ip/traffic-flow/ipfix/set", "POST /ip/traffic-flow/set",
"POST /ip/traffic-flow/set", "POST /ip/traffic-flow/ipfix/set"])
const lost = fixture(true, true)
await assert.rejects(lost.harness.enable(), /requires reconciliation/)
await assert.rejects(lost.harness.restore(), /No verified/)
assert.deepEqual(lost.dispatches, ["POST /ip/traffic-flow/ipfix/set"])
})
test("refuses restore while a target remains and refuses replay after unknown acknowledgement", async () => {
const known = fixture()
await known.harness.enable()
known.state.targets.push({ ".id": "*1" })
await assert.rejects(known.harness.restore(), /baseline changed/)
assert.equal(known.writes.length, 2)
const unknown = fixture(true)
await assert.rejects(unknown.harness.enable(), /requires reconciliation/)
await assert.rejects(unknown.harness.enable(), /already attempted/)
await assert.rejects(unknown.harness.restore(), /No verified/)
assert.equal(unknown.writes.length, 1)
})
@@ -0,0 +1,116 @@
import { runChrWrite, type ChrWriteAdmission } from "./traffic-flow-chr-write-admission.js"
import { isDeepStrictEqual } from "node:util"
import type { MikrotikClient } from "../services/mikrotik.js"
import { encodeRosId } from "../services/routeros-native-paths.js"
type Row = Record<string, unknown>
type Inventory = { deviceId: string; flow: Row; ipfix: Row; targets: Row[] }
type Step = "ipfix.enable" | "flow.enable" | "flow.restore" | "ipfix.restore"
type Options = {
baseline: Inventory
relayWrite?: { side: "A" | "B"; admission: ChrWriteAdmission }
transitInterface: string
read: () => Promise<Inventory>
authorize: () => Promise<void>
verifyContext?: () => Promise<void>
native: Pick<MikrotikClient, "patch" | "post">
checkpoint: (entry: { step: Step; phase: "intent" | "acknowledged" | "verified";
method: "PATCH" | "POST"; path: string; id?: string }) => Promise<void>
}
const fields = ["bytes", "packets", "src-address", "dst-address", "protocol", "src-port", "dst-port", "in-interface", "out-interface"]
const yes = (value: unknown) => value === true || value === "true" || value === "yes"
const no = (value: unknown) => value === false || value === "false" || value === "no"
const base = "/ip/traffic-flow"
function must(value: unknown, message: string): asserts value {
if (!value) throw Error(message)
}
function idOf(row: Row) {
if (!Object.hasOwn(row, ".id")) return undefined
const id = row[".id"]
must(typeof id === "string" && /^\*[a-f0-9]{1,16}$/i.test(id), "Exact Traffic Flow singleton ID required")
return id
}
export function chrDisabledFlowPatchCount(ipfix: Row) {
must(fields.every(key => yes(ipfix[key]) || no(ipfix[key])), "Exact restorable IPFIX fields required")
return fields.some(key => no(ipfix[key])) ? 2 : 1
}
/** Scope is an already proven isolated transit interface. This component owns
* singleton changes only. The separate target owner must remove its ACKed row
* and conserve both singletons before restore is admitted. */
export function createDisabledChrFlowHarness(options: Options) {
const baseline = structuredClone(options.baseline)
const { transitInterface, authorize, verifyContext, read, native, checkpoint, relayWrite } = options
must(baseline.deviceId && baseline.targets.length === 0 && no(baseline.flow.enabled), "Disabled target-free baseline required")
must(/^[a-zA-Z0-9_.-]{1,64}$/.test(transitInterface) && transitInterface !== "all", "Exact transit interface required")
const flowId = idOf(baseline.flow), ipfixId = idOf(baseline.ipfix)
must(typeof baseline.flow.interfaces === "string" && baseline.flow.interfaces.length > 0, "Exact restorable settings required")
chrDisabledFlowPatchCount(baseline.ipfix)
const missing = fields.filter(key => no(baseline.ipfix[key]))
const enableFields = Object.fromEntries(missing.map(key => [key, "yes"]))
const restoreFields = Object.fromEntries(missing.map(key => [key, "no"]))
let phase: "new" | "blocked" | "enabled" | "restored" = "new"
let active: Inventory | undefined
async function current() {
await authorize()
const result = structuredClone(await read())
await verifyContext?.()
await authorize()
return result
}
async function change(step: Step, kind: "flow" | "ipfix", before: Inventory, body: Record<string, string>) {
const id = kind === "flow" ? flowId : ipfixId
const path = (kind === "flow" ? base : base + "/ipfix") + "/" + (id ? encodeRosId(id) : "set")
const method = id ? "PATCH" : "POST"
const record = { step, path, method, ...(id && { id }) } as const
must(isDeepStrictEqual(await current(), before), "Traffic Flow baseline changed")
await checkpoint({ ...record, phase: "intent" })
let dispatched = false
try {
await runChrWrite(relayWrite?.admission, relayWrite && { step: "flow:" + relayWrite.side + ":" + step,
side: relayWrite.side, deviceId: baseline.deviceId, method, path, body }, async arm => {
const beforeDispatch = async () => {
must(!dispatched && isDeepStrictEqual(await current(), before) && !dispatched, "Traffic Flow dispatch refused")
dispatched = true
await arm()
}
if (id) await native.patch(path, body, 15000, beforeDispatch)
else await native.post(path, body, 15000, undefined, beforeDispatch)
})
must(dispatched, "Traffic Flow dispatch fence omitted")
await checkpoint({ ...record, phase: "acknowledged" })
const after = await current()
const changed = after[kind]
must(Object.entries(body).every(([key, value]) => key === "interfaces" ? changed[key] === value :
value === "yes" ? yes(changed[key]) : no(changed[key])), "Traffic Flow settings differ")
const conserved = structuredClone(after)
for (const key of Object.keys(body)) conserved[kind][key] = before[kind][key]
must(isDeepStrictEqual(conserved, before), "Unrelated Traffic Flow settings changed")
await checkpoint({ ...record, phase: "verified" })
return after
} catch (error) {
if (dispatched) throw Error("Traffic Flow write requires reconciliation. Automatic replay forbidden.")
throw error
}
}
return {
async enable() {
must(phase === "new", "Traffic Flow enable already attempted")
phase = "blocked"
const before = missing.length ? await change("ipfix.enable", "ipfix", baseline, enableFields) : baseline
active = await change("flow.enable", "flow", before, { enabled: "yes", interfaces: transitInterface })
phase = "enabled"
return structuredClone(active)
},
async restore() {
must(phase === "enabled" && active, "No verified enabled state to restore")
phase = "blocked"
const after = await change("flow.restore", "flow", active, { enabled: "no", interfaces: String(baseline.flow.interfaces) })
const restored = missing.length ? await change("ipfix.restore", "ipfix", after, restoreFields) : after
must(isDeepStrictEqual(restored, baseline), "Original Traffic Flow baseline not restored")
phase = "restored"
return { restored: true as const, automaticReplay: false as const }
},
}
}
@@ -0,0 +1,116 @@
import assert from "node:assert/strict"
import { mkdtemp, readFile, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { test } from "node:test"
import { chrExportDrainMs, executeDisabledChrCapture, type DisabledChrCaptureOptions } from "./traffic-flow-chr-disabled-runner.js"
test("export drain requires a bounded inactive-flow timeout plus two seconds", () => {
assert.equal(chrExportDrainMs("15s"), 17000)
assert.equal(chrExportDrainMs("00:00:30"), 32000)
for (const value of ["", "0s", "1m1s", "300", undefined]) {
assert.throws(() => chrExportDrainMs(value), /Inactive flow timeout/)
}
})
const cases = {
"create ACK loss": /Target creation outcome unknown/,
"unsealed direct capture": /capture producer stopped/,
"staged capture ACK missing": /capture acknowledgement unavailable/,
"staged enable ACK loss": /Target enable outcome unknown/,
"unsealed staged capture": /capture producer stopped/,
} as const
for (const mode of Object.keys(cases) as (keyof typeof cases)[]) test(mode, async () => {
const staged = mode.includes("staged")
const dir = await mkdtemp(join(tmpdir(), "mm-chr-disabled-"))
const actions: string[] = []
const cleanup: boolean[] = []
let topologyChecks = 0, topologyChecksAtCapture = 0
function router(index: number) {
const name = "nfa012bcde" + (index === 0 ? "ea" : "eb")
const baseline = { deviceId: String(index), flow: { ".id": "*F", enabled: "false", interfaces: "all", "inactive-flow-timeout": "15s" },
ipfix: { ".id": "*A", ...Object.fromEntries(["bytes", "packets", "src-address", "dst-address", "protocol", "src-port",
"dst-port", "in-interface", "out-interface"].map(key => [key, "true"])) }, targets: [] as Record<string, unknown>[] }
const state = structuredClone(baseline)
return { baseline, transitInterface: name, interfaces: [{ id: "*1", name, ifIndex: "1" },
{ id: "*2", name: "nfa012bcde" + (index === 0 ? "ma" : "mb"), ifIndex: "2" }] as const,
read: async () => state, counters: async () => {
if (staged) actions.push("counters:" + index)
return { startedAt: 1, observedAt: 1, rows: [] }
},
native: { async post() { throw Error("unexpected POST") },
async patch<T>(_path: string, body: Record<string, string>, _timeout?: number, fence?: () => Promise<unknown>) {
await fence?.()
actions.push("flow:" + index)
Object.assign(state.flow, body, { enabled: "true" })
return undefined as T
} },
targetNative: { async put<T>(_path: string, body: Record<string, string>, _timeout?: number, fence?: () => Promise<unknown>): Promise<T> {
await fence?.()
actions.push("target:" + index)
if (mode === "create ACK loss") throw Error("lost ACK")
assert.equal(body.disabled, staged ? "yes" : undefined)
state.targets.push({ ...body, ".id": "*C", disabled: body.disabled ?? "false" })
return { ".id": "*C" } as T
}, async patch<T>(path: string, body: Record<string, string>, _timeout?: number, fence?: () => Promise<unknown>) {
await fence?.()
assert.equal(path, "/ip/traffic-flow/target/*C")
assert.deepEqual(body, { disabled: "no" })
assert.equal(state.targets[0]?.disabled, "yes")
state.targets[0].disabled = "false"
actions.push("enable:" + index)
if (mode === "staged enable ACK loss") throw Error("lost ACK")
return { ".id": "*C" } as T
}, async delete() { actions.push("unexpected inverse") } } }
}
const journalPath = join(dir, "owned.jsonl")
try {
const options: DisabledChrCaptureOptions = {
journalPath, deadlineAt: Date.now() + 180000, ...(staged ? { stagedTarget: true as const } : {}),
scope: { sources: ["192.0.2.1", "192.0.2.2"], destination: "192.0.2.3", port: 50000 },
expected: [], serverIds: [1, 2], routers: [router(0), router(1)],
assertTopology: async () => { topologyChecks++ }, authorize: async () => {},
capture: async exercise => {
if (staged) actions.push("capture")
topologyChecksAtCapture = topologyChecks
await exercise(new AbortController().signal, staged && mode !== "staged capture ACK missing"
? { kind: "staged-ipfix-timeout-20s", captureDeadlineAt: Date.now() + 120000 } : undefined)
throw Error("unexpected capture")
},
generate: async () => {
if (staged) {
assert.equal(topologyChecks - topologyChecksAtCapture, 5) // Two per enable, one before generation.
actions.push("generate")
}
throw Error("capture producer stopped before seal")
},
drainCollector: async () => { actions.push("unexpected drain") },
reconcile: async () => {}, reportRouterCleanup: clean => cleanup.push(clean),
}
await assert.rejects(executeDisabledChrCapture(options), cases[mode])
await assert.rejects(executeDisabledChrCapture(options), { code: "EEXIST" })
const prefix = ["flow:0", "target:0", "flow:1", "target:1"]
assert.deepEqual(actions, {
"create ACK loss": prefix.slice(0, 2),
"unsealed direct capture": prefix,
"staged capture ACK missing": [...prefix, "capture"],
"staged enable ACK loss": [...prefix, "capture", "enable:0"],
"unsealed staged capture": [...prefix, "capture", "enable:0", "enable:1", "counters:0", "counters:1", "generate"],
}[mode])
assert.deepEqual(cleanup, [false])
const journal = (await readFile(journalPath, "utf8")).trim().split("\n").map(line => JSON.parse(line))
if (staged && mode !== "staged capture ACK missing") {
assert(journal.find(entry => entry.phase === "capture-ready"))
}
assert.deepEqual(journal.at(-1), {
"create ACK loss": { device: 0, action: "create", phase: "intent" },
"unsealed direct capture": { device: 1, action: "create", phase: "verified", id: "*C" },
"staged capture ACK missing": { phase: "targets-staged-disabled" },
"staged enable ACK loss": { device: 0, action: "enable", phase: "intent", id: "*C" },
"unsealed staged capture": { device: 1, action: "enable", phase: "verified", id: "*C" },
}[mode])
} finally {
await rm(dir, { recursive: true, force: true })
}
})
@@ -0,0 +1,224 @@
import { open } from "node:fs/promises"
import { dirname } from "node:path"
import { setTimeout as delay } from "node:timers/promises"
import { createDisabledChrFlowHarness } from "./traffic-flow-chr-disabled-baseline.js"
import { assertChrTargetTimerWindow, createChrTargetHarness, type ChrStagedTargetTimerProfile, type ChrTargetTimerProfile } from "./traffic-flow-chr-target-harness.js"
import { verifyChrOwnedWgEvidence } from "./traffic-flow-chr-owned-wg-evidence.js"
import { projectChrWire } from "./traffic-flow-chr-wire.js"
import type { ChrWriteAdmission } from "./traffic-flow-chr-write-admission.js"
type Flow = Parameters<typeof createDisabledChrFlowHarness>[0]
type Target = Parameters<typeof createChrTargetHarness>[0]
type Evidence = Parameters<typeof verifyChrOwnedWgEvidence>[0]
type Counters = Evidence["exporters"][0]["before"]
type InterfacePins = Evidence["exporters"][0]["interfaces"]
type Router = Pick<Flow, "baseline" | "read" | "transitInterface" | "native"> & {
targetNative: Target["native"]
interfaces: InterfacePins
counters: () => Promise<Counters>
}
type Capture = Pick<Evidence, "bytes" | "sha256" | "complete" | "dropped">
export type DisabledChrCaptureOptions = {
journalPath: string
deadlineAt: number
writeAdmission?: ChrWriteAdmission
targetTimerProfile?: Omit<ChrTargetTimerProfile, "now">
stagedTarget?: true
scope: Evidence["scope"]
expected: Evidence["expected"]
serverIds: readonly [number, number]
routers: readonly [Router, Router]
assertTopology: () => Promise<void>
authorize: () => Promise<void>
capture: (exercise: (signal: AbortSignal, timer?: Omit<ChrStagedTargetTimerProfile, "now">) => Promise<void>) => Promise<Capture>
generate: (signal: AbortSignal) => Promise<{ lastDatagramAt: number }>
drainCollector: () => Promise<void>
reconcile: (wire: ReturnType<typeof projectChrWire>, serverIds: readonly [number, number]) => Promise<void>
reportRouterCleanup: (clean: boolean) => void
}
function must(value: unknown, reason: string): asserts value { if (!value) throw Error(reason) }
export function chrExportDrainMs(value: unknown) {
must(typeof value === "string", "Inactive flow timeout unavailable")
const compact = /^(?:(\d{1,2})m)?(?:(\d{1,2})s)?$/.exec(value)
const clock = /^(\d{2}):(\d{2}):(\d{2})$/.exec(value)
const seconds = compact ? Number(compact[1] ?? 0) * 60 + Number(compact[2] ?? 0) :
clock ? Number(clock[1]) * 3600 + Number(clock[2]) * 60 + Number(clock[3]) : 0
must(seconds >= 1 && seconds <= 60, "Inactive flow timeout outside owned capture budget")
return seconds * 1000 + 2000
}
/** Trusted local runner composition. The caller owns topology and collector.
* A post-dispatch ambiguity is never converted into a synthetic cleanup ACK. */
export async function executeDisabledChrCapture(options: DisabledChrCaptureOptions) {
const scope = structuredClone(options.scope), expected = structuredClone(options.expected)
const serverIds = structuredClone(options.serverIds), deadline = options.deadlineAt
const writeAdmission = options.writeAdmission
const targetTimerProfile = options.targetTimerProfile && { ...options.targetTimerProfile }
const staged = options.stagedTarget === true
const routers = options.routers.map(router => ({ ...router, baseline: structuredClone(router.baseline),
interfaces: structuredClone(router.interfaces) }))
const live = () => must(Number.isSafeInteger(deadline) && Date.now() < deadline, "Disabled capture authority expired")
const authorize = async () => {
live()
await options.authorize()
live()
}
const verifyTopology = async () => {
await authorize()
await options.assertTopology()
await authorize()
}
live()
must(deadline - Date.now() <= 600000 && routers.length === 2 && routers[0].baseline.deviceId !== routers[1].baseline.deviceId &&
serverIds.length === 2 && new Set(serverIds).size === 2 && serverIds.every(id => Number.isSafeInteger(id) && id > 0) &&
routers.every(router => router.interfaces.some(pin => pin.name === router.transitInterface && /e[ab]$/.test(pin.name))),
"Invalid disabled capture fixture")
must(!staged || (!targetTimerProfile && routers.every(router => "patch" in router.targetNative && typeof router.targetNative.patch === "function" &&
router.baseline.targets.length === 0 && ["15s", "00:00:15"].some(value => value === router.baseline.flow["inactive-flow-timeout"]))),
"Invalid staged target fixture")
const drainMs = Math.max(...routers.map(router => chrExportDrainMs(router.baseline.flow["inactive-flow-timeout"])))
const assertTargetTimersFit = () => {
if (!targetTimerProfile) return
must(targetTimerProfile.captureDeadlineAt <= deadline, "IPFIX capture timer exceeds the owner deadline")
for (const router of routers) assertChrTargetTimerWindow(targetTimerProfile, router.baseline.flow["inactive-flow-timeout"])
}
assertTargetTimersFit()
const journal = await open(options.journalPath, "wx", 0o600)
const append = async (record: object) => {
await journal.writeFile(JSON.stringify(record) + "\n")
await journal.sync()
}
const states = routers.map(() => ({ clean: true, inverse: undefined as (() => Promise<unknown>) | undefined }))
let work: Promise<void> | undefined, closed = false
let before: Counters[] | undefined, finalExport: { lastDatagramAt: number; completedAt: number } | undefined
let captureSealed = false
let captureSignal: AbortSignal | undefined
const routerAuthority = async () => {
must(captureSealed || (!closed && !captureSignal?.aborted), "Disabled capture cancelled")
await authorize()
must(captureSealed || (!closed && !captureSignal?.aborted), "Disabled capture cancelled")
}
const routerContext = async () => {
await routerAuthority()
await options.assertTopology()
await routerAuthority()
}
const stageRouters = async () => {
const targets: ReturnType<typeof createChrTargetHarness>[] = []
for (const [index, router] of routers.entries()) {
const state = states[index]
const checkpoint = (entry: object) => append({ device: index, ...entry })
const relayWrite = writeAdmission && { side: index === 0 ? "A" as const : "B" as const, admission: writeAdmission }
const flow = createDisabledChrFlowHarness({ ...router, authorize: routerAuthority, verifyContext: routerContext, checkpoint, relayWrite })
await routerAuthority()
state.clean = false
const active = await flow.enable()
state.inverse = async () => { await flow.restore(); state.clean = true }
const targetOptions = { baseline: active, read: router.read,
source: scope.sources[index], destination: scope.destination, port: scope.port, authorize: routerAuthority,
verifyContext: routerContext, checkpoint, relayWrite }
const native = router.targetNative
const target = staged ? (() => {
must("patch" in native && typeof native.patch === "function", "Staged target enable transport unavailable")
return createChrTargetHarness({ ...targetOptions, stagedDisabled: true, native })
})() : createChrTargetHarness({ ...targetOptions, native, timerProfile: targetTimerProfile })
await routerAuthority()
state.inverse = undefined
await target.create()
state.inverse = async () => { await target.remove(); await flow.restore(); state.clean = true }
targets.push(target)
}
return targets
}
try {
if (process.platform !== "win32") {
const dir = await open(dirname(options.journalPath), "r")
try { await dir.sync() } finally { await dir.close() }
}
await verifyTopology()
await append({ phase: "admitted", scope, serverIds })
const stagedTargets = staged ? await stageRouters() : undefined
if (stagedTargets) await append({ phase: "targets-staged-disabled" })
const capture = await options.capture((signal, captureTimer) => {
must(!work && !closed && !signal.aborted, "Disabled capture exercise already dispatched or cancelled")
work = (async () => {
captureSignal = signal
const timer = captureTimer && { ...captureTimer }
must(staged ? timer?.kind === "staged-ipfix-timeout-20s" && timer.captureDeadlineAt <= deadline : !timer,
"Staged capture acknowledgement unavailable")
if (timer) for (const router of routers) assertChrTargetTimerWindow(timer, router.baseline.flow["inactive-flow-timeout"])
if (stagedTargets) await routerAuthority()
else await routerContext()
if (!stagedTargets) before = await Promise.all(routers.map(router => router.counters()))
assertTargetTimersFit()
const targets = stagedTargets ?? await stageRouters()
if (timer) {
await append({ phase: "capture-ready", captureDeadlineAt: timer.captureDeadlineAt })
for (const target of targets) await target.enable(timer)
}
await routerAuthority()
if (stagedTargets) {
await routerContext()
for (const target of stagedTargets) await target.assertStagedTimerReady()
before = await Promise.all(routers.map(router => router.counters()))
} else if (targetTimerProfile) {
for (const target of targets) await target.assertTimerReady()
}
await routerAuthority()
const generatedAfter = Date.now()
const { lastDatagramAt } = await options.generate(signal)
must(Number.isSafeInteger(lastDatagramAt) && lastDatagramAt >= generatedAfter && lastDatagramAt <= Date.now() &&
lastDatagramAt + drainMs < Math.min(deadline, timer?.captureDeadlineAt ?? targetTimerProfile?.captureDeadlineAt ?? deadline),
"Final export drain cannot fit the owned window")
await append({ phase: "final-export-drain", lastDatagramAt, drainMs })
await delay(Math.max(0, lastDatagramAt + drainMs - Date.now()), undefined, { signal })
// The staged path takes its complete post-traffic snapshot after the
// host seals capture, with no native write between the two points.
if (stagedTargets) await routerAuthority()
else await routerContext()
const completedAt = Date.now()
must(completedAt >= lastDatagramAt + drainMs && (!timer || completedAt < timer.captureDeadlineAt), "Final export inactivity unconfirmed")
await append({ phase: "final-export-drain-complete", lastDatagramAt, drainMs, completedAt })
finalExport = { lastDatagramAt, completedAt }
})()
return work
})
must(work, "Capture omitted the disabled exercise")
await work
if (!staged) await verifyTopology()
await authorize()
const after = await Promise.all(routers.map(router => router.counters()))
if (staged) await verifyTopology()
must(before?.length === 2 && after.length === 2 && finalExport && states.every(state => state.inverse), "Disabled capture evidence incomplete")
const evidence = verifyChrOwnedWgEvidence({ ...capture, scope, expected, requireRefresh: true,
...(staged ? { counterWindow: "traffic" as const } : {}), exporters: [
{ interfaces: routers[0].interfaces, before: before[0], after: after[0] },
{ interfaces: routers[1].interfaces, before: before[1], after: after[1] },
] })
await append({ phase: "wire-bound", captureSha256: evidence.captureSha256, ...finalExport })
captureSealed = true
// The parent independently proves export inactivity and sealed capture
// before authorizing these exact one-shot inverses.
for (const state of [...states].reverse()) {
const inverse = state.inverse
state.inverse = undefined
must(inverse, "Verified router inverse unavailable")
await inverse()
}
must(states.every(state => state.clean), "Disabled capture router cleanup requires reconciliation")
await verifyTopology()
await options.drainCollector()
await verifyTopology()
await options.reconcile(projectChrWire(capture.bytes, scope), serverIds)
await append({ phase: "reconciled" })
return { ...evidence, componentSequenceComplete: true as const }
} finally {
closed = true
// Capture may report child failure before its exercise settles.
if (work) await work.catch(() => undefined)
try { options.reportRouterCleanup(states.every(state => state.clean)) }
finally { await journal.close() }
}
}
@@ -0,0 +1,153 @@
import { randomUUID } from "node:crypto"
import { networkInterfaces } from "node:os"
import { setTimeout as delay } from "node:timers/promises"
import type { McpPrincipal } from "../modules/mcp/store.js"
import { readChrLinuxNamespaceIdentity, readChrLinuxUdpOwner } from "./traffic-flow-chr-linux-udp-owner.js"
import { CHR_COLLECTOR_RESULT_POLL_MS } from "./traffic-flow-chr-target-timing-profile.js"
type Namespace = Awaited<ReturnType<typeof readChrLinuxNamespaceIdentity>>
type Options = {
runId: string
deadlineAt: number
namespace: Namespace
hostNamespace: Namespace
port: number
serverIds: readonly [number, number]
principal: McpPrincipal
database: { name: string; oid: string; role: string }
assertCurrent: () => Promise<void>
/** Waits for a separate operator's ordinary approval of this exact new plan. */
awaitHumanApproval: (planId: string, enabled: boolean, deadlineAt: number) => Promise<void>
}
const address = "198.18.242.3"
const uuid = (value: unknown): value is string =>
typeof value === "string" && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value)
function must(value: unknown, reason: string): asserts value { if (!value) throw Error(reason) }
/** Runs inside the disposable backend process. The reviewed parent owns its
* launch, admission, process exit and namespace removal. */
export async function createChrDisposableCollector(options: Options) {
const { runId, deadlineAt, port, assertCurrent, awaitHumanApproval } = options
const { namespace, hostNamespace, serverIds, database, principal } = structuredClone({
namespace: options.namespace, hostNamespace: options.hostNamespace, serverIds: options.serverIds,
database: options.database, principal: options.principal,
})
const instance = "netflow-" + runId
const interfaceName = "nf" + runId + "cv"
must(process.platform === "linux" && process.env.MMAPP_OWN_CHR_NETFLOW_COLLECTOR === "true" &&
/^[a-f0-9]{8}$/.test(runId) && Number.isSafeInteger(deadlineAt) &&
Date.now() < deadlineAt && deadlineAt - Date.now() <= 600000 &&
Number.isSafeInteger(port) && port >= 49152 && port <= 65535 &&
[namespace.device, namespace.inode, hostNamespace.device, hostNamespace.inode, database.oid].every(value => /^[1-9][0-9]{0,19}$/.test(value)) &&
(namespace.device !== hostNamespace.device || namespace.inode !== hostNamespace.inode) &&
database.name === "mcp_netflow_" + runId && database.role === "mcp_test" &&
serverIds.length === 2 && new Set(serverIds).size === 2 && serverIds.every(value => Number.isSafeInteger(value) && value > 0),
"Invalid disposable collector boundary")
const fixtureRoot = "/tmp/mcp-netflow-" + runId
const dbUrl = new URL(process.env.DATABASE_URL ?? "about:blank")
must(["postgres:", "postgresql:"].includes(dbUrl.protocol) && dbUrl.hostname === "localhost" &&
dbUrl.username === database.role && /^[a-f0-9]{64}$/.test(dbUrl.password) && !dbUrl.port && !dbUrl.hash &&
dbUrl.pathname === "/" + database.name && [...dbUrl.searchParams.keys()].join() === "host" &&
dbUrl.searchParams.get("host") === fixtureRoot + "/pg-socket" &&
process.env.MCP_STATE_DIR === fixtureRoot + "/state", "Disposable database or MCP state endpoint is unpinned")
const local = async () => {
must(Date.now() < deadlineAt, "Disposable collector deadline ended")
await assertCurrent()
const actual = await readChrLinuxNamespaceIdentity()
must(Date.now() < deadlineAt && actual.device === namespace.device && actual.inode === namespace.inode &&
(networkInterfaces()[interfaceName] ?? []).filter(row => row.family === "IPv4" && row.address === address).length === 1 &&
(!process.env.FLOW_LISTEN_HOST?.trim() || process.env.FLOW_LISTEN_HOST.trim() === address) &&
!process.env.FLOW_AGENT_SOCKET?.trim(), "Disposable namespace or listener source changed")
}
await local()
const [{ pool }, { invokeMcpTool, applyPlan, loadPlan }, { validatePrincipal }, ingest, { createFlowSettingsStore },
{ flowSettingsPrivateProof }] = await Promise.all([
import("../db/index.js"), import("../modules/mcp/runtime.js"), import("../modules/mcp/store.js"),
import("../services/traffic-flow-ingest.js"), import("../services/traffic-flow-settings-store.js"),
import("../services/traffic-flow-host-keys-service.js"),
])
const authorize = async () => {
await local()
await validatePrincipal(principal)
await local()
}
await authorize()
const db = (await pool.query("SELECT current_database() AS name,current_user AS role,oid::text AS oid," +
"inet_server_addr() IS NULL AS unix_socket FROM pg_database WHERE datname=current_database()")).rows
const servers = (await pool.query("SELECT id FROM servers ORDER BY id LIMIT 3")).rows.map(row => Number(row.id))
must(db.length === 1 && db[0].name === database.name && db[0].role === database.role && db[0].oid === database.oid &&
db[0].unix_socket === true && servers.length === 2 && serverIds.every(id => servers.includes(id)), "Disposable database identity differs")
const configStore = createFlowSettingsStore(pool, authorize, () => {})
const config = await configStore.read()
must(config.enabled === false && config.collectorIp === address && config.flowListenPort === port,
"Expected disabled exact collector configuration")
const proof = (value: typeof config) => flowSettingsPrivateProof({ ...value, enabled: false })
const invariantProof = await proof(config)
const observedUdp = () => readChrLinuxUdpOwner({ namespace, address, port })
const read = async () => {
await authorize()
const configuration = await configStore.read()
const udpOwners = await observedUdp()
const listener = ingest.getFlowListenerState(), worker = ingest.getFlowWorkerHealth()
const currentProof = await proof(configuration)
must(currentProof === invariantProof && configuration.collectorIp === address &&
configuration.flowListenPort === port && typeof configuration.enabled === "boolean", "Disposable collector settings changed")
await authorize()
return { instance, pid: process.pid, invariantProof: currentProof, listenAddress: configuration.collectorIp,
listenPort: configuration.flowListenPort, enabled: configuration.enabled, workerAlive: worker.alive,
boundAddress: listener.bound ? listener.address : null, udpOwners }
}
const initial = await read()
must(initial.udpOwners.length === 0 && !initial.workerAlive && initial.boundAddress === null, "Disposable collector not idle")
let enableAttempted = false, disableAttempted = false, enabledVerified = false, disableVerified = false
const applyEnabled = async (enabled: boolean, beforeDispatch: () => Promise<void>) => {
must(enabled ? !enableAttempted : enabledVerified && !disableAttempted, "Collector mutation already attempted or unknown")
if (enabled) enableAttempted = true
else disableAttempted = true
await authorize()
const current = await read()
must(current.enabled !== enabled, "Unexpected collector enable state")
const prepared = await invokeMcpTool(principal, "mm_netflow_settings_update_plan", { settings: { enabled } }) as {
state?: string; planId?: string
}
must(prepared.state === "awaiting-approval" && uuid(prepared.planId), "Collector plan did not await human approval")
await awaitHumanApproval(prepared.planId, enabled, deadlineAt)
await authorize()
const approved = await loadPlan(prepared.planId)
must(approved.connection_id === principal.connectionId && approved.operation_id === "netflow.settings.update" &&
approved.state === "approved" && approved.approved_by && approved.expires_at.getTime() > Date.now(), "Collector approval unconfirmed")
await beforeDispatch()
await authorize()
const operation = await applyPlan(principal, prepared.planId, randomUUID())
must(uuid(operation.id), "Collector operation acknowledgement missing")
const pollUntil = Math.min(deadlineAt, Date.now() + CHR_COLLECTOR_RESULT_POLL_MS)
while (Date.now() < pollUntil) {
await authorize()
const rows = (await pool.query("SELECT o.state,o.verification,s.state AS step_state,s.result " +
"FROM mcp_operations o LEFT JOIN mcp_operation_steps s ON s.operation_id=o.id " +
"WHERE o.id=$1 AND o.connection_id=$2 AND o.plan_id=$3 LIMIT 2",
[operation.id, principal.connectionId, prepared.planId])).rows
const [row] = rows
if (rows.length === 1 && ["pending", "running"].includes(row.state)) { await delay(100); continue }
must(rows.length === 1 && row.state === "applied" && row.verification === "passed" && row.step_state === "verified" &&
row.result?.configurationConfirmed === true && row.result.receiptConfirmed === true &&
row.result.listener === "request-acknowledged" && row.result.automaticReplay === false &&
uuid(row.result.receiptId), "Collector operation result requires reconciliation")
const receipt = (await pool.query("SELECT phase,state FROM flow_settings_intents " +
"WHERE receipt_id=$1 ORDER BY phase", [row.result.receiptId])).rows
must(receipt.length === 2 && receipt[0].phase === "configuration" && receipt[0].state === "completed" &&
receipt[1].phase === "listener" && receipt[1].state === "completed", "Collector durable listener acknowledgement missing")
await authorize()
if (enabled) enabledVerified = true
else disableVerified = true
return { operationId: operation.id, acknowledged: true }
}
throw Error("Collector operation outcome unknown. Automatic replay forbidden.")
}
const assertDrained = async () => {
const observed = await read()
must(disableVerified && !observed.enabled && !observed.workerAlive &&
observed.boundAddress === null && observed.udpOwners.length === 0, "Collector retirement unconfirmed")
}
return { interfaceName, pins: { instance, pid: process.pid, invariantProof }, collector: { authorize, read, applyEnabled, assertDrained } }
}
@@ -0,0 +1,88 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { verifyChrFirewallPreflight } from "./traffic-flow-chr-firewall-preflight.js"
type Row = Record<string, unknown>
const rule = { ".id": "*A", chain: "forward", action: "accept", disabled: "false", protocol: "udp",
"src-address": "198.18.240.0/24", "dst-address": "198.18.240.2/32",
"in-interface": "edge-a", "out-interface": "middle-a", "dst-port": "50003,50101-50103",
"connection-state": "new,established", log: "false", "log-prefix": "" }
test("owned WG policy admits complete empty snapshots only and requires matching path profile", async () => {
const { config, rules } = fixture()
rules.filter.length = 0
const profile = "owned-wg-empty-firewall" as const
const owned = { ...config, profile, readPathEvidence: async () => ({
...await config.readPathEvidence(), profile, ownedInterfaces: ["edge-a", "middle-a"],
ownedRoutes: [{ destination: "198.18.240.1", interfaceName: "edge-a" }, { destination: "198.18.240.2", interfaceName: "middle-a" }],
udpPairs: ["198.18.240.1:50100/198.18.240.2:50101"],
}) }
assert.equal((await verifyChrFirewallPreflight(owned)).policyVerified, true)
await assert.rejects(verifyChrFirewallPreflight({ ...owned, packets: [{ ...config.packets[0], destinationPort: 50102 }] }), /only owned WireGuard/)
await assert.rejects(verifyChrFirewallPreflight({ ...owned, readPathEvidence: config.readPathEvidence }), /reachability/)
rules.raw.push({ ".id": "*A", chain: "output", action: "accept", disabled: "true" })
await assert.rejects(verifyChrFirewallPreflight(owned), /requires empty firewall/)
})
function fixture() {
const calls: string[] = []
const rules = { raw: [] as Row[], mangle: [] as Row[], nat: [] as Row[], filter: [rule] as Row[] }
const config = {
identity: "router-a", version: "7.24.5 (stable)",
packets: [{ direction: "forward" as const, source: "198.18.240.1", destination: "198.18.240.2",
sourcePort: 50100, destinationPort: 50101, ingress: "edge-a", egress: "middle-a" }],
assertCurrent: async () => {},
readPathEvidence: async () => ({ routesVerified: true, freshConntrackTuples: true, fingerprint: "a".repeat(64) }),
client: { async get<T>(path: string, timeout?: number, _signal?: AbortSignal, bytes?: number, fence?: () => Promise<unknown>) {
assert.ok(timeout && timeout <= 2000)
assert.equal(bytes, 128 * 1024)
await fence?.()
calls.push(path)
if (path === "/system/identity") return [{ name: "router-a" }] as T
if (path === "/system/resource") return [{ version: "7.24.5 (stable)" }] as T
const table = path.replace("/ip/firewall/", "") as keyof typeof rules
return rules[table].map(row => ({ ...row, packets: String(calls.length), bytes: "100" })) as T
} },
}
return { config, rules, calls }
}
test("reads only fixed bounded native inventories twice and accepts exact simple UDP policy", async () => {
const { config, calls, rules } = fixture()
rules.filter.unshift({ ".id": "*B", chain: "forward", action: "drop", protocol: "tcp", "in-interface-list": "any" })
const paths = ["/system/identity", "/system/resource", "/ip/firewall/raw", "/ip/firewall/mangle",
"/ip/firewall/nat", "/ip/firewall/filter"]
assert.deepEqual(await verifyChrFirewallPreflight(config), { policyVerified: true, packetScopes: 1, actualIngressVerified: false })
assert.deepEqual(calls, [...paths, ...paths])
})
test("rejects matching FastTrack, NAT, jumps, unsupported matchers and incomplete filter admission", async () => {
for (const [table, inserted] of [
["filter", { chain: "forward", action: "fasttrack-connection", "connection-state": "established" }],
["nat", { chain: "srcnat", action: "masquerade" }],
["raw", { chain: "prerouting", action: "jump", "jump-target": "policy" }],
["filter", { chain: "forward", action: "accept", "src-address-list": "unknown" }],
] as const) {
const { config, rules } = fixture()
rules[table].unshift({ ".id": "*B", ...inserted })
await assert.rejects(verifyChrFirewallPreflight(config), /matching|unsupported/)
}
const { config, rules } = fixture()
rules.filter.length = 0
await assert.rejects(verifyChrFirewallPreflight(config), /no explicit filter allow/)
})
test("rejects inventory drift, unavailable route or conntrack proof, and lost authorization", async () => {
const stale = fixture()
stale.config.readPathEvidence = async () => {
if (stale.calls.length === 6) stale.rules.filter[0] = { ...rule, comment: "changed" }
return { routesVerified: true, freshConntrackTuples: true, fingerprint: "a".repeat(64) }
}
await assert.rejects(verifyChrFirewallPreflight(stale.config), /stale inventory/)
const unknown = fixture()
unknown.config.readPathEvidence = async () => ({ routesVerified: true, freshConntrackTuples: false, fingerprint: "a".repeat(64) })
await assert.rejects(verifyChrFirewallPreflight(unknown.config), /unknown active reachability/)
const expired = fixture()
expired.config.assertCurrent = async () => { throw Error("ownership expired") }
await assert.rejects(verifyChrFirewallPreflight(expired.config), /ownership expired/)
assert.equal(expired.calls.length, 0)
})
@@ -0,0 +1,179 @@
import { isIPv4 } from "node:net"
import { performance } from "node:perf_hooks"
import { isDeepStrictEqual } from "node:util"
import type { MikrotikClient } from "../services/mikrotik.js"
type Row = Record<string, unknown>
const tables = ["raw", "mangle", "nat", "filter"] as const
type Table = (typeof tables)[number]
type Packet = {
direction: "input" | "forward" | "output"
source: string
destination: string
sourcePort: number
destinationPort: number
ingress?: string
egress?: string
}
type Options = {
client: Pick<MikrotikClient, "get">
identity: string
version: string
packets: readonly Packet[]
assertCurrent: () => Promise<void>
/** Trusted reader of exact active routes and address/interface bindings. */
profile?: "owned-wg-empty-firewall"
readPathEvidence: () => Promise<{ routesVerified: boolean; freshConntrackTuples: boolean; fingerprint: string;
profile?: "owned-wg-empty-firewall"; ownedInterfaces?: readonly string[];
ownedRoutes?: readonly { destination: string; interfaceName: string }[]; udpPairs?: readonly string[] }>
}
const paths: Record<Table, Record<Packet["direction"], readonly string[]>> = {
raw: { input: ["prerouting"], forward: ["prerouting"], output: ["output"] },
mangle: { input: ["prerouting", "input"], forward: ["prerouting", "forward", "postrouting"], output: ["output", "postrouting"] },
nat: { input: ["dstnat", "input"], forward: ["dstnat", "srcnat"], output: ["output", "srcnat"] },
filter: { input: ["input"], forward: ["forward"], output: ["output"] },
}
const meta = new Set([".id", "action", "chain", "comment", "disabled", "dynamic", "invalid"])
const matchKeys = new Set(["protocol", "src-address", "dst-address", "src-port", "dst-port", "in-interface", "out-interface", "connection-state"])
const states = ["new", "established", "related", "invalid", "untracked"] as const
const deny = (reason: string): never => { throw Error("RouterOS fixture preflight refused: " + reason) }
const validPort = (value: number) => Number.isInteger(value) && value > 0 && value <= 65535
const flag = (value: unknown) => {
if (value === undefined || value === false || value === "false" || value === "no") return false
if (value === true || value === "true" || value === "yes") return true
return deny("unsupported rule flag")
}
const ipv4Number = (address: string) => address.split(".").reduce((sum, octet) => sum * 256 + Number(octet), 0)
function addressMatches(value: string, ip: string) {
const [base, bits = "32", ...extra] = value.split("/")
if (!isIPv4(base) || !/^(0|[1-9]|[12][0-9]|3[0-2])$/.test(bits) || extra.length) return undefined
const size = 2 ** (32 - Number(bits))
return Math.floor(ipv4Number(base) / size) === Math.floor(ipv4Number(ip) / size)
}
function portMatches(value: string, port: number) {
const ranges = value.split(",").map(range => /^([1-9][0-9]{0,4})(?:-([1-9][0-9]{0,4}))?$/.exec(range))
if (ranges.some(range => !range || !validPort(Number(range[1])) || !validPort(Number(range[2] ?? range[1])) ||
Number(range[1]) > Number(range[2] ?? range[1]))) return undefined
return ranges.some(range => range && Number(range[1]) <= port && port <= Number(range[2] ?? range[1]))
}
function matcher(key: string, value: unknown, packet: Packet, state: "new" | "established", chain: string) {
if (typeof value !== "string" || !value) return undefined
switch (key) {
case "protocol":
return /^(udp|tcp|icmp|gre|ipsec-esp|17|6|1|47|50)$/.test(value) ? value === "udp" || value === "17" : undefined
case "src-address": return addressMatches(value, packet.source)
case "dst-address": return addressMatches(value, packet.destination)
case "src-port": return portMatches(value, packet.sourcePort)
case "dst-port": return portMatches(value, packet.destinationPort)
case "in-interface":
return !["postrouting", "srcnat"].includes(chain) && packet.ingress && /^[\w.-]{1,64}$/.test(value)
? value === packet.ingress : undefined
case "out-interface":
return !["prerouting", "dstnat"].includes(chain) && packet.egress && /^[\w.-]{1,64}$/.test(value)
? value === packet.egress : undefined
case "connection-state": {
const values = value.split(",")
return values.every(item => (states as readonly string[]).includes(item)) ? values.includes(state) : undefined
}
default: return undefined
}
}
function matches(rule: Row, packet: Packet, state: "new" | "established", chain: string, table: Table) {
const results = Object.entries(rule).filter(([key]) => !meta.has(key))
.filter(([key, value]) => !(key === "log" && (value === false || value === "false" || value === "no") ||
key === "log-prefix" && value === ""))
.map(([key, value]) => matchKeys.has(key) && !(table === "raw" && key === "connection-state")
? matcher(key, value, packet, state, chain) : undefined)
if (results.includes(false)) return false
if (results.includes(undefined)) deny("unsupported potentially matching rule")
if (flag(rule.dynamic) || flag(rule.invalid)) deny("dynamic or invalid matching rule")
return true
}
function verifyRules(inventory: Record<Table, Row[]>, packet: Packet) {
for (const state of ["new", "established"] as const) {
for (const table of tables) {
for (const chain of paths[table][packet.direction]) {
let accepted = false
for (const rule of inventory[table]) {
if (rule.chain !== chain || flag(rule.disabled) || !matches(rule, packet, state, chain, table)) continue
if (rule.action !== "accept") deny("matching non-accept action")
accepted = true
break
}
// Require an explicit filter allow even though RouterOS may implicitly accept.
if (table === "filter" && !accepted) deny("no explicit filter allow")
}
}
}
}
function rows(value: unknown, limit: number): Row[] {
const result = Array.isArray(value) ? value : [value]
if (result.length > limit || result.some(row => !row || typeof row !== "object" || Array.isArray(row))) return deny("invalid inventory")
return result
}
/** Bounded double read via the existing RouterOS client. This proves a narrow
* policy subset at observation time. It does not prove packet delivery. */
export async function verifyChrFirewallPreflight(options: Options) {
const { client, assertCurrent, readPathEvidence, profile } = options
const { identity, version, packets } = structuredClone({ identity: options.identity, version: options.version, packets: options.packets })
if (!identity || !version || !packets.length || packets.length > 32 ||
packets.some(packet => !Object.hasOwn(paths.filter, packet.direction) ||
!isIPv4(packet.source) || !isIPv4(packet.destination) || !validPort(packet.sourcePort) || !validPort(packet.destinationPort) ||
(packet.direction !== "output" && !packet.ingress) || (packet.direction !== "input" && !packet.egress) ||
[packet.ingress, packet.egress].some(name => name !== undefined && !/^[\w.-]{1,64}$/.test(name)))) deny("invalid packet scope")
const deadline = performance.now() + 15000
async function fence() {
if (performance.now() >= deadline) deny("observation expired")
await assertCurrent()
if (performance.now() >= deadline) deny("observation expired")
}
async function get(path: string, limit: number) {
await fence()
const result = rows(await client.get<unknown>(path, Math.min(2000, Math.ceil(deadline - performance.now())),
undefined, 128 * 1024, fence), limit)
await fence()
return result
}
async function snapshot() {
const [device] = await get("/system/identity", 1)
const [resource] = await get("/system/resource", 1)
if (device?.name !== identity || resource?.version !== version) deny("device identity changed")
const inventory = {} as Record<Table, Row[]>
for (const table of tables) {
inventory[table] = (await get("/ip/firewall/" + table, 256)).map(({ bytes: _bytes, packets: _packets, ...rule }) => {
if (typeof rule.chain !== "string" || !rule.chain || typeof rule.action !== "string" ||
typeof rule[".id"] !== "string" || !/^\*[a-f0-9]{1,16}$/i.test(rule[".id"])) deny("invalid rule identity")
return structuredClone(rule)
})
if (new Set(inventory[table].map(rule => rule[".id"])).size !== inventory[table].length) deny("duplicate rule ID")
}
await fence()
const evidence = await readPathEvidence()
await fence()
if (evidence.routesVerified !== true || evidence.freshConntrackTuples !== true || evidence.profile !== profile ||
!/^[a-f0-9]{64}$/.test(evidence.fingerprint)) deny("unknown active reachability or existing connection")
if (profile && (evidence.ownedInterfaces?.length !== 2 ||
!packets.some(packet => packet.direction === "forward") ||
packets.some(packet => packet.direction === "forward" &&
(packet.ingress === packet.egress || !evidence.ownedInterfaces?.includes(packet.ingress!) ||
!evidence.ownedInterfaces?.includes(packet.egress!) ||
!evidence.ownedRoutes?.some(route => route.destination === packet.destination && route.interfaceName === packet.egress) ||
!evidence.ownedRoutes?.some(route => route.destination === packet.source && route.interfaceName === packet.ingress) ||
!evidence.udpPairs?.includes([packet.source + ":" + packet.sourcePort, packet.destination + ":" + packet.destinationPort].sort().join("/")))))) {
deny("forwarding does not use only owned WireGuard")
}
return { inventory, fingerprint: evidence.fingerprint, ownedInterfaces: evidence.ownedInterfaces,
ownedRoutes: evidence.ownedRoutes, udpPairs: evidence.udpPairs }
}
const before = await snapshot()
const after = await snapshot()
if (!isDeepStrictEqual(before, after)) deny("stale inventory or path")
if (profile === "owned-wg-empty-firewall") {
if (tables.some(table => after.inventory[table].length !== 0)) deny("owned WireGuard profile requires empty firewall")
} else {
for (const packet of packets) verifyRules(after.inventory, packet)
}
await fence()
return { policyVerified: true as const, packetScopes: packets.length, actualIngressVerified: false as const }
}
@@ -0,0 +1,158 @@
import { createHash } from "node:crypto"
import { constants } from "node:fs"
import { lstat, open } from "node:fs/promises"
import { createConnection } from "node:net"
import { CHR_STAGED_TARGET_CAPTURE_MAX_MS } from "./traffic-flow-chr-target-timing-profile.js"
import type { ChrStagedTargetTimerProfile } from "./traffic-flow-chr-target-harness.js"
type Operation = "topology-ready" | "generate" | "capture-start" | "capture-stop" | "cancel"
type Phase = "calibration" | "initial" | "refresh"
type Identity = { device: string; inode: string }
const limit = 4096
const captureLimit = 512 * 1024
function must(value: unknown, reason: string): asserts value { if (!value) throw Error(reason) }
export function decodeChrHostReply(raw: Buffer, sequence: number, op: Operation, stagedStart = false) {
must(raw.length > 0 && raw.length <= limit && raw.at(-1) === 10 && raw.indexOf(10) === raw.length - 1,
"Invalid host IPC frame")
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw))
must(value && value.sequence === sequence && value.op === op && value.ok === true, "Host IPC acknowledgement unavailable")
const extra = op === "generate" ? ["lastDatagramAt"] : op === "capture-stop" ? ["sha256", "complete", "dropped"] :
op === "capture-start" && stagedStart ? ["captureDeadlineAt"] : []
must(Object.keys(value).sort().join() === ["sequence", "op", "ok", ...extra].sort().join(), "Invalid host IPC reply shape")
if (op === "generate") must(Number.isSafeInteger(value.lastDatagramAt) && value.lastDatagramAt > 0, "Host UDP time unconfirmed")
if (op === "capture-stop") must(value.complete === true && value.dropped === 0 && typeof value.sha256 === "string" &&
/^[a-f0-9]{64}$/.test(value.sha256), "Host capture unconfirmed")
if (op === "capture-start" && stagedStart) must(Number.isSafeInteger(value.captureDeadlineAt) && value.captureDeadlineAt > 0,
"Host capture deadline unconfirmed")
return value as { lastDatagramAt?: number; sha256?: string; captureDeadlineAt?: number }
}
export function bindChrStagedHostCaptureTimer(captureDeadlineAt: number | undefined, captureMs: number,
ownerDeadline: number, requestedAt: number, acknowledgedAt: number): Omit<ChrStagedTargetTimerProfile, "now"> {
must(Number.isSafeInteger(captureMs) && captureMs >= 90000 && captureMs <= CHR_STAGED_TARGET_CAPTURE_MAX_MS &&
[ownerDeadline, requestedAt, acknowledgedAt].every(value => Number.isSafeInteger(value) && value > 0) &&
captureDeadlineAt !== undefined && Number.isSafeInteger(captureDeadlineAt) && requestedAt <= acknowledgedAt &&
captureDeadlineAt > acknowledgedAt && captureDeadlineAt <= ownerDeadline &&
captureDeadlineAt >= requestedAt + captureMs && captureDeadlineAt <= acknowledgedAt + captureMs,
"Host capture deadline outside reviewed window")
return { kind: "staged-ipfix-timeout-20s", captureDeadlineAt }
}
export async function createChrHostControl(options: {
runId: string; nonce: string; deadlineAt: number; socket: Identity; assertCurrent: () => Promise<void>
}) {
const { runId, nonce, deadlineAt, assertCurrent } = options
const pin = structuredClone(options.socket)
const gid = process.getegid?.()
must(process.platform === "linux" && gid && /^[a-f0-9]{8}$/.test(runId) &&
/^[a-f0-9]{32}$/.test(nonce) && nonce.startsWith(runId), "Host IPC ownership unavailable")
const directory = "/tmp/mcp-netflow-" + runId + "/ipc"
const socketPath = directory + "/control.sock"
const fence = async () => {
await assertCurrent()
const [dir, socket] = await Promise.all([lstat(directory, { bigint: true }), lstat(socketPath, { bigint: true })])
must(Date.now() < deadlineAt && dir.isDirectory() && dir.uid === 0n && dir.gid === BigInt(gid) &&
(dir.mode & 0o777n) === 0o710n && socket.isSocket() && socket.uid === 0n && socket.gid === BigInt(gid) &&
(socket.mode & 0o777n) === 0o660n && String(socket.dev) === pin.device && String(socket.ino) === pin.inode,
"Host IPC ownership changed")
}
await fence()
let sequence = 0, busy = false, failed = false, phase = "new"
const reached = (expected: string) => phase === expected
async function exchange(op: Operation, signal?: AbortSignal, generation?: Phase, stagedStart = false) {
must(!failed && !busy && !signal?.aborted && sequence < 8, "Host IPC request refused")
busy = true
try {
await fence()
const current = ++sequence
const started = Date.now()
const raw = await new Promise<Buffer>((resolve, reject) => {
const sock = createConnection(socketPath)
let reply = Buffer.alloc(0)
const fail = () => { sock.destroy(); reject(Error("Host IPC outcome unknown")) }
const timer = setTimeout(fail, Math.max(1, Math.min(15000, deadlineAt - Date.now())))
const cleanup = () => { clearTimeout(timer); signal?.removeEventListener("abort", fail) }
signal?.addEventListener("abort", fail, { once: true })
if (signal?.aborted) fail()
sock.once("connect", () => sock.write(JSON.stringify({ runId, nonce, sequence: current, op,
...(generation && { phase: generation }) }) + "\n"))
sock.on("data", chunk => {
if (reply.length + chunk.length > limit) return fail()
reply = Buffer.concat([reply, chunk])
if (reply.includes(10)) { cleanup(); resolve(reply); sock.destroy() }
})
sock.once("error", fail)
sock.once("close", () => { cleanup(); reject(Error("Host IPC closed before acknowledgement")) })
})
const value = decodeChrHostReply(raw, current, op, stagedStart)
await fence()
if (op === "generate") must(value.lastDatagramAt! >= started && value.lastDatagramAt! <= Date.now(),
"Host UDP receipt is outside request window")
return value
} catch (error) { failed = true; throw error }
finally { busy = false }
}
async function capture(deadline: number, exercise: (signal: AbortSignal, timer?: Omit<ChrStagedTargetTimerProfile, "now">) => Promise<void>,
stagedMs?: number) {
const requestedAt = Date.now()
must(phase === "calibration" && requestedAt < deadline && deadline <= deadlineAt, "Host capture phase differs")
const startSignal = AbortSignal.timeout(deadline - requestedAt)
const started = await exchange("capture-start", startSignal, undefined, stagedMs !== undefined)
phase = "capturing"
try {
const acknowledgedAt = Date.now()
const timer = stagedMs === undefined ? undefined :
bindChrStagedHostCaptureTimer(started.captureDeadlineAt, stagedMs, deadline, requestedAt, acknowledgedAt)
const signal = timer ? AbortSignal.timeout(timer.captureDeadlineAt - acknowledgedAt) : startSignal
if (timer) await exercise(signal, timer)
else await exercise(signal)
must(reached("refresh") && !signal.aborted, "Host capture exercise incomplete")
const { sha256 } = await exchange("capture-stop", signal)
phase = "finished"
const file = await open(directory + "/capture.pcap", constants.O_RDONLY | constants.O_NOFOLLOW)
try {
const row = await file.stat()
must(row.isFile() && row.uid === 0 && row.gid === gid && (row.mode & 0o777) === 0o640 &&
row.nlink === 1 && row.size > 0 && row.size <= captureLimit, "Host capture ownership unavailable")
const bytes = Buffer.alloc(row.size + 1)
let offset = 0
while (offset < bytes.length) {
const result = await file.read(bytes, offset, bytes.length - offset, null)
if (!result.bytesRead) break
offset += result.bytesRead
}
const capture = bytes.subarray(0, offset)
must(offset === row.size && createHash("sha256").update(capture).digest("hex") === sha256, "Host capture bytes differ")
return { bytes: capture, sha256: sha256!, complete: true, dropped: 0 }
} finally { await file.close() }
} finally {
if (!failed && phase !== "finished") {
phase = "cancelled"
// Parent cancellation intentionally refuses the session. The
// original capture failure remains the terminal cause.
await exchange("cancel").catch(() => {})
}
}
}
return {
async topologyReady() {
must(phase === "new", "Host topology acknowledgement already attempted")
await exchange("topology-ready")
phase = "topology"
},
async generate(generation: Phase, signal?: AbortSignal) {
const expected = generation === "calibration" ? "topology" : generation === "initial" ? "capturing" : "initial"
must(phase === expected, "Host UDP phase order differs")
const value = await exchange("generate", signal, generation)
phase = generation
return { lastDatagramAt: value.lastDatagramAt! }
},
capture: (deadline: number, exercise: (signal: AbortSignal) => Promise<void>) => capture(deadline, exercise),
captureStaged: (captureMs: number, exercise: Parameters<typeof capture>[1]) => {
must(Number.isSafeInteger(captureMs) && captureMs >= 90000 && captureMs <= CHR_STAGED_TARGET_CAPTURE_MAX_MS &&
Date.now() + captureMs < deadlineAt, "Staged host capture window unavailable")
return capture(deadlineAt, exercise, captureMs)
},
}
}
@@ -0,0 +1,66 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { decodeOwnedIpfix } from "./traffic-flow-chr-independent-ipfix.js"
const fields = [[1, 8], [2, 8], [4, 1], [7, 2], [8, 4], [10, 4], [11, 2], [12, 4], [14, 4]]
function set(id: number, body: Buffer) {
const header = Buffer.alloc(4)
header.writeUInt16BE(id); header.writeUInt16BE(body.length + 4, 2)
return Buffer.concat([header, body])
}
function packet(sets: Buffer[]) {
const header = Buffer.alloc(16)
header.writeUInt16BE(10)
header.writeUInt16BE(16 + sets.reduce((total, value) => total + value.length, 0), 2)
header.writeUInt32BE(7, 12)
return Buffer.concat([header, ...sets]).toString("hex")
}
function fixture() {
const template = Buffer.alloc(4 + fields.length * 4)
template.writeUInt16BE(256); template.writeUInt16BE(fields.length, 2)
fields.forEach(([field, size], index) => { template.writeUInt16BE(field, 4 + index * 4); template.writeUInt16BE(size, 6 + index * 4) })
const data = Buffer.alloc(37)
data.writeBigUInt64BE(9007199254740993n); data.writeBigUInt64BE(3n, 8); data[16] = 17
data.writeUInt16BE(9000, 17); Buffer.from([192, 0, 2, 1]).copy(data, 19); data.writeUInt32BE(11, 23)
data.writeUInt16BE(9001, 27); Buffer.from([198, 51, 100, 1]).copy(data, 29); data.writeUInt32BE(12, 33)
const frames = [0, 1].map((exporter, index) => ({
ordinal: index + 1, receivedMs: 1700000000000, receivedNs: "1700000000000000000", exporter, sourcePort: 2055,
version: 10, domain: 7, payloadHex: packet([set(2, template), set(256, data)]),
}))
return { template, data, input: { captureSha256: "a".repeat(64), scope: { sources: ["192.0.2.10", "192.0.2.20"], destination: "192.0.2.100", port: 4739 }, frames } }
}
test("independent IPv4 IPFIX decoder preserves exact counters beyond JavaScript safe integer range", () => {
const { input } = fixture()
const result = decodeOwnedIpfix(input)
assert.equal(result.captureSha256, input.captureSha256)
assert.equal(result.nativeAcceptance, false)
for (const row of result.frames) {
assert.deepEqual(row.setIds, [2, 256])
assert.deepEqual(row.flows, [{ src: "192.0.2.1", dst: "198.51.100.1", proto: 17, srcPort: 9000, dstPort: 9001,
inIface: "11", outIface: "12", bytes: "9007199254740993", packets: "3" }])
}
})
test("refuses template borrowing across exporters and UDP source ports", () => {
const { input, data } = fixture()
input.frames[1].payloadHex = packet([set(256, data)])
assert.throws(() => decodeOwnedIpfix(input), /Missing IPFIX template/)
input.frames[1].exporter = 0
input.frames[1].sourcePort = 2056
assert.throws(() => decodeOwnedIpfix(input), /Missing IPFIX template/)
})
test("refuses duplicate required fields, ambiguous padding and unsupported variable fields", () => {
const duplicate = fixture()
duplicate.template.writeUInt16BE(1, 8)
duplicate.input.frames[0].payloadHex = packet([set(2, duplicate.template), set(256, duplicate.data)])
assert.throws(() => decodeOwnedIpfix(duplicate.input), /Ambiguous/)
const variable = fixture()
variable.template.writeUInt16BE(65535, 6)
variable.input.frames[0].payloadHex = packet([set(2, variable.template), set(256, variable.data)])
assert.throws(() => decodeOwnedIpfix(variable.input), /Variable/)
const padded = fixture()
padded.input.frames[0].payloadHex = packet([set(2, padded.template), set(256, Buffer.concat([padded.data, Buffer.from([1])]))])
assert.throws(() => decodeOwnedIpfix(padded.input), /padding/)
})
@@ -0,0 +1,135 @@
/** Narrow independent decoder for the reviewed IPv4 RouterOS IPFIX fixture.
* Deliberately imports no product flow parser or SQL aggregation. */
import { createHash } from "node:crypto"
import { open } from "node:fs/promises"
import { isIPv4 } from "node:net"
import { resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { z } from "zod"
const sha = z.string().regex(/^[a-f0-9]{64}$/)
const ip = z.string().max(15).refine(isIPv4)
const port = z.number().int().min(1).max(65535)
const projection = z.object({
captureSha256: sha,
scope: z.object({ sources: z.tuple([ip, ip]), destination: ip, port, linkType: z.enum(["RAW", "EN10MB"]).optional() }).strict(),
frames: z.array(z.object({
ordinal: z.number().int().positive().safe(), receivedMs: z.number().int().nonnegative().safe(),
receivedNs: z.string().regex(/^\d{1,20}$/), exporter: z.union([z.literal(0), z.literal(1)]),
sourcePort: port, version: z.literal(10), domain: z.number().int().min(0).max(0xffffffff),
payloadHex: z.string().min(40).max(8192).regex(/^(?:[0-9a-f]{2})+$/),
}).strict()).min(2).max(64),
}).strict()
const required = new Map([[1, [1, 2, 4, 8]], [2, [1, 2, 4, 8]], [4, [1]], [7, [2]], [8, [4]], [10, [1, 2, 4]], [11, [2]], [12, [4]], [14, [1, 2, 4]]])
type Field = { offset: number; size: number }
type Template = { fields: Map<number, Field>; width: number }
function must(value: unknown, error: string): asserts value {
if (!value) throw Error(error)
}
function padding(bytes: Buffer) {
return bytes.length <= 3 && bytes.every(value => value === 0)
}
export function decodeOwnedIpfix(raw: unknown) {
const input = projection.parse(raw)
must(new Set([...input.scope.sources, input.scope.destination]).size === 3, "Invalid exporter scope")
const templates = new Map<string, Template>()
let last = 0, records = 0, lastTimestamp = -1n
const positive = new Set<number>()
const frames = input.frames.map(frame => {
const timestamp = BigInt(frame.receivedNs)
must(frame.ordinal === last + 1 && timestamp >= lastTimestamp && timestamp / 1000000n === BigInt(frame.receivedMs), "Invalid wire ordinal or timestamp")
last = frame.ordinal
lastTimestamp = timestamp
const data = Buffer.from(frame.payloadHex, "hex")
must(data.readUInt16BE(0) === 10 && data.readUInt16BE(2) === data.length && data.readUInt32BE(12) === frame.domain, "IPFIX header disagrees with wire metadata")
const session = [frame.exporter, frame.sourcePort, frame.domain].join(":")
const flows = []
const setIds: number[] = []
const templateIds: number[] = []
for (let offset = 16; offset < data.length;) {
must(data.length - offset >= 4 && setIds.length < 32, "Invalid IPFIX set count or header")
const id = data.readUInt16BE(offset), length = data.readUInt16BE(offset + 2)
must(length >= 4 && offset + length <= data.length, "Invalid IPFIX set length")
const body = data.subarray(offset + 4, offset + length)
offset += length
setIds.push(id)
if (id === 2) {
for (let at = 0; at < body.length && !padding(body.subarray(at));) {
must(body.length - at >= 4, "Truncated IPFIX template")
const templateId = body.readUInt16BE(at), fieldCount = body.readUInt16BE(at + 2)
must(templateId >= 256 && fieldCount >= required.size && fieldCount <= 64, "Unsupported template or withdrawal")
at += 4
const fields = new Map<number, Field>()
let width = 0
for (let index = 0; index < fieldCount; index++) {
must(body.length - at >= 4, "Truncated IPFIX field")
const field = body.readUInt16BE(at), size = body.readUInt16BE(at + 2)
at += 4
must(size > 0 && width + size <= 4096, "Variable or oversized IPFIX field")
if (field & 0x8000) {
must(body.length - at >= 4, "Truncated enterprise field")
at += 4
} else if (required.has(field)) {
must(!fields.has(field) && required.get(field)!.includes(size), "Ambiguous or unsupported required field")
fields.set(field, { offset: width, size })
}
width += size
}
must(fields.size === required.size, "Required IPv4 flow fields are missing")
templates.set(session + ":" + templateId, { fields, width })
must(templates.size <= 32, "Template budget exceeded")
templateIds.push(templateId)
}
continue
}
must(id >= 256, "Unsupported IPFIX control set")
const template = templates.get(session + ":" + id)
must(template && body.length >= template.width, "Missing IPFIX template or data")
let at = 0
for (; at + template.width <= body.length; at += template.width) {
must(++records <= 200 && flows.length < 64, "Independent record budget exceeded")
const part = (field: number) => {
const item = template.fields.get(field)!
return body.subarray(at + item.offset, at + item.offset + item.size)
}
const unsigned = (field: number) => part(field).reduce((value, byte) => value * 256n + BigInt(byte), 0n)
const bytes = unsigned(1), packets = unsigned(2)
must(bytes > 0n && packets > 0n && bytes <= 9223372036854775807n && packets <= 9223372036854775807n, "Counter outside positive SQL bigint range")
flows.push({ src: [...part(8)].join("."), dst: [...part(12)].join("."), proto: Number(unsigned(4)),
srcPort: Number(unsigned(7)), dstPort: Number(unsigned(11)), inIface: String(unsigned(10)), outIface: String(unsigned(14)),
bytes: String(bytes), packets: String(packets) })
}
must(padding(body.subarray(at)), "Ambiguous IPFIX data padding")
}
if (flows.length) positive.add(frame.exporter)
return { ordinal: frame.ordinal, flows, setIds, templateIds }
})
must(positive.size === 2, "Both exporters need independently decoded positive traffic")
return { captureSha256: input.captureSha256, frames, source: "independent-fixed-ipv4-ipfix-v1", nativeAcceptance: false }
}
async function main() {
const [path, expectedSha, ...extra] = process.argv.slice(2)
must(path && !extra.length && sha.safeParse(expectedSha).success, "Expected pinned wire projection")
const handle = await open(path, "r")
try {
const info = await handle.stat()
must(info.isFile() && info.size > 0 && info.size <= 512 * 1024, "Wire projection outside file bound")
const buffer = Buffer.alloc(info.size + 1)
let size = 0
while (size < buffer.length) {
const { bytesRead } = await handle.read(buffer, size, buffer.length - size, size)
if (!bytesRead) break
size += bytesRead
}
const body = buffer.subarray(0, size)
must(size === info.size && createHash("sha256").update(body).digest("hex") === expectedSha, "Wire projection changed or digest differs")
console.log(JSON.stringify({ wireProjectionSha256: expectedSha, ...decodeOwnedIpfix(JSON.parse(body.toString("utf8"))) }))
} finally {
await handle.close()
}
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch(() => { console.error("Independent IPFIX projection refused"); process.exitCode = 1 })
}
@@ -0,0 +1,79 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { discoverChrIngressEndpoint } from "./traffic-flow-chr-ingress-discovery.js"
type Options = Parameters<typeof discoverChrIngressEndpoint>[0]
function fixture() {
const reads: string[] = [], toolReads: string[] = []
const collectors = { enabled: true, collectorIp: "192.0.2.100", flowListenPort: 4740,
listenerBound: true, listenerAddress: "192.0.2.100:4740" }
let revoked = false
const mappings = new Map([["192.0.2.10", 1], ["192.0.2.20", 2]])
const native = (serverId: number) => {
const identity = "owned-" + serverId, version = "7.24.5 (stable)", source = "192.0.2." + serverId * 10
const table: Record<string, unknown> = {
"/system/identity": [{ name: identity }],
"/system/resource": { version },
"/ip/address": [{ address: source + "/32", disabled: "false" }],
"/ip/traffic-flow": [{ enabled: "true" }],
"/ip/traffic-flow/target": [{ ".id": "*A", disabled: "false", "dst-address": "192.0.2.100",
"src-address": serverId === 1 ? source : "0.0.0.0", port: "4740", version: "ipfix" }],
}
const device: Options["devices"][0] = {
identity, version, source, serverId,
assertPinnedTransport: async () => { if (revoked) throw Error("transport pin lost") },
client: { async get<T>(path: string, timeout?: number, _signal?: AbortSignal, max?: number, before?: () => Promise<unknown>) {
assert(timeout! > 0 && timeout! <= 5000)
assert.equal(max, 65536)
assert(before)
await before()
reads.push(serverId + ":" + path)
assert(Object.hasOwn(table, path))
return structuredClone(table[path]) as T
} },
}
return { table, device }
}
const a = native(1), b = native(2)
const options: Options = { destination: collectors.collectorIp, baselinePort: 4740, testPort: 4739,
deadlineAt: Date.now() + 5000, devices: [a.device, b.device], assertCurrent: async () => {},
callTool: async (name, input) => { assert.deepEqual(input, {}); toolReads.push(name); return collectors },
resolveExporter: async address => mappings.get(address) ?? null,
}
return { options, reads, toolReads, a, b, mappings, collectors, revoke: () => { revoked = true } }
}
test("discovers exact configured two-CHR baseline endpoint without claiming observed ingress", async () => {
const f = fixture()
const result = await discoverChrIngressEndpoint(f.options)
assert.deepEqual(result.endpoint, { sources: ["192.0.2.10", "192.0.2.20"], destination: "192.0.2.100", port: 4740 })
assert.deepEqual(result.observations.map(row => row.sourceConfiguredExplicitly), [true, false])
assert.equal(result.ingressVerified, false)
assert.equal(f.reads.length, 10)
assert.deepEqual(f.toolReads, ["mm_netflow_settings_get", "mm_netflow_settings_get"])
})
test("refuses absent exact target, competing test port, source drift and attribution mismatch", async () => {
for (const [mutate, reason] of [
[(f: ReturnType<typeof fixture>) => { f.b.table["/ip/traffic-flow/target"] = [] }, /baseline IPFIX target/],
[(f: ReturnType<typeof fixture>) => { (f.a.table["/ip/traffic-flow/target"] as object[]).push({ ".id": "*B", "dst-address": "192.0.2.100", port: "4739" }) }, /test endpoint/],
[(f: ReturnType<typeof fixture>) => { f.b.table["/ip/address"] = [{ address: "192.0.2.99/32", disabled: false }] }, /source is not assigned/],
[(f: ReturnType<typeof fixture>) => { f.mappings.set("192.0.2.20", 1) }, /attribution differs/],
] as const) {
const f = fixture()
mutate(f)
await assert.rejects(discoverChrIngressEndpoint(f.options), reason)
}
})
test("stops on wrong collector bind, lost transport pin and changed CHR identity", async () => {
const wrong = fixture(); wrong.collectors.listenerAddress = "0.0.0.0:4740"
await assert.rejects(discoverChrIngressEndpoint(wrong.options), /collector endpoint/)
assert.deepEqual(wrong.reads, [])
const revoked = fixture(); revoked.revoke()
await assert.rejects(discoverChrIngressEndpoint(revoked.options), /transport pin lost/)
assert.deepEqual(revoked.reads, [])
const changed = fixture(); changed.a.table["/system/identity"] = [{ name: "foreign" }]
await assert.rejects(discoverChrIngressEndpoint(changed.options), /identity or version/)
assert.deepEqual(changed.reads, ["1:/system/identity", "1:/system/resource"])
})
@@ -0,0 +1,123 @@
/** Exact read-only prerequisite for a passive two-CHR ingress capture. */
import { isIPv4 } from "node:net"
import { setTimeout as delay } from "node:timers/promises"
import type { MikrotikClient } from "../services/mikrotik.js"
import { captureOwnedChrPackets } from "./traffic-flow-chr-capture-process.js"
type Row = Record<string, unknown>
type Device = {
client: Pick<MikrotikClient, "get">
identity: string; version: string; source: string; serverId: number
assertPinnedTransport: () => Promise<void>
}
type Options = {
destination: string; baselinePort: number; testPort: number
devices: readonly [Device, Device]
callTool: (name: "mm_netflow_settings_get", input: Record<string, never>) => Promise<unknown>
resolveExporter: (address: string) => Promise<number | null>
assertCurrent: () => Promise<void>
deadlineAt: number
signal?: AbortSignal
}
const targetPath = "/ip/traffic-flow/target"
const trueValue = (value: unknown) => value === true || value === "true" || value === "yes"
const falseValue = (value: unknown) => value === false || value === "false" || value === "no"
function must(value: unknown, reason: string): asserts value {
if (!value) throw Error(reason)
}
function rows(value: unknown, limit: number): Row[] {
const result = Array.isArray(value) ? value : [value]
must(result.length <= limit && result.every(row => row && typeof row === "object" && !Array.isArray(row)), "Invalid bounded native observation")
return result as Row[]
}
function singleton(value: unknown) {
const result = rows(value, 1)
must(result.length === 1, "Missing native singleton observation")
return result[0]
}
export async function discoverChrIngressEndpoint(options: Options) {
const { destination, baselinePort, testPort, deadlineAt, signal } = options
const devices = options.devices.map(device => ({ ...device }))
const port = (value: number) => Number.isInteger(value) && value >= 1 && value <= 65535
must(devices.length === 2 && new Set([...devices.map(device => device.source), destination]).size === 3 &&
[...devices.map(device => device.source), destination].every(isIPv4) && new Set(devices.map(device => device.serverId)).size === 2 &&
new Set(devices.map(device => device.identity)).size === 2 &&
devices.every(device => Number.isSafeInteger(device.serverId) && device.serverId > 0 && device.identity.length > 0 &&
device.identity.length <= 128 && device.version.length > 0 && device.version.length <= 128) &&
port(baselinePort) && port(testPort) && baselinePort !== testPort &&
Number.isSafeInteger(deadlineAt) && deadlineAt > Date.now() && deadlineAt - Date.now() <= 90000, "Invalid pinned ingress scope")
const live = () => {
signal?.throwIfAborted()
must(Date.now() < deadlineAt, "Ingress discovery deadline ended")
}
const check = async () => { live(); await options.assertCurrent(); live() }
const collector = async () => {
await check()
const value = singleton(await options.callTool("mm_netflow_settings_get", {}))
await check()
must(value.enabled === true && value.collectorIp === destination && value.flowListenPort === baselinePort &&
value.listenerBound === true && value.listenerAddress === destination + ":" + baselinePort, "Current baseline collector endpoint is not confirmed")
}
await collector()
const observations = []
for (const device of devices) {
const fence = async () => { await check(); await device.assertPinnedTransport(); await check() }
const get = async (path: string) => {
await fence()
const value = await device.client.get<unknown>(path, Math.min(5000, deadlineAt - Date.now()), signal, 64 * 1024, fence)
await fence()
return value
}
const identity = singleton(await get("/system/identity")), resource = singleton(await get("/system/resource"))
must(identity.name === device.identity && resource.version === device.version, "Pinned CHR identity or version differs")
const addresses = rows(await get("/ip/address"), 256)
must(addresses.filter(row => typeof row.address === "string" && row.address.split("/")[0] === device.source && falseValue(row.disabled)).length === 1,
"Pinned exporter source is not assigned exactly once")
await fence()
must(await options.resolveExporter(device.source) === device.serverId, "Collector exporter attribution differs")
await fence()
const flow = singleton(await get("/ip/traffic-flow"))
must(trueValue(flow.enabled), "Traffic Flow is disabled or unconfirmed")
const targets = rows(await get(targetPath), 64)
must(targets.every(row => typeof row[".id"] === "string" && /^\*[a-f0-9]{1,16}$/i.test(row[".id"])) &&
new Set(targets.map(row => row[".id"])).size === targets.length, "Target identity is malformed or ambiguous")
const matches = targets.filter(row => row["dst-address"] === destination && String(row.port) === String(baselinePort))
must(matches.length === 1 && falseValue(matches[0].disabled) && matches[0].version === "ipfix",
"Exact enabled baseline IPFIX target is missing or ambiguous")
must(!targets.some(row => row["dst-address"] === destination && String(row.port) === String(testPort)),
"Proposed test endpoint is already configured")
const source = matches[0]["src-address"]
must(source === device.source || source === "0.0.0.0", "Baseline target source conflicts with the pinned address")
observations.push({ serverId: device.serverId, identity: device.identity, targetId: matches[0][".id"],
sourceAddress: device.source, sourceConfiguredExplicitly: source === device.source })
}
await collector()
return {
endpoint: { sources: [devices[0].source, devices[1].source] as const, destination, port: baselinePort },
testPort, observations, passiveCaptureCandidate: true,
ingressVerified: false, actualNativeAcceptance: false,
}
}
/** Passive capture entry for the pinned Linux operator. The exercise only waits. */
export async function observeChrBaselineIngress(options: Options & { interfaceName: string; sampleMs: number }) {
must(process.platform === "linux" && Number.isInteger(options.sampleMs) && options.sampleMs >= 1000 && options.sampleMs <= 30000,
"Invalid passive capture platform or window")
const discovered = await discoverChrIngressEndpoint(options)
must(Date.now() + options.sampleMs < options.deadlineAt, "Passive capture window exceeds the reviewed deadline")
const capture = await captureOwnedChrPackets({ ...discovered.endpoint, interfaceName: options.interfaceName }, {
deadlineAt: options.deadlineAt,
assertCurrent: async () => {
options.signal?.throwIfAborted()
await options.assertCurrent()
for (const device of options.devices) await device.assertPinnedTransport()
options.signal?.throwIfAborted()
},
exercise: async signal => {
await delay(options.sampleMs, undefined, { signal: options.signal ? AbortSignal.any([signal, options.signal]) : signal })
},
})
return { discovery: discovered, ingress: { bytes: capture.bytes, sha256: capture.sha256, existingPort: discovered.endpoint.port },
counts: capture.counts, nativeAcceptance: false }
}
@@ -0,0 +1,51 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { createChrInterfaceCounterReader } from "./traffic-flow-chr-interface-counters.js"
function fixture() {
const interfaces = [{ id: "*A", name: "nfa012bcdeea", ifIndex: "12" },
{ id: "*B", name: "nfa012bcdema", ifIndex: "13" }] as const
const calls: string[] = []
const catalog: Record<string, Record<string, unknown>> = {
"/system/identity": { name: "chr-a" },
"/system/resource": { version: "7.24.5 (stable)" },
}
for (const pin of interfaces) catalog["/interface/" + pin.id] = {
".id": pin.id, name: pin.name, type: "wg", ifindex: pin.ifIndex, running: "true", disabled: "false",
"rx-packet": "10", "tx-packet": "11", "fp-rx-packet": "0", "fp-tx-packet": "0",
"fp-rx-byte": "0", "fp-tx-byte": "0", opaque: "must stay private",
}
const config = { identity: "chr-a", version: "7.24.5 (stable)", interfaces, assertCurrent: async () => {},
client: { async get<T>(path: string, timeout?: number, _signal?: AbortSignal, bytes?: number, fence?: () => Promise<unknown>) {
assert.ok(timeout && timeout <= 1500)
assert.equal(bytes, 16384)
await fence?.()
calls.push(path)
assert.ok(Object.hasOwn(catalog, path))
return structuredClone(catalog[path]) as T
} },
}
return { config, catalog, calls }
}
test("reads only the exact native IDs with bounded identity checks and emits selected counters", async () => {
const { config, calls } = fixture()
const read = createChrInterfaceCounterReader(config)
const result = await read()
assert.deepEqual(calls, ["/system/identity", "/system/resource", "/interface/*A", "/interface/*B", "/system/identity", "/system/resource"])
assert.ok(result.startedAt <= result.observedAt)
assert.deepEqual(result.rows.map(row => row.ifindex), ["12", "13"])
assert.ok(result.rows.every(row => row["fp-rx-packet"] === "0" && !Object.hasOwn(row, "opaque")))
})
test("refuses missing and overflowing counters, changed identity and index guesses", async () => {
for (const change of [{ "fp-rx-packet": undefined }, { "fp-rx-byte": "18446744073709551616" },
{ ifindex: undefined }, { ifindex: "10" }, { ".id": "*F" }]) {
const { config, catalog } = fixture()
Object.assign(catalog["/interface/*A"], change)
await assert.rejects(createChrInterfaceCounterReader(config)(), /counter unavailable|identity or state differs/)
}
const stale = fixture()
stale.config.assertCurrent = async () => { if (stale.calls.length >= 4) throw Error("expired") }
await assert.rejects(createChrInterfaceCounterReader(stale.config)(), /expired/)
})
@@ -0,0 +1,70 @@
import { performance } from "node:perf_hooks"
import { encodeRosId, type MikrotikClient } from "../services/mikrotik.js"
import { isChrWireGuardInterfaceType } from "./traffic-flow-chr-transit-fixture.js"
type Row = Record<string, unknown>
type InterfacePin = { id: string; name: string; ifIndex: string }
type Options = {
client: Pick<MikrotikClient, "get">
identity: string
version: string
interfaces: readonly [InterfacePin, InterfacePin]
assertCurrent: () => Promise<void>
}
export const ownedWgFastPathFields = ["fp-rx-byte", "fp-tx-byte", "fp-rx-packet", "fp-tx-packet"] as const
export const ownedWgPacketFields = ["rx-packet", "tx-packet"] as const
const fields = [...ownedWgFastPathFields, ...ownedWgPacketFields]
const validId = (id: string) => /^\*[a-f0-9]{1,16}$/i.test(id)
const index = (value: unknown) => typeof value === "string" && /^[1-9][0-9]{0,9}$/.test(value) && Number(value) <= 0xffffffff
const observedUint = (value: unknown) => typeof value === "string" && /^(0|[1-9][0-9]{0,19})$/.test(value) &&
BigInt(value) <= 0xffffffffffffffffn
const deny = (reason: string): never => { throw Error("RouterOS owned counter read refused: " + reason) }
/** Uses only fixed read operations. Missing counter/index fields refuse the
* profile rather than substituting zero or guessing the wire ifIndex. */
export function createChrInterfaceCounterReader(options: Options) {
const { client, assertCurrent, identity, version } = options
const interfaces = structuredClone(options.interfaces)
if (!identity || !version || interfaces.length !== 2 || interfaces.some(pin =>
!validId(pin.id) || !/^nf[a-f0-9]{8}(ea|ma|mb|eb)$/.test(pin.name) || !index(pin.ifIndex)) ||
interfaces[0].id === interfaces[1].id || interfaces[0].ifIndex === interfaces[1].ifIndex ||
interfaces[0].name.slice(0, -2) !== interfaces[1].name.slice(0, -2) ||
!["ea,ma", "eb,mb"].includes(interfaces.map(pin => pin.name.slice(-2)).sort().join(","))) deny("invalid interface pins")
return async () => {
const startedAt = Date.now(), deadline = performance.now() + 10000
const fence = async () => {
if (performance.now() >= deadline) deny("observation expired")
await assertCurrent()
if (performance.now() >= deadline) deny("observation expired")
}
const read = async (path: string) => {
await fence()
const result = await client.get<unknown>(path, Math.min(1500, Math.ceil(deadline - performance.now())), undefined, 16384, fence)
await fence()
const rows = Array.isArray(result) ? result : [result]
if (rows.length !== 1 || !rows[0] || typeof rows[0] !== "object" || Array.isArray(rows[0])) deny("incomplete response")
return rows[0] as Row
}
const verifyIdentity = async () => {
const device = await read("/system/identity")
const resource = await read("/system/resource")
if (device.name !== identity || resource.version !== version) deny("device identity changed")
}
await verifyIdentity()
const rows: Row[] = []
for (const pin of interfaces) {
const row = await read("/interface/" + encodeRosId(pin.id))
if (row[".id"] !== pin.id || row.name !== pin.name || !isChrWireGuardInterfaceType(row.type) || row.ifindex !== pin.ifIndex ||
![true, "true", "yes"].includes(row.running as string | boolean) ||
![false, "false", "no"].includes(row.disabled as string | boolean)) deny("interface identity or state differs")
if (!fields.every(field => observedUint(row[field]))) deny("required counter unavailable")
rows.push({ ".id": pin.id, name: pin.name, type: "wg", ifindex: pin.ifIndex,
...Object.fromEntries(fields.map(field => [field, row[field]])) })
}
await verifyIdentity()
const observedAt = Date.now()
if (observedAt < startedAt || observedAt - startedAt > 10000) deny("observation wall clock changed")
return { startedAt, observedAt, rows }
}
}
@@ -0,0 +1,20 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { parseChrLinuxUdpTable } from "./traffic-flow-chr-linux-udp-owner.js"
const header = "sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode"
const row = (address: string, port: string, inode: string) =>
` 14: ${address}:${port} 00000000:0000 07 00000000:00000000 00:00000000 00000000 1000 0 ${inode} 2 0000000000000000 0`
test("parses exact kernel IPv4 socket inode and leaves unrelated UDP ports out", () => {
const text = [header, row("03F212C6", "C355", "42"), row("00000000", "C356", "43")].join("\n")
assert.deepEqual(parseChrLinuxUdpTable(text, 4, 50005), [{ address: "198.18.242.3", inode: "42" }])
assert.deepEqual(parseChrLinuxUdpTable(text, 4, 50007), [])
})
test("fails closed for malformed rows, ownerless sockets and IPv6 conflicts", () => {
assert.throws(() => parseChrLinuxUdpTable("header", 4, 50005), /kernel table/)
assert.throws(() => parseChrLinuxUdpTable(header + "\n14: malformed", 4, 50005), /kernel socket row/)
assert.throws(() => parseChrLinuxUdpTable(header + "\n" + row("00000000", "C355", "0"), 4, 50005), /unattributed/)
assert.throws(() => parseChrLinuxUdpTable(header + "\n" + row("0".repeat(32), "C355", "42"), 6, 50005), /ambiguous IPv6/)
})
@@ -0,0 +1,86 @@
import { open, opendir, readlink, stat } from "node:fs/promises"
const MAX_TABLE_BYTES = 256 * 1024
const MAX_ROWS = 2048
const deny = (reason: string): never => { throw Error("Collector UDP ownership refused: " + reason) }
type UdpOwner = { pid: number; address: string; port: number }
export function parseChrLinuxUdpTable(text: string, family: 4 | 6, port: number) {
const lines = text.trim().split("\n")
if (Buffer.byteLength(text) > MAX_TABLE_BYTES || lines.length > MAX_ROWS ||
!/^sl\s+local_address\s+(?:rem_address|remote_address)\s+st\s/.test(lines[0])) deny("unrecognized or oversized kernel table")
const width = family === 4 ? 8 : 32
const entries: { address: string; inode: string }[] = []
for (const line of lines.slice(1)) {
const values = line.trim().split(/\s+/)
const socket = values[1]?.split(":")
if (!/^\d+:$/.test(values[0]) || socket?.length !== 2 || socket[0].length !== width ||
!/^[0-9A-F]+$/.test(socket[0]) || !/^[0-9A-F]{4}$/.test(socket[1]) ||
!/^(0|[1-9][0-9]{0,19})$/.test(values[9] ?? "")) deny("incomplete kernel socket row")
if (Number.parseInt(socket[1], 16) !== port) continue
if (family !== 4 || values[9] === "0") deny("ambiguous IPv6 or unattributed socket")
const address = socket[0].match(/../g)!.reverse().map(value => Number.parseInt(value, 16)).join(".")
entries.push({ address, inode: values[9] })
}
return entries
}
async function boundedRead(path: "/proc/self/net/udp" | "/proc/self/net/udp6") {
const file = await open(path, "r")
try {
const bytes = Buffer.alloc(MAX_TABLE_BYTES + 1)
let length = 0
while (length < bytes.length) {
const { bytesRead } = await file.read(bytes, length, bytes.length - length, null)
if (bytesRead === 0) break
length += bytesRead
}
if (length > MAX_TABLE_BYTES) deny("kernel table exceeds budget")
return new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length))
} finally {
await file.close()
}
}
export async function readChrLinuxNamespaceIdentity() {
if (process.platform !== "linux") deny("Linux required")
const identity = await stat("/proc/self/ns/net", { bigint: true })
return { device: String(identity.dev), inode: String(identity.ino) }
}
/** Self-process observation in the collector namespace, with no process
* execution, pathname overrides or inference from a missing socket owner. */
export async function readChrLinuxUdpOwner(options: {
namespace: { device: string; inode: string }; port: number; address: string
}): Promise<UdpOwner[]> {
const { namespace, port, address } = options
const current = async () => {
const identity = await readChrLinuxNamespaceIdentity()
if (identity.device !== namespace.device || identity.inode !== namespace.inode) deny("network namespace changed")
}
await current()
const scan = async () => {
const ipv4 = parseChrLinuxUdpTable(await boundedRead("/proc/self/net/udp"), 4, port)
parseChrLinuxUdpTable(await boundedRead("/proc/self/net/udp6"), 6, port)
if (ipv4.length > 1 || ipv4.some(row => row.address !== address)) deny("competing or wildcard UDP bind")
return ipv4[0]
}
const candidate = await scan()
if (candidate) {
let matches = 0, visited = 0
const directory = await opendir("/proc/self/fd")
for await (const entry of directory) {
if (++visited > 2048 || !/^\d+$/.test(entry.name)) deny("process descriptor inventory exceeds budget")
try {
if (await readlink("/proc/self/fd/" + entry.name) === `socket:[${candidate.inode}]`) matches++
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
}
}
if (matches !== 1) deny("UDP socket is not exclusively identified in the collector process")
}
const recheck = await scan()
if (candidate?.inode !== recheck?.inode) deny("UDP socket changed during observation")
await current()
return candidate ? [{ pid: process.pid, address, port }] : []
}
@@ -0,0 +1,26 @@
import { wireguardInterfaceDependencyPaths } from "../services/routeros-native-paths.js"
const readPaths = new Set(["/system/identity", "/system/resource", "/interface", "/interface/wireguard",
"/interface/wireguard/peers", "/interface/bridge", "/interface/list/member", "/ip/address", "/ip/route",
"/ipv6/address", "/ipv6/route", "/routing/route", "/routing/rule", "/routing/table", "/ip/vrf", "/ip/settings",
"/ip/firewall/filter", "/ip/firewall/raw", "/ip/firewall/mangle", "/ip/firewall/nat", "/ipv6/firewall/filter",
"/ip/firewall/connection/tracking", "/ip/firewall/connection", "/ip/traffic-flow", "/ip/traffic-flow/ipfix",
"/ip/traffic-flow/target", ...wireguardInterfaceDependencyPaths])
const rowMenus = ["/interface/wireguard", "/interface/wireguard/peers", "/ip/address", "/ip/route", "/ip/traffic-flow/target"]
export const targetMenu = "/ip/traffic-flow/target"
const singletonMenus = ["/ip/traffic-flow", "/ip/traffic-flow/ipfix"]
export const nativeId = /^\*[0-9a-f]{1,16}$/i
const deny = () => { throw Error("Owned CHR namespace relay refused") }
export function chrNamespaceRelayOperation(method: string, rest: string) {
if (!rest.startsWith("/rest/")) return deny()
const path = rest.slice(5)
const last = path.lastIndexOf("/")
const menu = path.slice(0, last), token = path.slice(last + 1)
if (method === "GET" && (readPaths.has(path) || menu === "/interface" && nativeId.test(token))) return { path, write: false }
if (method === "PUT" && rowMenus.includes(path) ||
method === "DELETE" && rowMenus.includes(menu) && nativeId.test(token) ||
method === "PATCH" && (singletonMenus.includes(menu) || menu === targetMenu) && nativeId.test(token) ||
method === "POST" && singletonMenus.includes(menu) && token === "set") return { path, write: true }
return deny()
}
@@ -0,0 +1,142 @@
import assert from "node:assert/strict"
import { execFile } from "node:child_process"
import { createHash } from "node:crypto"
import { mkdtemp, rm, writeFile } from "node:fs/promises"
import { join } from "node:path"
import { fileURLToPath } from "node:url"
import { promisify } from "node:util"
import { test } from "node:test"
import { replayRecordedChrEvidence } from "./traffic-flow-chr-offline-replay.js"
import { verifyChrOwnedWgEvidence } from "./traffic-flow-chr-owned-wg-evidence.js"
const digest = (value: Buffer | string) => createHash("sha256").update(value).digest("hex")
function fixture(ethernet = false, earlyTemplates = false) {
const link = ethernet ? 14 : 0
const seconds = 1700000010
const bucket = Math.floor(seconds / 60) * 60000
const from = new Date(bucket).toISOString(), until = new Date(bucket + 60000).toISOString()
const scope = { sources: ["192.0.2.10", "192.0.2.20"], destination: "192.0.2.100", port: 4739, ...(ethernet ? { linkType: "EN10MB" as const } : {}) }
const fields = [[1, 4], [2, 4], [4, 1], [7, 2], [8, 4], [10, 4], [11, 2], [12, 4], [14, 4]]
const template = Buffer.alloc(8 + fields.length * 4)
template.writeUInt16BE(2); template.writeUInt16BE(template.length, 2)
template.writeUInt16BE(256, 4); template.writeUInt16BE(fields.length, 6)
fields.forEach(([id, width], index) => { template.writeUInt16BE(id, 8 + index * 4); template.writeUInt16BE(width, 10 + index * 4) })
const data = Buffer.alloc(33)
data.writeUInt16BE(256); data.writeUInt16BE(data.length, 2)
data.writeUInt32BE(1200, 4); data.writeUInt32BE(3, 8); data[12] = 17
data.writeUInt16BE(9000, 13); Buffer.from([192, 0, 2, 1]).copy(data, 15); data.writeUInt32BE(11, 19)
data.writeUInt16BE(9001, 23); Buffer.from([198, 51, 100, 1]).copy(data, 25); data.writeUInt32BE(12, 29)
const ipfix = Buffer.alloc(16)
ipfix.writeUInt16BE(10); ipfix.writeUInt16BE(ipfix.length + template.length + data.length, 2); ipfix.writeUInt32BE(7, 12)
const header = Buffer.alloc(24)
header.writeUInt32LE(0xa1b2c3d4); header.writeUInt16LE(2, 4); header.writeUInt16LE(4, 6)
header.writeUInt32LE(4124 + link, 16); header.writeUInt32LE(ethernet ? 1 : 101, 20)
const frames = [10, 20, 10, 20].map((octet, index) => ({
octet, at: seconds, micros: index * 1000, sequence: Math.floor(index / 2),
sets: earlyTemplates && index < 2 ? [data] : [template, data],
}))
if (earlyTemplates) frames.unshift(...[10, 20].map(octet =>
({ octet, at: seconds - 1, micros: 0, sequence: 0, sets: [template] })))
const packets = frames.map(({ octet, at, micros, sequence, sets }) => {
const payload = Buffer.concat([ipfix, ...sets])
payload.writeUInt16BE(payload.length, 2)
const packet = Buffer.alloc(44 + link + payload.length)
packet.writeUInt32LE(at, 0); packet.writeUInt32LE(micros, 4)
packet.writeUInt32LE(packet.length - 16, 8); packet.writeUInt32LE(packet.length - 16, 12)
if (ethernet) packet.writeUInt16BE(0x0800, 28)
const ip = packet.subarray(16 + link)
ip[0] = 0x45; ip.writeUInt16BE(ip.length, 2); ip[9] = 17
Buffer.from([192, 0, 2, octet, 192, 0, 2, 100]).copy(ip, 12)
ip.writeUInt16BE(2055, 20); ip.writeUInt16BE(scope.port, 22); ip.writeUInt16BE(ip.length - 20, 24)
payload.copy(ip, 28)
ip.writeUInt32BE(seconds, 32)
ip.writeUInt32BE(sequence, 36)
return packet
})
const pcap = Buffer.concat([header, ...packets])
const raw = [1, 2].map(serverId => ({ serverId, bucketAt: from, src: "192.0.2.1", dst: "198.51.100.1",
proto: 17, srcPort: 9000, dstPort: 9001, inIface: "11", outIface: "12", bytes: "2400", packets: "6" }))
const minute = raw.map(({ serverId, bucketAt, bytes, packets }) => ({ serverId, bucketAt, bytes, packets }))
const evidence = {
captureSha256: digest(pcap), complete: true, dropped: 0, scope, serverIds: [1, 2], from, until, minutes: 2,
interfaceLabels: [1, 2].map(serverId => ({ serverId, index: "11", label: "ether-" + serverId })),
before: { raw, minute }, after: structuredClone({ raw, minute }),
talkers: raw.map(row => ({ serverId: row.serverId, result: {
serverIds: [row.serverId], from: new Date(bucket - 60000).toISOString(), to: until,
topConversationsLimit: 20, total: 1, totalConversations: 1, observedExporters: 1, retainedRowsBytes: 2400, nextOffset: null,
coverageVerified: false, rowSumIsNetworkTotal: false, deduplicatedAcrossInterfaces: false, forwardingVerified: false,
items: [{ ...row, serverId: String(row.serverId), inIface: "ether-" + row.serverId, inIfaceIndex: "11", bytes: 2400, packets: 6, bps: 160 }],
} })),
}
return { pcap, evidence }
}
test("owned WG evidence requires exact IPFIX tuples and unchanged FastPath counters", () => {
const { pcap, evidence } = fixture()
const makeExporter = (roles: [string, string]) => {
const interfaces = roles.map((role, index) => ({ id: "*" + (index + 1), name: "nfa012bcde" + role,
ifIndex: String(index + 11) })) as [{ id: string; name: string; ifIndex: string }, { id: string; name: string; ifIndex: string }]
const snapshot = (observedAt: number, packetCount: string) => ({ startedAt: observedAt, observedAt, rows: interfaces.map(item => ({
".id": item.id, name: item.name, type: "wg", ifindex: item.ifIndex, "rx-packet": packetCount, "tx-packet": packetCount,
"fp-rx-byte": "0", "fp-tx-byte": "0", "fp-rx-packet": "0", "fp-tx-packet": "0",
})) })
return { interfaces, before: snapshot(1700000010000, "0"), after: snapshot(1700000011000, "6") }
}
const input = { bytes: pcap, sha256: evidence.captureSha256, complete: true, dropped: 0,
scope: { ...evidence.scope, sources: [evidence.scope.sources[0], evidence.scope.sources[1]] as [string, string] },
exporters: [makeExporter(["ea", "ma"]), makeExporter(["mb", "eb"])] as const,
expected: ([0, 1] as const).map(exporter => ({ exporter, src: "192.0.2.1", dst: "198.51.100.1",
srcPort: 9000, dstPort: 9001, inIface: "11", outIface: "12", packets: 6 })),
}
assert.equal(verifyChrOwnedWgEvidence(input).nativeAcceptance, false)
const initialAndRefreshed = input.expected.flatMap(row => (["initial", "refreshed"] as const)
.map(phase => ({ ...row, phase, packets: 3 })))
assert.equal(verifyChrOwnedWgEvidence({ ...input, expected: initialAndRefreshed, requireRefresh: true }).exactIpfixMatched, true)
const staged = fixture(false, true)
const stagedInput = { ...input, bytes: staged.pcap, sha256: staged.evidence.captureSha256,
expected: initialAndRefreshed, requireRefresh: true }
assert.throws(() => verifyChrOwnedWgEvidence(stagedInput), /counter window/)
assert.equal(verifyChrOwnedWgEvidence({ ...stagedInput, counterWindow: "traffic" }).exactIpfixMatched, true)
input.exporters[0].before.observedAt++
assert.throws(() => verifyChrOwnedWgEvidence({ ...stagedInput, counterWindow: "traffic" }), /counter window/)
input.exporters[0].before.observedAt--
assert.throws(() => verifyChrOwnedWgEvidence({ ...input, requireRefresh: true }), /initial and refreshed/)
assert.throws(() => verifyChrOwnedWgEvidence({ ...input, expected: input.expected.map(row => ({ ...row, packets: 5 })) }), /totals/)
input.exporters[0].after.rows[0]["fp-rx-packet"] = "1"
assert.throws(() => verifyChrOwnedWgEvidence(input), /FastPath/)
})
test("offline CLI joins original multi-set IPFIX pcap, product parser and recorded SQL/MCP evidence", async () => {
const { pcap, evidence } = fixture()
const dir = await mkdtemp(join(fileURLToPath(new URL("../../../tmp/", import.meta.url)), "mm-chr-offline-"))
try {
const packetPath = join(dir, "owned.pcap"), evidencePath = join(dir, "owned.json"), body = JSON.stringify(evidence)
await Promise.all([writeFile(packetPath, pcap), writeFile(evidencePath, body)])
const { stdout } = await promisify(execFile)(process.execPath, ["--import", "tsx",
fileURLToPath(new URL("./traffic-flow-chr-offline-replay.ts", import.meta.url)), packetPath, digest(pcap), evidencePath, digest(body)],
{ timeout: 15000, windowsHide: true })
const result = JSON.parse(stdout)
assert.equal(result.recordedEvidenceMatched, true)
assert.equal(result.replay.frames, 4)
assert.equal(result.reconciliation.rawRows, 2)
assert.equal(result.actualNativeAcceptance, false)
} finally {
await rm(dir, { recursive: true, force: true })
}
})
test("refuses a mixed capture pin or drift in post-MCP SQL evidence", async () => {
const { pcap, evidence } = fixture()
await assert.rejects(replayRecordedChrEvidence(pcap, "0".repeat(64), evidence), /Capture SHA/)
evidence.after.minute[0].packets = "5"
await assert.rejects(replayRecordedChrEvidence(pcap, digest(pcap), evidence), /persisted SQL/)
})
test("offline replay preserves an explicit Ethernet capture scope across both decoders", async () => {
const { pcap, evidence } = fixture(true)
const result = await replayRecordedChrEvidence(pcap, digest(pcap), evidence)
assert.equal(result.recordedEvidenceMatched, true)
assert.equal(result.replay.frames, 4)
await assert.rejects(replayRecordedChrEvidence(pcap, digest(pcap), { ...evidence,
scope: { ...evidence.scope, linkType: "RAW" } }), /pinned link type RAW/)
})
@@ -0,0 +1,129 @@
/** Offline comparison of one pcap with recorded SQL and MCP evidence. */
import { createHash } from "node:crypto"
import { open } from "node:fs/promises"
import { isIPv4 } from "node:net"
import { resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { z } from "zod"
import { checkChrCapture } from "./traffic-flow-chr-capture-check.js"
import { decodeOwnedIpfix } from "./traffic-flow-chr-independent-ipfix.js"
import { reconcileOwnedChrFlow } from "./traffic-flow-chr-reconcile.js"
import { projectChrWire } from "./traffic-flow-chr-wire.js"
const sha = z.string().regex(/^[a-f0-9]{64}$/)
const ip = z.string().max(15).refine(isIPv4)
const serverId = z.number().int().positive().safe()
const snapshot = z.object({ raw: z.array(z.unknown()).max(200), minute: z.array(z.unknown()).max(20) }).strict()
const evidenceSchema = z.object({
captureSha256: sha, complete: z.literal(true), dropped: z.literal(0),
scope: z.object({ sources: z.tuple([ip, ip]), destination: ip, port: z.number().int().min(1).max(65535), linkType: z.enum(["RAW", "EN10MB"]).optional() }).strict(),
serverIds: z.tuple([serverId, serverId]),
from: z.string().max(24), until: z.string().max(24), minutes: z.number().int().min(1).max(15),
interfaceLabels: z.array(z.object({ serverId, index: z.string().regex(/^(?:0|[1-9]\d{0,9})$/), label: z.string().min(1).max(200) }).strict()).max(200),
before: snapshot, after: snapshot,
talkers: z.tuple([z.object({ serverId, result: z.unknown() }).strict(), z.object({ serverId, result: z.unknown() }).strict()]),
}).strict()
function must(value: unknown, reason: string): asserts value {
if (!value) throw Error(reason)
}
function replayCounter(value: string) {
const integer = BigInt(value)
must(integer <= BigInt(Number.MAX_SAFE_INTEGER), "Product replay requires exact JavaScript integer counters")
return Number(integer)
}
export function prepareChrReplay(wire: ReturnType<typeof projectChrWire>) {
const decoded = decodeOwnedIpfix(wire)
const exporters = wire.scope.sources.map((address, index) => {
const frames = wire.frames.filter(frame => frame.exporter === index)
const first = frames[0]
must(first && frames.every(frame => frame.sourcePort === first.sourcePort && frame.version === first.version && frame.domain === first.domain), "Exporter session changed")
const ids = new Set(decoded.frames.flatMap((frame, ordinal) => wire.frames[ordinal].exporter === index ? frame.templateIds : []))
must(ids.size === 1, "Replay requires one pinned template ID per exporter")
return { address, port: first.sourcePort, version: first.version, domain: first.domain, templateId: [...ids][0] }
})
const phase = [0, 0]
const frames = wire.frames.map((frame, index) => {
const { flows, templateIds } = decoded.frames[index]
let kind = "data"
if (templateIds.length) kind = phase[frame.exporter]++ === 0 ? "template" : "refresh"
const expected = flows.map(({ src, dst, bytes, packets, inIface, outIface }) =>
({ src, dst, bytes: replayCounter(bytes), packets: replayCounter(packets), inIface, outIface }))
return { exporter: frame.exporter, ordinal: frame.ordinal, receivedMs: frame.receivedMs, payloadHex: frame.payloadHex, kind, expected }
})
return { decoded, projection: { destination: wire.scope.destination, exporters, frames } }
}
export async function replayRecordedChrEvidence(bytes: Buffer, expectedSha: string, rawEvidence: unknown) {
const evidence = evidenceSchema.parse(rawEvidence)
const wire = projectChrWire(bytes, evidence.scope)
must(wire.captureSha256 === expectedSha && evidence.captureSha256 === expectedSha, "Capture SHA-256 does not match recorded evidence")
const { decoded, projection } = prepareChrReplay(wire)
const replay = checkChrCapture(projection)
const labels = new Map<string, string>()
for (const row of evidence.interfaceLabels) {
const key = row.serverId + ":" + row.index
must(evidence.serverIds.includes(row.serverId) && !labels.has(key), "Interface evidence identity differs")
labels.set(key, row.label)
}
must(evidence.talkers.every((row, index) => row.serverId === evidence.serverIds[index]), "Public evidence order or scope differs")
let snapshots = 0, publicReads = 0
const reconciliation = await reconcileOwnedChrFlow({
wire, decoded, serverIds: evidence.serverIds, from: evidence.from, until: evidence.until, minutes: evidence.minutes,
interfaceLabel: (id, index) => {
const label = labels.get(id + ":" + index)
must(label, "Missing independently observed interface label")
return label
},
readOnlySnapshot: async work => {
const recorded = [evidence.before, evidence.after][snapshots++]
must(recorded, "Unexpected SQL evidence read")
return work(async sql => {
if (sql.includes("FROM flow_buckets")) return recorded.raw
must(sql.includes("FROM flow_minute_stats"), "Unexpected SQL evidence query")
return recorded.minute
})
},
callTool: async (tool, request) => {
const recorded = evidence.talkers[publicReads++]
must(tool === "mm_netflow_talkers_get" && recorded?.serverId === request.serverId, "Unexpected recorded MCP scope")
return recorded.result
},
assertCurrent: async () => { /* Offline comparison of immutable recorded evidence. */ },
})
return { captureSha256: wire.captureSha256, replay, reconciliation, recordedEvidenceMatched: true, actualNativeAcceptance: false }
}
async function readPinned(path: string, digest: string) {
sha.parse(digest)
const file = await open(path, "r")
try {
const info = await file.stat()
must(info.isFile() && info.size > 0 && info.size <= 512 * 1024, "Offline evidence file outside size bound")
const bytes = Buffer.alloc(info.size + 1)
let read = 0
while (read < bytes.length) {
const { bytesRead } = await file.read(bytes, read, bytes.length - read, read)
if (!bytesRead) break
read += bytesRead
}
const body = bytes.subarray(0, read)
must(read === info.size && createHash("sha256").update(body).digest("hex") === digest, "Offline evidence hash differs")
return body
} finally {
await file.close()
}
}
async function main() {
const args = process.argv.slice(2)
must(args.length === 4, "Expected pcap and recorded evidence pins")
const [capture, captureSha, evidence, evidenceSha] = args
const [bytes, raw] = await Promise.all([readPinned(capture, captureSha), readPinned(evidence, evidenceSha)])
console.log(JSON.stringify({ evidenceSha256: evidenceSha, ...await replayRecordedChrEvidence(bytes, captureSha, JSON.parse(raw.toString("utf8"))) }))
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch(() => {
console.error("Recorded CHR evidence refused")
process.exitCode = 1
})
}
@@ -89,6 +89,17 @@ test("owned journal is synchronized before target dispatch and captures target c
assert.deepEqual(events, ["start", "create:A", "create:B", "exercise", "delete:B", "delete:A", "stop", "restore", "reconcile"])
await assert.rejects(executeOwnedChrCapture(options), { code: "EEXIST" })
assert.equal(events.length, 9)
bound = true
await assert.rejects(executeOwnedChrCapture({
...options, journalPath: join(directory, "aborted.jsonl"),
capture: async run => {
const owner = new AbortController()
owner.abort()
await run(owner.signal)
return fixtures(Date.now())
},
}), /ownership ended/)
assert.equal(events.length, 9)
} finally {
await rm(directory, { recursive: true, force: true })
}
@@ -6,7 +6,7 @@ import { createChrTargetHarness } from "./traffic-flow-chr-target-harness.js"
import { projectChrWire } from "./traffic-flow-chr-wire.js"
import { checkChrCapture } from "./traffic-flow-chr-capture-check.js"
type Target = Parameters<typeof createChrTargetHarness>[0]
type Target = Extract<Parameters<typeof createChrTargetHarness>[0], { stagedDisabled?: false }>
type Scope = Parameters<typeof projectChrWire>[1]
type Wire = ReturnType<typeof projectChrWire>
type Projection = {
@@ -15,10 +15,19 @@ type Projection = {
frames: readonly { exporter: number; ordinal: number; receivedMs: number; payloadHex: string }[]
}
type Capture = { bytes: Buffer; sha256: string; dropped: number; complete: boolean; projection: Projection }
type Ingress = { bytes: Buffer; sha256: string; existingPort: number }
function must(value: unknown, reason: string): asserts value {
if (!value) throw Error(reason)
}
export function checkChrBaselineIngress(input: Ingress, scope: Scope, now = Date.now()) {
const wire = projectChrWire(input.bytes, { sources: scope.sources, destination: scope.destination, port: input.existingPort })
must(wire.captureSha256 === input.sha256, "Baseline ingress SHA-256 differs")
must(wire.frames.every(frame => frame.receivedMs >= now - 10 * 60_000 && frame.receivedMs <= now),
"Baseline ingress is outside the current evidence window")
return wire
}
/** Bind every reviewed row to its original wire record and require complete
* capture coverage for conservation. Sample-only replay uses the separate CLI. */
export function bindChrCapture(capture: Capture, scope: Scope) {
@@ -42,11 +51,11 @@ type Options = {
journalPath: string
deadlineAt: number
scope: Scope
ingress: { bytes: Buffer; sha256: string; existingPort: number }
ingress: Ingress
serverIds: readonly [number, number]
targets: readonly [Omit<Target, "source" | "destination" | "port" | "authorize" | "checkpoint">, Omit<Target, "source" | "destination" | "port" | "authorize" | "checkpoint">]
collector: { authorize: () => Promise<void>; assertBound: () => Promise<void>; restore: () => Promise<void> }
capture: (exercise: () => Promise<void>) => Promise<Capture>
capture: (exercise: (signal?: AbortSignal) => Promise<void>) => Promise<Capture>
exercise: () => Promise<void>
reconcile: (wire: Wire, serverIds: readonly [number, number]) => Promise<void>
}
@@ -54,17 +63,13 @@ type Options = {
export async function executeOwnedChrCapture(options: Options) {
const scope = structuredClone(options.scope), serverIds = Object.freeze([...options.serverIds]) as readonly [number, number]
const deadlineAt = options.deadlineAt
let active = true
const live = () => must(active && Number.isSafeInteger(deadlineAt) && Date.now() < deadlineAt, "Owned capture deadline expired")
let active = true, captureSignal: AbortSignal | undefined
const live = () => must(active && !captureSignal?.aborted && Number.isSafeInteger(deadlineAt) && Date.now() < deadlineAt, "Owned capture deadline or ownership ended")
live()
must(deadlineAt - Date.now() <= 10 * 60_000 && serverIds.length === 2 && new Set(serverIds).size === 2 &&
serverIds.every(id => Number.isSafeInteger(id) && id > 0) && options.targets.length === 2 &&
options.targets[0].baseline.deviceId !== options.targets[1].baseline.deviceId, "Invalid owned capture identity")
const ingress = projectChrWire(options.ingress.bytes, { ...scope, port: options.ingress.existingPort })
must(ingress.captureSha256 === options.ingress.sha256, "Baseline ingress SHA-256 differs")
const admittedAt = Date.now()
must(ingress.frames.every(frame => frame.receivedMs >= admittedAt - 10 * 60_000 && frame.receivedMs <= admittedAt),
"Baseline ingress is outside the current evidence window")
const ingress = checkChrBaselineIngress(options.ingress, scope)
const file = await open(options.journalPath, "wx", 0o600)
const append = async (record: object) => {
live()
@@ -93,9 +98,10 @@ export async function executeOwnedChrCapture(options: Options) {
await authorize()
let started = false, exercised = false
const captureStartedAt = Date.now()
const capture = await options.capture(async () => {
const capture = await options.capture(async signal => {
must(!started, "Capture exercise was already attempted")
started = true
captureSignal = signal
await authorize()
for (const target of targets) await target.create()
await authorize()
@@ -106,6 +112,7 @@ export async function executeOwnedChrCapture(options: Options) {
})
const captureEndedAt = Date.now()
must(exercised, "Capture omitted or did not complete its exercise")
captureSignal = undefined
await authorize()
await append({ phase: "collector-restore-intent" })
await options.collector.authorize()
@@ -0,0 +1,92 @@
import { decodeOwnedIpfix } from "./traffic-flow-chr-independent-ipfix.js"
import { projectChrWire } from "./traffic-flow-chr-wire.js"
import { ownedWgFastPathFields as fp, ownedWgPacketFields as packets } from "./traffic-flow-chr-interface-counters.js"
import { isChrWireGuardInterfaceType } from "./traffic-flow-chr-transit-fixture.js"
type Scope = Parameters<typeof projectChrWire>[1]
type Counters = { startedAt: number; observedAt: number; rows: readonly Record<string, unknown>[] }
type Interface = { id: string; name: string; ifIndex: string }
type Expected = { exporter: 0 | 1; src: string; dst: string; srcPort: number; dstPort: number;
inIface: string; outIface: string; packets: number; phase?: "initial" | "refreshed" }
function must(value: unknown, reason: string): asserts value { if (!value) throw Error(reason) }
function counter(row: Record<string, unknown>, name: string) {
const value = row[name]
must(typeof value === "string" && /^(0|[1-9][0-9]{0,19})$/.test(value), "Owned interface counter unavailable")
return BigInt(value)
}
/** Requires real before/after counters from the runner's pinned native reader
* and original pcap bytes. Artifact agreement alone confers no live authority. */
export function verifyChrOwnedWgEvidence(options: {
bytes: Buffer; sha256: string; complete: boolean; dropped: number; scope: Scope
exporters: readonly [{ interfaces: readonly [Interface, Interface]; before: Counters; after: Counters },
{ interfaces: readonly [Interface, Interface]; before: Counters; after: Counters }]
expected: readonly Expected[]
requireRefresh?: boolean
counterWindow?: "traffic"
}) {
const { scope, expected, exporters } = options
const wire = projectChrWire(options.bytes, scope)
must(options.complete === true && options.dropped === 0 && options.sha256 === wire.captureSha256, "Owned capture provenance differs")
const decoded = decodeOwnedIpfix(wire)
must(options.counterWindow !== "traffic" || options.requireRefresh, "Staged traffic counters require refreshed data")
const matches = expected.map(() => 0n)
must(expected.length >= 2 && expected.length <= 16 && [0, 1].every(exporter => expected.some(flow => flow.exporter === exporter)) &&
expected.every(flow => Number.isSafeInteger(flow.packets) && flow.packets > 0), "Expected owned flow scope is incomplete")
const sessions = new Map<number, { key: string; templateId: number; phase: "initial" | "refreshed" }>()
if (options.requireRefresh) must([0, 1].every(exporter =>
["initial", "refreshed"].every(phase => expected.some(pin => pin.exporter === exporter && pin.phase === phase))),
"Both exporters need initial and refreshed flow pins")
for (const [index, { flows, templateIds }] of decoded.frames.entries()) {
const { exporter, sourcePort, domain } = wire.frames[index]
if (options.requireRefresh) {
const key = sourcePort + "/" + domain
const session = sessions.get(exporter)
must(!session || session.key === key, "Owned exporter session changed")
if (templateIds.length) {
must(templateIds.length === 1 && (!session || session.templateId === templateIds[0]), "Owned template identity changed")
sessions.set(exporter, { key, templateId: templateIds[0], phase: session ? "refreshed" : "initial" })
}
}
for (const flow of flows) {
const matching = expected.map((pin, offset) => ({ pin, offset })).filter(({ pin }) =>
pin.exporter === exporter && pin.src === flow.src && pin.dst === flow.dst && pin.srcPort === flow.srcPort &&
pin.dstPort === flow.dstPort && pin.inIface === flow.inIface && pin.outIface === flow.outIface && flow.proto === 17 &&
(!options.requireRefresh || sessions.get(exporter)?.phase === pin.phase))
must(matching.length === 1, "Unscoped or ambiguous owned IPFIX flow")
matches[matching[0].offset] += BigInt(flow.packets)
}
}
must(expected.every((pin, offset) => matches[offset] === BigInt(pin.packets)), "Owned IPFIX packet totals differ")
for (const [index, { before, after, interfaces }] of exporters.entries()) {
// A staged target can emit a template as soon as it is enabled. Only
// record-bearing frames are relevant to the forwarding counter interval.
const timestamps = wire.frames.filter((frame, offset) => frame.exporter === index &&
(options.counterWindow !== "traffic" || decoded.frames[offset].flows.length > 0)).map(frame => frame.receivedMs)
must([before.startedAt, before.observedAt, after.startedAt, after.observedAt].every(Number.isSafeInteger) &&
before.startedAt <= before.observedAt && after.startedAt <= after.observedAt &&
timestamps.length > 0 && before.observedAt <= Math.min(...timestamps) && after.startedAt >= Math.max(...timestamps) &&
after.observedAt - before.startedAt <= 180000 &&
interfaces[0].id !== interfaces[1].id && interfaces[0].name !== interfaces[1].name &&
interfaces[0].ifIndex !== interfaces[1].ifIndex &&
interfaces.every(pin => /^\*[a-f0-9]{1,16}$/i.test(pin.id) && /^nf[a-f0-9]{8}(ea|ma|mb|eb)$/.test(pin.name) &&
/^[1-9][0-9]{0,9}$/.test(pin.ifIndex)) &&
expected.filter(flow => flow.exporter === index).every(flow => flow.inIface !== flow.outIface &&
interfaces.some(pin => pin.ifIndex === flow.inIface) && interfaces.some(pin => pin.ifIndex === flow.outIface)),
"Owned counter window or interface scope differs")
for (const pin of interfaces) {
const read = (sample: Counters) => {
const rows = sample.rows.filter(row => row[".id"] === pin.id || row.name === pin.name)
must(rows.length === 1 && rows[0][".id"] === pin.id && rows[0].name === pin.name && isChrWireGuardInterfaceType(rows[0].type) &&
rows[0].ifindex === pin.ifIndex,
"Owned interface counter identity differs")
return rows[0]
}
const old = read(before), current = read(after)
must(fp.every(name => counter(old, name) === counter(current, name)), "Owned interface FastPath counter changed")
must(packets.every(name => counter(current, name) > counter(old, name)), "Owned interface did not observe bidirectional packets")
}
}
return { captureSha256: wire.captureSha256, exactIpfixMatched: true as const,
ownedFastPathDeltasZero: true as const, nativeAcceptance: false as const }
}
@@ -0,0 +1,127 @@
import { fstatSync } from "node:fs"
import { Socket } from "node:net"
import type { Duplex } from "node:stream"
import { createChrParentOrchestration } from "./traffic-flow-chr-parent-orchestration.js"
type Parent = Awaited<ReturnType<typeof createChrParentOrchestration>>
/** The root launcher inherits one private socketpair end from its credential
* verifying AF_UNIX acceptor. The collector's pathname never reaches Node. */
function inheritedSocket(descriptor: number) {
if (process.platform !== "linux" || process.geteuid?.() !== 0 ||
!Number.isSafeInteger(descriptor) || descriptor < 3 || descriptor > 1024 ||
!fstatSync(descriptor).isSocket()) {
throw Error("Inherited parent CHR admission socket refused")
}
return new Socket({ fd: descriptor, readable: true, writable: true, allowHalfOpen: true })
}
export async function serveChrParentInheritedAdmission(parent: Parent, descriptor: number) {
let socket: Socket | undefined
try {
socket = inheritedSocket(descriptor)
return await parent.serve({ input: socket, output: socket })
} finally {
socket?.destroy()
parent.retire()
}
}
/** Ask the host owner to recheck actual bridge milestones. This channel is
* separate from the collector connection and has no mutation operations. */
export function openChrParentHostPhaseChecks(descriptor: number, deadlineAt: number, staged = false) {
return openChrParentConnectedPhaseChecks(inheritedSocket(descriptor), deadlineAt, staged)
}
export function openChrParentConnectedPhaseChecks(socket: Duplex, deadlineAt: number, staged = false) {
socket.on("error", () => {})
const responses = socket[Symbol.asyncIterator]()
let sequence = 0, pending = false
const check = async (phase: "preflight" | "stage" | "ready" | "drain" | "inverse" | "timer") => {
if (pending || socket.destroyed || ++sequence > (staged ? 99 : 91) || Date.now() >= deadlineAt) {
socket.destroy()
throw Error("Host phase query refused")
}
pending = true
const timer = setTimeout(() => socket.destroy(), Math.min(8000, deadlineAt - Date.now()))
try {
const expected = sequence + ":ok"
await new Promise<void>((resolve, reject) => {
socket.write(sequence + ":" + phase + "\n", error => error ? reject(error) : resolve())
})
let received = Buffer.alloc(0)
while (received.at(-1) !== 10) {
const { value, done } = await responses.next()
if (done || !Buffer.isBuffer(value) || received.length + value.length > 64) throw Error("Host phase query refused")
received = Buffer.concat([received, value])
}
const text = received.toString("ascii")
if (phase !== "timer") {
if (text !== expected + "\n") throw Error("Host phase query refused")
return
}
const deadline = text.startsWith(expected + ":") ? text.slice(expected.length + 1) : ""
const value = Number(deadline.trim())
if (!/^[1-9][0-9]{12}\n$/.test(deadline) || !Number.isSafeInteger(value) || value <= Date.now() ||
value > Math.min(deadlineAt, Date.now() + 240000)) throw Error("Host capture deadline refused")
return value
} catch (error) { socket.destroy(); throw error }
finally { clearTimeout(timer); pending = false }
}
const fence = async (phase: Exclude<Parameters<typeof check>[0], "timer">) => { await check(phase) }
return Object.freeze({
assertTimingPreflight: () => fence("preflight"),
...(staged ? { assertTransitAndCollectorStagingReady: () => fence("stage") } : {}),
assertTransitAndCollectorReady: () => fence("ready"),
assertCaptureAndDrainComplete: () => fence("drain"),
assertLinuxInverseReady: () => fence("inverse"),
async captureDeadline() {
if (!staged) throw Error("Staged host capture deadline unavailable")
const result = await check("timer")
if (result === undefined) throw Error("Staged host capture deadline unavailable")
return result
},
close: () => socket.destroy(),
})
}
type HostCheck = keyof Omit<ReturnType<typeof openChrParentHostPhaseChecks>, "close" | "captureDeadline">
/** The host launches one root Node with two private descriptors. Call serve
* after opening the CHR relay with the returned hooks and sealing its pins. */
export async function openChrParentAdmissionTransport(
options: Omit<Parameters<typeof createChrParentOrchestration>[0], HostCheck>,
descriptors: { admission: number; phase: number },
) {
if (descriptors.admission === descriptors.phase) throw Error("Parent admission descriptors overlap")
const socket = inheritedSocket(descriptors.admission)
socket.on("error", () => {})
let phases: ReturnType<typeof openChrParentHostPhaseChecks> | undefined
let parent: Parent | undefined
try {
phases = openChrParentHostPhaseChecks(descriptors.phase, options.deadlineAt, !!options.stagedTarget)
parent = await createChrParentOrchestration({ ...options, ...phases })
const owner = parent
const close = () => { owner.retire(); socket.destroy(); phases?.close() }
return Object.freeze({
authorizeWrite: owner.authorizeWrite,
recordWriteAcknowledgement: owner.recordWriteAcknowledgement,
async serve() {
try { return await owner.serve({ input: socket, output: socket }) }
finally { close() }
},
close,
})
} catch (error) {
parent?.retire()
socket.destroy()
phases?.close()
throw error
}
}
@@ -0,0 +1,68 @@
import type { Duplex } from "node:stream"
import type { createChrParentOrchestration } from "./traffic-flow-chr-parent-orchestration.js"
import type { ChrWriteIntent } from "./traffic-flow-chr-write-admission.js"
type Owner = Awaited<ReturnType<typeof createChrParentOrchestration>>
type Hooks = Pick<Owner, "authorizeWrite" | "recordWriteAcknowledgement">
const frameLimit = 16384
const nativeId = /^\*[a-f0-9]{1,16}$/i
const fail = () => { throw Error("Parent native boundary refused. Automatic replay forbidden.") }
const object = (raw: unknown): raw is Record<string, unknown> => !!raw && typeof raw === "object" && !Array.isArray(raw)
const shape = (raw: Record<string, unknown>, keys: string[]) =>
Object.keys(raw).length === keys.length && keys.every(key => Object.hasOwn(raw, key))
async function* frames(socket: Duplex) {
let buffered = Buffer.alloc(0)
for await (const data of socket) {
const bytes = Buffer.isBuffer(data) ? data : Buffer.from(data)
if (bytes.length > frameLimit * 2) return fail()
for (let offset = 0; offset < bytes.length;) {
const end = bytes.indexOf(10, offset)
const length = (end < 0 ? bytes.length : end) - offset
if (buffered.length + length > frameLimit) return fail()
buffered = Buffer.concat([buffered, bytes.subarray(offset, offset + length)])
if (end < 0) break
const value: unknown = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffered))
if (!object(value)) return fail()
yield value
buffered = Buffer.alloc(0)
offset = end + 1
}
}
if (buffered.length) return fail()
}
/** Host obtains a consumed grant before sending native bytes. Only its
* authenticated peer can submit the subsequent complete response projection. */
export async function serveChrParentNativeBoundary(socket: Duplex, hooks: Hooks, assertCurrent: () => Promise<void>) {
let sequence = 0
let pending: ChrWriteIntent | undefined
socket.on("error", () => {})
for await (const value of frames(socket)) {
await assertCurrent()
const { side, method, path } = value
if (value.sequence !== ++sequence || sequence > 100 || side !== "A" && side !== "B" ||
!["PUT", "PATCH", "POST", "DELETE"].includes(String(method)) || typeof path !== "string" || path.length > 128) return fail()
if (value.op === "consume") {
if (pending || !shape(value, ["sequence", "op", "side", "method", "path", "body"]) || !object(value.body) ||
!Object.values(value.body).every(item => typeof item === "string")) return fail()
pending = await hooks.authorizeWrite({ side, method: String(method), path, body: value.body as Record<string, string> })
if (pending.side !== side || pending.method !== method || pending.path !== path) return fail()
} else if (value.op === "ack") {
const { status, id } = value
if (!pending || pending.side !== side || pending.method !== method || pending.path !== path ||
!shape(value, ["sequence", "op", "side", "method", "path", "status", ...(Object.hasOwn(value, "id") ? ["id"] : [])]) ||
typeof status !== "number" || !Number.isInteger(status) || status < 100 || status > 599) return fail()
const needsId = (method === "PUT" && (status === 200 || status === 201)) ||
(pending.step === "target:" + side + ":enable" && status === 200)
if (needsId ? typeof id !== "string" || !nativeId.test(id) : Object.hasOwn(value, "id")) return fail()
hooks.recordWriteAcknowledgement({ intent: pending, status, body: needsId ? { ".id": id } : undefined })
pending = undefined
} else return fail()
await assertCurrent()
await new Promise<void>((resolve, reject) => {
socket.write(JSON.stringify({ sequence, ok: true }) + "\n", error => error ? reject(error) : resolve())
})
}
return fail()
}
@@ -0,0 +1,8 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { runChrParentSynthetic } from "./traffic-flow-chr-parent-synthetic.js"
for (const mode of ["direct", "staged", "wrong-ack"] as const) test("paired catalogue and native ACK parent profile: " + mode, async () => {
const result = await runChrParentSynthetic(mode)
assert.equal(result.writes, mode === "direct" ? 48 : mode === "staged" ? 50 : 25)
})
@@ -0,0 +1,131 @@
import type { MikrotikClient } from "../services/mikrotik.js"
import { createChrParentAckMailbox, createChrParentTrustedStages } from "./traffic-flow-chr-parent-stages.js"
import { createChrTransitTopologyInventory } from "./traffic-flow-chr-topology-inventory.js"
import { createChrParentWriteAdmission, type ChrWriteIntent } from "./traffic-flow-chr-write-admission.js"
import { serveChrParentAdmissionPipe } from "./traffic-flow-chr-write-admission-ipc.js"
import { chrFixtureDeadlineMaxMs, type ChrStagedFixtureLease } from "./traffic-flow-chr-target-timing-profile.js"
type StagesOptions = Parameters<typeof createChrParentTrustedStages>[0]
type Side = "A" | "B"
type Row = Record<string, unknown>
type Options = Pick<StagesOptions, "scope" | "linux" | "deadlineAt" | "authorize" | "timerProfile" | "stagedTarget"> & {
leaseProfile?: ChrStagedFixtureLease
routers: Record<Side, { deviceId: string; endpoint: string; client: Pick<MikrotikClient, "get"> }>
assertTimingPreflight(): Promise<void>
assertRouterIdentity(side: Side, deviceId: string): Promise<void>
assertTransitAndCollectorReady(): Promise<void>
assertTransitAndCollectorStagingReady?(): Promise<void>
assertCaptureAndDrainComplete(): Promise<void>
assertLinuxInverseReady(): Promise<void>
}
const fail = () => { throw Error("Parent CHR composition refused. Automatic replay forbidden.") }
const isRow = (value: unknown): value is Row => !!value && typeof value === "object" && !Array.isArray(value)
export async function createChrParentOrchestration(options: Options) {
const { deadlineAt, leaseProfile, authorize, assertRouterIdentity } = options
const stagedTarget = options.stagedTarget && { ...options.stagedTarget }
const stageReady = options.assertTransitAndCollectorStagingReady
if ((stagedTarget && !stageReady) || (leaseProfile && !stagedTarget)) return fail()
const routers = { A: { ...options.routers.A }, B: { ...options.routers.B } }
const sides = ["A", "B"] as const
const check = () => {
const left = deadlineAt - Date.now()
if (!Number.isSafeInteger(deadlineAt) || left <= 0 || left > chrFixtureDeadlineMaxMs(leaseProfile)) return fail()
return Math.min(8000, left)
}
const assertCurrent = async (side: Side, deviceId: string) => {
check()
if (routers[side].deviceId !== deviceId) return fail()
await authorize()
await assertRouterIdentity(side, deviceId)
check()
}
const assertPair = async () => {
for (const side of sides) await assertCurrent(side, routers[side].deviceId)
}
const assertStageAuthority = async (intent: Readonly<ChrWriteIntent>) => {
await assertPair()
if (intent.step.startsWith("topology:delete:")) {
await options.assertLinuxInverseReady()
} else if (intent.step.endsWith(".restore") || intent.step.startsWith("target:") && intent.method === "DELETE") {
await options.assertCaptureAndDrainComplete()
} else if (stageReady && stagedTarget && (intent.step.endsWith(".enable") || intent.step.startsWith("target:") && intent.method === "PUT")) {
await stageReady()
} else if (!intent.step.startsWith("topology:create:")) {
await options.assertTransitAndCollectorReady()
}
check()
}
const exactGet = async (side: Side, path: string, singleton = false) => {
const { client } = routers[side]
const fence = async () => { check(); await authorize(); check() }
await fence()
const timeout = check()
const raw = await client.get<unknown>(path, timeout, AbortSignal.timeout(timeout), 65536, fence)
await fence()
const rows = Array.isArray(raw) ? raw : singleton && isRow(raw) ? [raw] : undefined
if (!rows || Buffer.byteLength(JSON.stringify(rows)) > 65536 || !rows.every(isRow)) return fail()
return rows
}
const readFlow = async (side: Side) => {
const deviceId = routers[side].deviceId
await assertCurrent(side, deviceId)
const [flow, ipfix, targets] = await Promise.all([
exactGet(side, "/ip/traffic-flow", true), exactGet(side, "/ip/traffic-flow/ipfix", true),
exactGet(side, "/ip/traffic-flow/target"),
])
await assertCurrent(side, deviceId)
if (flow.length !== 1 || ipfix.length !== 1 || targets.length > 1) return fail()
return { deviceId, flow: flow[0], ipfix: ipfix[0], targets }
}
check()
await assertPair()
await options.assertTimingPreflight()
check()
const scope = structuredClone(options.scope)
const inventory = createChrTransitTopologyInventory({
runId: scope[0], deadlineAt, authorize: assertPair, authorizeRequest: authorize, parallelism: 2, routers,
})
const mailbox = createChrParentAckMailbox()
const trusted = await createChrParentTrustedStages({
scope, linux: options.linux, deadlineAt, timerProfile: options.timerProfile, stagedTarget, authorize: assertPair,
routers: {
A: { ...routers.A, read: () => inventory.read("A"), readFlow: () => readFlow("A") },
B: { ...routers.B, read: () => inventory.read("B"), readFlow: () => readFlow("B") },
},
assertOutsideScope: inventory.assertOutsideScope, assertStageAuthority, takeAcknowledgement: mailbox.take,
})
const gate = createChrParentWriteAdmission({
deadlineAt, profile: trusted.profile, deviceIds: { A: routers.A.deviceId, B: routers.B.deviceId }, assertCurrent, assertIntentCurrent: assertStageAuthority,
})
let started = false, retired = false
let activeInput: Parameters<typeof serveChrParentAdmissionPipe>[0]["input"] | undefined
const retire = () => {
retired = true
gate.retire()
mailbox.retire()
trusted.retire()
activeInput?.destroy()
}
return Object.freeze({
authorizeWrite: gate.authorizeWrite,
recordWriteAcknowledgement: mailbox.record,
retire,
async serve(pipes: Parameters<typeof serveChrParentAdmissionPipe>[0]) {
if (started || retired) return fail()
started = true
activeInput = pipes.input
try {
const result = await serveChrParentAdmissionPipe(pipes, gate, trusted.stages, deadlineAt, trusted.profile, leaseProfile)
if (result.completed !== trusted.stages.length || gate.consumed !== result.completed) return fail()
return result
} finally { retire() }
},
})
}
@@ -0,0 +1,121 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { chrTransitNames } from "./traffic-flow-chr-transit-fixture.js"
const runId = "a012bcde"
const ports = { linuxA: 50101, edgeA: 50102, middleA: 50103, middleB: 50104, edgeB: 50105, linuxB: 50106,
collector: 50107, calibrationSource: 50112, calibrationDestination: 50113, phase1Source: 50108, phase1Destination: 50109, phase2Source: 50110, phase2Destination: 50111 }
const directions = ["A-to-B", "B-to-A", "export-A", "export-B", "underlay-linuxA-to-A", "underlay-A-to-linuxA",
"underlay-A-to-B", "underlay-B-to-A", "underlay-B-to-linuxB", "underlay-linuxB-to-B"] as const
const isolation = { checkedPorts: Object.values(ports), checkedNames: Object.values(chrTransitNames(runId)),
checkedCidrs: ["198.18.240.1/32", "198.18.240.2/32", "198.18.241.0/30", "198.18.241.4/30", "198.18.241.8/30", "198.18.242.0/29"],
occupiedPorts: [], overlappingCidrs: [], occupiedNames: [],
policies: directions.map(direction => ({ direction, verdict: "allow", nat: "none", fastTrack: "none", ambiguousRules: 0 } as const)) }
import { createChrParentAckMailbox, createChrParentTrustedStages } from "./traffic-flow-chr-parent-stages.js"
import type { ChrWriteIntent } from "./traffic-flow-chr-write-admission.js"
const menus = { "/interface/wireguard": "wireguard", "/interface/wireguard/peers": "peer", "/ip/address": "address", "/ip/route": "route" } as const
async function fixture(fault?: "foreign" | "ack" | "singleton" | "target" | "authority") {
const makeRows = () => ({ wireguard: [] as Row[], peer: [] as Row[], address: [] as Row[], route: [] as Row[] })
const ipfix = Object.fromEntries(["bytes", "packets", "src-address", "dst-address", "protocol", "src-port", "dst-port", "in-interface", "out-interface"].map(key => [key, "no"]))
const rows = { A: makeRows(), B: makeRows() }
const makeFlow = (side: "A" | "B") => ({
deviceId: side, flow: { ...(side === "B" ? { ".id": "*F" } : {}), enabled: "no", interfaces: "all",
"inactive-flow-timeout": "15s" }, ipfix: { ...ipfix }, targets: [] as Row[],
})
const flow = { A: makeFlow("A"), B: makeFlow("B") }
function router(side: "A" | "B", endpoint: string) {
return { deviceId: side, endpoint, read: async () => ({ deviceId: side, complete: true as const, rows: structuredClone(rows[side]) }),
readFlow: async () => structuredClone(flow[side]) }
}
let count = 0
const writes: ChrWriteIntent[] = []
const result = await createChrParentTrustedStages({
scope: [runId, ports, isolation], deadlineAt: 100000, now: () => 10000,
timerProfile: { kind: "bounded-ipfix-timeout-20s", captureDeadlineAt: 99000, now: () => 10000 },
routers: { A: router("A", "192.0.2.1"), B: router("B", "192.0.2.2") },
linux: { endpoint: "192.0.2.3", publicKeys: { linuxA: "X".repeat(43) + "=", linuxB: "Y".repeat(43) + "=" } },
authorize: async () => {}, assertOutsideScope: async () => {},
async assertStageAuthority(intent) {
if (fault === "authority" && intent.step === "flow:A:ipfix.enable") throw Error("Owner phase refused")
},
async takeAcknowledgement(intent) {
const recorded = writes.at(-1)!
const id = "*" + (++count).toString(16)
const slash = recorded.path.lastIndexOf("/")
const menuPath = recorded.method === "PUT" ? recorded.path : recorded.path.slice(0, slash)
const menu = menus[menuPath as keyof typeof menus]
if (menu) {
if (recorded.method === "PUT") {
rows[recorded.side][menu].push({ ...recorded.body, ".id": id, ...(menu === "wireguard" ? { "public-key": String.fromCharCode(64 + count).repeat(43) + "=" } : {}) })
if (fault === "foreign") rows.B.address.push({ ".id": "*FF" })
} else rows[recorded.side][menu] = rows[recorded.side][menu].filter(row => row[".id"] !== decodeURIComponent(recorded.path.slice(slash + 1)))
} else if (menuPath === "/ip/traffic-flow/target") {
if (recorded.method === "PUT") {
flow[recorded.side].targets.push({ ".id": id, disabled: false, ...recorded.body,
...(fault === "target" ? { "v9-template-timeout": "21s" } : {}) })
} else flow[recorded.side].targets = []
} else {
Object.assign(menuPath.endsWith("/ipfix") ? flow[recorded.side].ipfix : flow[recorded.side].flow, recorded.body)
if (fault === "singleton") flow[recorded.side].flow.interfaces = "foreign"
}
return { intent: { ...intent, ...(fault === "ack" ? { side: "B" } : {}) },
status: recorded.method === "PUT" ? 201 : 204, body: recorded.method === "PUT" ? { ".id": id } : undefined }
},
})
return { ...result, rows, flow, writes, async step(index: number) {
const stage = result.stages[index]
const intent = await stage.intent()
writes.push(intent)
await stage.verify(intent)
} }
}
test("parent resolves exact 48 stages from real proof owners and restores both baselines", async () => {
const f = await fixture()
assert.equal(f.stages.length, 48)
for (let i = 0; i < f.stages.length; i++) await f.step(i)
assert(f.writes.some(write => write.step === "flow:B:flow.enable" && write.method === "PATCH" && write.path === "/ip/traffic-flow/*F"))
assert(f.writes.some(write => write.step === "flow:A:flow.enable" && write.method === "POST" && write.path === "/ip/traffic-flow/set"))
assert(f.writes.some(write => write.step === "target:A:create" && write.body["v9-template-timeout"] === "20s"))
assert.deepEqual(Object.values(f.rows).flatMap(side => Object.values(side).flat()), [])
assert.equal(f.flow.B.flow.enabled, "no")
})
test("parent refuses ACK identity, peer drift, singleton drift and wrong target timer without releasing next stage", async () => {
for (const [fault, failedAt] of [["ack", 0], ["foreign", 0], ["singleton", 18], ["target", 20]] as const) {
const f = await fixture(fault)
for (let i = 0; i < failedAt; i++) await f.step(i)
await assert.rejects(f.step(failedAt))
await assert.rejects(f.stages[failedAt + 1].intent())
await assert.rejects(f.stages[failedAt].intent())
assert.equal(f.writes.length, failedAt + 1)
}
})
test("outer relay ACK mailbox allows one exact take and latches missing or duplicate data", async () => {
const f = await fixture()
const intent = await f.stages[0].intent()
const ack = { intent, status: 201, body: { ".id": "*A" } }
f.retire()
const mailbox = createChrParentAckMailbox()
mailbox.record(ack)
assert.deepEqual(await mailbox.take(intent), ack)
await assert.rejects(mailbox.take(intent))
assert.throws(() => mailbox.record(ack))
const duplicate = createChrParentAckMailbox()
duplicate.record(ack)
assert.throws(() => duplicate.record(ack))
await assert.rejects(duplicate.take(intent))
})
test("parent readiness refusal blocks the export phase before any singleton or target grant", async () => {
const f = await fixture("authority")
for (let i = 0; i < 18; i++) await f.step(i)
await assert.rejects(f.stages[18].intent())
await assert.rejects(f.stages[19].intent())
assert.equal(f.writes.length, 18)
})
@@ -0,0 +1,280 @@
import { isDeepStrictEqual } from "node:util"
import { createDisabledChrFlowHarness } from "./traffic-flow-chr-disabled-baseline.js"
import { assertChrTargetTimerWindow, createChrTargetHarness, type ChrTargetTimerProfile, type ChrStagedTargetTimerProfile } from "./traffic-flow-chr-target-harness.js"
import { createChrTransitTopologyOwner } from "./traffic-flow-chr-topology-owner.js"
import { planChrTransitFixture } from "./traffic-flow-chr-transit-fixture.js"
import { chrWriteScheduleLimit, type ChrWriteAdmission, type ChrWriteIntent } from "./traffic-flow-chr-write-admission.js"
import type { serveChrParentAdmissionPipe } from "./traffic-flow-chr-write-admission-ipc.js"
type Topology = Parameters<typeof createChrTransitTopologyOwner>[0]
type Flow = Parameters<typeof createDisabledChrFlowHarness>[0]
type Side = "A" | "B"
type Stage = Parameters<typeof serveChrParentAdmissionPipe>[2][number]
export type ChrParentNativeAck = { intent: ChrWriteIntent; status: number; body: unknown }
type Options = Pick<Topology, "scope" | "linux" | "deadlineAt" | "authorize" | "assertOutsideScope" | "now"> & {
routers: Record<Side, Omit<Topology["routers"][Side], "native"> & { readFlow: Flow["read"] }>
timerProfile?: ChrTargetTimerProfile
stagedTarget?: { captureTimer(): Promise<ChrStagedTargetTimerProfile> }
/** Assert the parent-owned phase permits this stage (for example, isolated transit ready before export). */
assertStageAuthority(intent: Readonly<ChrWriteIntent>): Promise<void>
takeAcknowledgement(intent: Readonly<ChrWriteIntent>): Promise<ChrParentNativeAck>
}
const roles = ["edgeA", "middleA", "middleB", "edgeB"] as const
const sides = ["A", "B"] as const
const exportFields = ["bytes", "packets", "src-address", "dst-address", "protocol", "src-port", "dst-port", "in-interface", "out-interface"]
const disabled = (value: unknown) => value === false || value === "false" || value === "no"
const refuse = (): never => { throw Error("Parent native stage refused. Automatic replay forbidden.") }
const require = (value: unknown) => { if (!value) refuse() }
function deferred<T>() {
let resolve!: (value: T) => void, reject!: (error: unknown) => void
const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no })
void promise.catch(() => {})
return { promise, resolve, reject }
}
/** The parent executes the same bounded proof owners using independent native reads.
* Its synthetic transport cannot send a write and can only consume the outer relay ACK. */
export async function createChrParentTrustedStages(options: Options) {
const scope = structuredClone(options.scope)
const plan = planChrTransitFixture(...scope)
const routers = { A: { ...options.routers.A }, B: { ...options.routers.B } }
const { deadlineAt, now = Date.now, authorize, takeAcknowledgement, assertOutsideScope, assertStageAuthority } = options
const timerProfile = options.timerProfile && { ...options.timerProfile }
const stagedTarget = options.stagedTarget && { ...options.stagedTarget }
require(!stagedTarget || !timerProfile)
const profile = stagedTarget ? "staged-disabled-target" : "direct-target"
let failed = false, cursor = 0
const live = () => require(!failed && Number.isSafeInteger(deadlineAt) && now() < deadlineAt)
const readFlow = async (side: Side) => {
live()
await authorize()
const value = structuredClone(await routers[side].readFlow())
await authorize()
live()
require(value.deviceId === routers[side].deviceId && value.flow && value.ipfix &&
Array.isArray(value.targets) && value.targets.length <= 1 && Buffer.byteLength(JSON.stringify(value)) <= 65536)
return value
}
const baseline = { A: await readFlow("A"), B: await readFlow("B") }
if (timerProfile) {
require(timerProfile.captureDeadlineAt <= deadlineAt)
for (const side of sides) assertChrTargetTimerWindow(timerProfile, baseline[side].flow["inactive-flow-timeout"])
}
const expected = structuredClone(baseline)
const hasIpfixChange = (side: Side) => exportFields.some(field => disabled(baseline[side].ipfix[field]))
const keys = [...roles, ...roles.map(role => "address:" + role), ...roles.map(role => "peer:" + role),
...plan.routes.filter(route => route.node === "A" || route.node === "B").map((route, i) => [route.node, "route:" + i].join(":"))]
const topologyStep = (action: string, key: string) => {
const side = key.startsWith("A:route:") || key.startsWith("B:route:") ? key[0] : key.at(-1)
return "topology:" + action + ":" + side + ":" + key.replace(/^[AB]:(?=route:)/, "")
}
const labels = keys.map(key => topologyStep("create", key))
for (const side of sides) {
if (hasIpfixChange(side)) labels.push("flow:" + side + ":ipfix.enable")
labels.push("flow:" + side + ":flow.enable", "target:" + side + ":create")
}
if (stagedTarget) for (const side of sides) labels.push("target:" + side + ":enable")
for (const side of [...sides].reverse()) {
labels.push("target:" + side + ":delete", "flow:" + side + ":flow.restore")
if (hasIpfixChange(side)) labels.push("flow:" + side + ":ipfix.restore")
}
labels.push(...[...keys].reverse().map(key => topologyStep("delete", key)))
require(keys.length === 18 && labels.length <= chrWriteScheduleLimit(profile) && new Set(labels).size === labels.length)
type Slot = {
intent: ChrWriteIntent
acknowledgement: ReturnType<typeof deferred<unknown>>
verified: ReturnType<typeof deferred<void>>
requested: boolean
supplied: boolean
}
const ready = labels.map(() => deferred<Slot>())
const requested = labels.map(() => deferred<void>())
let slot: Slot | undefined
let timer: ReturnType<typeof setTimeout> | undefined
let launched = false
const abort = (error: unknown) => {
failed = true
clearTimeout(timer)
slot?.acknowledgement.reject(error)
slot?.verified.reject(error)
for (const entry of ready) entry.reject(error)
for (const entry of requested) entry.reject(error)
}
const expectFlow = async (side: Side) => require(isDeepStrictEqual(await readFlow(side), expected[side]))
const otherSide = (side: Side) => side === "A" ? "B" : "A"
const conserveFlow = async (intent: ChrWriteIntent) => {
await expectFlow(otherSide(intent.side))
if (intent.step.startsWith("topology:")) await expectFlow(intent.side)
}
const broker: ChrWriteAdmission = {
async run<T>(intent: ChrWriteIntent, send: (arm: () => Promise<void>) => Promise<T>): Promise<T> {
live()
require(!slot && labels[cursor] === intent.step)
await requested[cursor].promise
live()
slot = { intent: structuredClone(intent), acknowledgement: deferred(), verified: deferred(), requested: false, supplied: false }
return send(async () => {
live()
require(slot?.intent.step === intent.step)
await assertStageAuthority(structuredClone(intent))
await conserveFlow(intent)
if (!intent.step.startsWith("topology:")) await topology.assertCurrent()
live()
ready[cursor].resolve(slot!)
})
},
}
const nativeFor = () => {
const simulate = async <T>(method: ChrWriteIntent["method"], path: string, body: unknown, before?: () => Promise<void>) => {
const entry = slot
require(entry && before && isDeepStrictEqual({ method, path, body }, {
method: entry?.intent.method, path: entry?.intent.path, body: entry?.intent.body,
}))
await before!()
return await entry!.acknowledgement.promise as T
}
return {
put: <T>(path: string, body: unknown, _timeout?: number, before?: () => Promise<void>) => simulate<T>("PUT", path, body, before),
patch: <T>(path: string, body: unknown, _timeout?: number, before?: () => Promise<void>) => simulate<T>("PATCH", path, body, before),
post: <T>(path: string, body: unknown, _timeout?: number, _headers?: unknown, before?: () => Promise<void>) => simulate<T>("POST", path, body, before),
delete: async (path: string, _timeout?: number, before?: () => Promise<void>) => { await simulate("DELETE", path, {}, before) },
}
}
const checkpoint = async (entry: { phase: string }) => {
if (entry.phase !== "verified") return
const verified = slot
require(verified?.supplied)
const intent = verified!.intent
await conserveFlow(intent)
if (!intent.step.startsWith("topology:")) {
await topology.assertCurrent()
// The harness already performed exact local readback. See checkedRead below.
require(nextFlow && nextFlow.side === intent.side)
expected[intent.side] = nextFlow!.value
nextFlow = undefined
}
slot = undefined
cursor++
verified!.verified.resolve()
}
let nextFlow: { side: Side; value: Flow["baseline"] } | undefined
const checkedRead = async (side: Side) => {
const value = await readFlow(side)
if (slot?.supplied) nextFlow = { side, value }
return value
}
const native = nativeFor()
const topology = createChrTransitTopologyOwner({
scope, linux: options.linux, deadlineAt, now, authorize, assertOutsideScope,
routers: { A: { ...routers.A, native }, B: { ...routers.B, native } },
writeAdmission: broker, checkpoint,
assertFlowRestored: async () => { await expectFlow("A"); await expectFlow("B"); require(isDeepStrictEqual(expected, baseline)) },
})
const flows = Object.fromEntries(sides.map(side => [side, createDisabledChrFlowHarness({
baseline: baseline[side], transitInterface: "nf" + plan.runId + (side === "A" ? "ea" : "eb"),
authorize, read: () => checkedRead(side), native, checkpoint, relayWrite: { side, admission: broker },
})])) as Record<Side, ReturnType<typeof createDisabledChrFlowHarness>>
const workflow = async () => {
await topology.acquire()
const targets: Partial<Record<Side, ReturnType<typeof createChrTargetHarness>>> = {}
for (const side of sides) {
const active = await flows[side].enable()
const targetOptions = {
baseline: active, source: side === "A" ? plan.address.exportA : plan.address.exportB,
destination: plan.address.collector, port: plan.ports.collector,
authorize, read: () => checkedRead(side), native, checkpoint, relayWrite: { side, admission: broker },
}
const target = stagedTarget
? createChrTargetHarness({ ...targetOptions, stagedDisabled: true })
: createChrTargetHarness({ ...targetOptions, timerProfile })
await target.create()
targets[side] = target
}
if (stagedTarget) for (const side of sides) {
await requested[cursor].promise
live()
const timer = await stagedTarget.captureTimer()
require(timer.kind === "staged-ipfix-timeout-20s" && timer.captureDeadlineAt <= deadlineAt)
await targets[side]!.enable(timer)
}
for (const side of [...sides].reverse()) {
await targets[side]!.remove()
await flows[side].restore()
}
await topology.restore()
await expectFlow("A")
await expectFlow("B")
require(cursor === labels.length && isDeepStrictEqual(expected, baseline))
}
let completed: Promise<void> | undefined
const retire = () => abort(Error("Parent native stage retired"))
const stages: Stage[] = labels.map((step, index) => Object.freeze({
step,
async intent() {
try {
live()
require(cursor === index)
if (!launched) {
launched = true
timer = setTimeout(retire, Math.max(1, deadlineAt - now()))
completed = workflow()
void completed.then(() => clearTimeout(timer), abort)
}
requested[index].resolve()
const entry = await ready[index].promise
live()
require(slot === entry && !entry.requested)
entry.requested = true
return structuredClone(entry.intent)
} catch (error) { abort(error); throw error }
},
async verify(intent: Readonly<ChrWriteIntent>) {
try {
live()
const entry = slot
require(cursor === index && entry?.requested && !entry.supplied && isDeepStrictEqual(entry.intent, intent))
const ack = await takeAcknowledgement(intent)
live()
require(ack && isDeepStrictEqual(ack.intent, intent) &&
(intent.method === "PUT" ? [200, 201].includes(ack.status) :
intent.step === "target:" + intent.side + ":enable" ? ack.status === 200 : [200, 204].includes(ack.status)))
entry!.supplied = true
entry!.acknowledgement.resolve(structuredClone(ack.body))
await entry!.verified.promise
if (index === labels.length - 1) await completed
live()
} catch (error) { abort(error); throw error }
},
}))
return Object.freeze({ profile, stages: Object.freeze(stages), retire })
}
/** Connect directly to the owned parent relay. Each response is disposable and
* usable once. Only exact consumed gate intent and projected ACK travel here. */
export function createChrParentAckMailbox() {
let pending: ChrParentNativeAck | undefined, retired = false
return Object.freeze({
record(ack: ChrParentNativeAck) {
if (retired || pending) { retired = true; pending = undefined; return refuse() }
pending = structuredClone(ack)
},
async take(intent: Readonly<ChrWriteIntent>) {
const ack = pending
pending = undefined
if (retired || !ack || !isDeepStrictEqual(ack.intent, intent)) { retired = true; return refuse() }
return ack
},
retire() { retired = true; pending = undefined },
})
}
@@ -0,0 +1,237 @@
import assert from "node:assert/strict"
import { Duplex, PassThrough } from "node:stream"
import { serveChrParentNativeBoundary } from "./traffic-flow-chr-parent-native-boundary.js"
import { chrTransitNames } from "./traffic-flow-chr-transit-fixture.js"
const runId = "a012bcde"
const ports = { linuxA: 50101, edgeA: 50102, middleA: 50103, middleB: 50104, edgeB: 50105, linuxB: 50106,
collector: 50107, calibrationSource: 50112, calibrationDestination: 50113, phase1Source: 50108, phase1Destination: 50109, phase2Source: 50110, phase2Destination: 50111 }
const directions = ["A-to-B", "B-to-A", "export-A", "export-B", "underlay-linuxA-to-A", "underlay-A-to-linuxA",
"underlay-A-to-B", "underlay-B-to-A", "underlay-B-to-linuxB", "underlay-linuxB-to-B"] as const
const isolation = { checkedPorts: Object.values(ports), checkedNames: Object.values(chrTransitNames(runId)),
checkedCidrs: ["198.18.240.1/32", "198.18.240.2/32", "198.18.241.0/30", "198.18.241.4/30", "198.18.241.8/30", "198.18.242.0/29"],
occupiedPorts: [], overlappingCidrs: [], occupiedNames: [],
policies: directions.map(direction => ({ direction, verdict: "allow", nat: "none", fastTrack: "none", ambiguousRules: 0 } as const)) }
import { createChrParentOrchestration } from "./traffic-flow-chr-parent-orchestration.js"
import { createChrChildAdmissionPipe } from "./traffic-flow-chr-write-admission-ipc.js"
import { parseChrVrfPrefix } from "./chr-vrf-companion-address-space.js"
import type { ChrWriteIntent } from "./traffic-flow-chr-write-admission.js"
const admittedMenus = ["/interface/wireguard", "/ip/address", "/interface/wireguard/peers", "/ip/route", "/ip/traffic-flow/target"]
const roles = ["edgeA", "middleA", "middleB", "edgeB"]
const roleSuffix: Record<string, string> = { edgeA: "ea", middleA: "ma", middleB: "mb", edgeB: "eb" }
const names = Object.fromEntries(roles.map(role => [role, "nf" + runId + roleSuffix[role]]))
const marker = "netflow-" + runId
const exportFields = ["bytes", "packets", "src-address", "dst-address", "protocol", "src-port", "dst-port", "in-interface", "out-interface"]
const singleton = "/ip/traffic-flow", ipfix = singleton + "/ipfix", target = singleton + "/target"
type Row = Record<string, unknown>
type Side = "A" | "B"
function state(side: Side) {
return {
[singleton]: [{ ...(side === "B" ? { ".id": "*F" } : {}), enabled: "no", interfaces: "all", "inactive-flow-timeout": "15s" }],
[ipfix]: [Object.fromEntries(exportFields.map(key => [key, "no"]))],
"/interface": [{ ".id": "*F0", name: "ether1", type: "ether" }],
} as Record<string, Row[]>
}
export async function runChrParentSynthetic(mode: "direct" | "staged" | "wrong-ack", density: "empty" | "dense" = "empty") {
const staged = mode !== "direct"
const phaseCalls: string[] = []
const rows = { A: state("A"), B: state("B") }, baseline = structuredClone(rows)
const stressPaths = new Set(["/ip/firewall/filter", "/ip/firewall/nat", "/ip/firewall/raw", "/ip/firewall/mangle",
"/ipv6/firewall/filter", "/ipv6/firewall/nat", "/ipv6/firewall/raw", "/ipv6/firewall/mangle"])
const denseBody = density === "dense" ? JSON.stringify(Array.from({ length: 700 }, (_, i) => ({
".id": "*" + (i + 4096).toString(16), chain: "forward", action: "accept", disabled: "true",
comment: "synthetic-stress-" + i + "-" + "x".repeat(1150),
}))) : ""
const catalogueFixtureBytesPerSide = Buffer.byteLength(denseBody) * stressPaths.size
const now = Date.now()
let writes = 0, staging = false, ready = false, drained = false, inverse = false
const reads: Record<Side, number> = { A: 0, B: 0 }
const makeRouter = (side: Side, endpoint: string) => ({
endpoint, deviceId: side,
client: { async get<T>(path: string, _timeout?: number, _signal?: AbortSignal, _max?: number, before?: () => Promise<unknown>) {
await before?.()
reads[side]++
if (denseBody && stressPaths.has(path)) return JSON.parse(denseBody) as T
return JSON.parse(JSON.stringify(rows[side][path] ?? [])) as T
} },
})
const orchestration = await createChrParentOrchestration({
scope: [runId, ports, isolation], deadlineAt: now + (density === "dense" ? 580000 : 100000),
linux: { endpoint: "192.0.2.3", publicKeys: { linuxA: "X".repeat(43) + "=", linuxB: "Y".repeat(43) + "=" } },
routers: { A: makeRouter("A", "192.0.2.1"), B: makeRouter("B", "192.0.2.2") },
...(staged ? { stagedTarget: { async captureTimer() {
assert(ready)
return { kind: "staged-ipfix-timeout-20s" as const, captureDeadlineAt: Math.min(Date.now() + 180000, now + (density === "dense" ? 580000 : 100000)) }
} } } : { timerProfile: { kind: "bounded-ipfix-timeout-20s" as const, captureDeadlineAt: now + 89000 } }),
assertTransitAndCollectorStagingReady: async () => { phaseCalls.push("staging"); assert(staging && !ready) },
authorize: async () => {}, assertTimingPreflight: async () => { phaseCalls.push("preflight") }, assertRouterIdentity: async (side, id) => assert.equal(side, id),
assertTransitAndCollectorReady: async () => { phaseCalls.push("ready"); assert(ready) },
assertCaptureAndDrainComplete: async () => { phaseCalls.push("drained"); assert(drained) },
assertLinuxInverseReady: async () => { phaseCalls.push("inverse"); assert(inverse) },
})
const toParent = new PassThrough(), toChild = new PassThrough()
const done = orchestration.serve({ input: toParent, output: toChild })
void done.catch(() => {})
const child = createChrChildAdmissionPipe({ input: toChild, output: toParent }, staged ? "staged-disabled-target" : "direct-target")
const nativeInput = new PassThrough(), nativeOutput = new PassThrough()
const nativeStream = Duplex.from({ readable: nativeInput, writable: nativeOutput })
const physicalBoundary = serveChrParentNativeBoundary(nativeStream, orchestration, async () => {})
void physicalBoundary.catch(() => { nativeStream.destroy(); nativeOutput.destroy() })
let nativeSequence = 0
const physicalExchange = async (data: object) => {
const sequence = ++nativeSequence
await new Promise<void>((resolve, reject) => {
const receive = (reply: Buffer) => {
nativeOutput.off("close", closed)
try { assert.deepEqual(JSON.parse(reply.toString()), { sequence, ok: true }); resolve() }
catch (error) { reject(error) }
}
const closed = () => { nativeOutput.off("data", receive); reject(Error("Synthetic native boundary closed")) }
nativeOutput.once("data", receive)
nativeOutput.once("close", closed)
nativeInput.write(JSON.stringify({ sequence, ...data }) + "\n")
})
}
const owned = new Map<string, { intent: ChrWriteIntent; id: string }>()
const execute = async (intent: ChrWriteIntent) => {
await child.run(intent, async arm => {
await arm()
const projection = { side: intent.side, method: intent.method, path: intent.path }
await physicalExchange({ op: "consume", ...projection, body: intent.body })
const catalogue = rows[intent.side]
const id = "*" + (++writes).toString(16)
const slash = intent.path.lastIndexOf("/")
const menu = intent.method === "PUT" ? intent.path : intent.path.slice(0, slash)
const list = catalogue[menu] ??= []
let body: unknown
if (intent.method === "PUT") {
assert(admittedMenus.includes(menu))
const row: Row = { ...intent.body, ".id": id }
if (menu === "/interface/wireguard") {
row["public-key"] = String.fromCharCode(64 + writes).repeat(43) + "="
catalogue["/interface"].push({ ".id": id, name: row.name, type: "wireguard", comment: marker, disabled: false })
}
if (menu === "/ip/address") {
row.dynamic = false
row.vrf = "main"
const prefix = parseChrVrfPrefix(row.address, 4)!
const ip = [24n, 16n, 8n, 0n].map(shift => Number((prefix.first >> shift) & 255n)).join(".")
const route = { ".id": "*a" + id.slice(1), "dst-address": ip + "/" + prefix.prefixLength, gateway: row.interface,
"routing-table": "main", connect: true, distance: "0", scope: "10", "target-scope": "5" }
;(catalogue["/ip/route"] ??= []).push({ ...route, dynamic: true })
;(catalogue["/routing/route"] ??= []).push({ ...route, afi: "ip4", "belongs-to": "connected",
"local-address": String(row.address).split("/")[0] + "%" + row.interface })
}
if (menu === target) row.disabled = staged ? "yes" : false
list.push(row)
owned.set(intent.step, { intent, id })
body = { ".id": id }
} else if (intent.method === "DELETE") {
const previous = decodeURIComponent(intent.path.slice(slash + 1))
assert.equal(list.filter(row => row[".id"] === previous).length, 1)
catalogue[menu] = list.filter(row => row[".id"] !== previous)
if (menu === "/interface/wireguard") catalogue["/interface"] = catalogue["/interface"].filter(row => row[".id"] !== previous)
if (menu === "/ip/address") for (const path of ["/ip/route", "/routing/route"]) {
catalogue[path] = catalogue[path].filter(row => row[".id"] !== "*a" + previous.slice(1))
}
} else if (menu === target) {
const previous = decodeURIComponent(intent.path.slice(slash + 1))
const matching = list.filter(row => row[".id"] === previous)
assert.equal(matching.length, 1)
assert.equal(matching[0].disabled, "yes")
Object.assign(matching[0], intent.body)
body = { ".id": mode === "wrong-ack" ? "*FFFF" : previous }
} else Object.assign(list[0], intent.body)
await physicalExchange({ op: "ack", ...projection, status: intent.method === "PUT" ? 201 : body ? 200 : 204,
...(body && typeof body === "object" ? { id: (body as Row)[".id"] } : {}) })
})
}
const send = (step: string, side: Side, method: ChrWriteIntent["method"], path: string, body: ChrWriteIntent["body"]) =>
execute({ step, side, deviceId: side, method, path, body })
const create = (key: string, side: Side, path: string, body: Record<string, string>) =>
send("topology:create:" + side + ":" + key, side, "PUT", path, { ...body, disabled: "false", comment: marker })
const addr: Record<string, string> = { edgeA: "198.18.241.2/30", middleA: "198.18.241.5/30", middleB: "198.18.241.6/30", edgeB: "198.18.241.9/30" }
const sideOf = (role: string): Side => role.endsWith("A") ? "A" : "B"
const portOf = (role: string) => String(ports[role as keyof typeof ports])
try {
for (const role of roles) await create(role, sideOf(role), "/interface/wireguard", { name: names[role], "listen-port": portOf(role), mtu: "1420" })
for (const role of roles) await create("address:" + role, sideOf(role), "/ip/address", { interface: names[role], address: addr[role] })
const peerSpecs = [
["edgeA", "linuxA", "192.0.2.3", "X", "198.18.240.1/32,198.18.242.3/32"],
["middleA", "middleB", "192.0.2.2", "C", "198.18.240.2/32"],
["middleB", "middleA", "192.0.2.1", "B", "198.18.240.1/32"],
["edgeB", "linuxB", "192.0.2.3", "Y", "198.18.240.2/32,198.18.242.3/32"],
]
for (const [role, opposite, endpoint, key, allowed] of peerSpecs) await create("peer:" + role, sideOf(role), "/interface/wireguard/peers", {
interface: names[role], "public-key": key.repeat(43) + "=", "allowed-address": allowed,
"endpoint-address": endpoint, "endpoint-port": portOf(opposite), "persistent-keepalive": "10",
})
const routes = [
["edgeA", "198.18.240.1", "198.18.241.1"], ["middleA", "198.18.240.2", "198.18.241.6"], ["edgeA", "198.18.242.3", "198.18.241.1"],
["edgeB", "198.18.240.2", "198.18.241.10"], ["middleB", "198.18.240.1", "198.18.241.5"], ["edgeB", "198.18.242.3", "198.18.241.10"],
]
for (const [i, [role, destination, gateway]] of routes.entries()) await create("route:" + i, sideOf(role), "/ip/route", {
"dst-address": destination + "/32", gateway: gateway + "%" + names[role], "routing-table": "main",
})
staging = staged
ready = !staged
const changeIpfix = (side: Side, enable: boolean) => send("flow:" + side + ":ipfix." + (enable ? "enable" : "restore"),
side, "POST", ipfix + "/set", Object.fromEntries(exportFields.map(key => [key, enable ? "yes" : "no"])))
const changeFlow = (side: Side, enable: boolean) => send("flow:" + side + ":flow." + (enable ? "enable" : "restore"),
side, side === "B" ? "PATCH" : "POST", singleton + (side === "B" ? "/*F" : "/set"),
{ enabled: enable ? "yes" : "no", interfaces: enable ? names["edge" + side] : "all" })
for (const side of ["A", "B"] as const) {
await changeIpfix(side, true)
await changeFlow(side, true)
await send("target:" + side + ":create", side, "PUT", target, { "dst-address": "198.18.242.3",
"src-address": side === "A" ? "198.18.241.2" : "198.18.241.9", port: portOf("collector"), version: "ipfix", "v9-template-timeout": "20s", ...(staged ? { disabled: "yes" } : {}) })
}
if (staged) {
ready = true
for (const side of ["A", "B"] as const) {
const enabling = send("target:" + side + ":enable", side, "PATCH", target + "/" + owned.get("target:" + side + ":create")!.id, { disabled: "no" })
if (mode === "wrong-ack") {
await assert.rejects(enabling)
await assert.rejects(done)
assert.equal(writes, 25)
assert.equal(rows.B[target][0].disabled, "yes")
return { completed: 0, writes, reads, catalogueFixtureBytesPerSide, nativeFrames: nativeSequence }
}
await enabling
}
}
drained = true
for (const side of ["B", "A"] as const) {
await send("target:" + side + ":delete", side, "DELETE", target + "/" + owned.get("target:" + side + ":create")!.id, {})
await changeFlow(side, false)
await changeIpfix(side, false)
}
inverse = true
for (const { intent, id } of [...owned.values()].reverse().filter(value => value.intent.step.startsWith("topology:"))) {
await send(intent.step.replace(":create:", ":delete:"), intent.side, "DELETE", intent.path + "/" + id, {})
}
toParent.end()
assert.deepEqual(await done, { completed: staged ? 50 : 48 })
assert.equal(writes, staged ? 50 : 48)
assert(reads.A > 100 && reads.B > 100)
assert.deepEqual([...new Set(phaseCalls)], ["preflight", ...(staged ? ["staging"] : []), "ready", "drained", "inverse"])
for (const side of ["A", "B"] as const) {
for (const [path, records] of Object.entries(rows[side])) assert.deepEqual(records, baseline[side][path] ?? [])
}
assert.equal(nativeSequence, writes * 2)
return { completed: writes, writes, reads, catalogueFixtureBytesPerSide, nativeFrames: nativeSequence }
} finally {
nativeStream.destroy()
nativeInput.destroy()
nativeOutput.destroy()
toParent.destroy()
toChild.destroy()
orchestration.retire()
}
}
@@ -0,0 +1,72 @@
import assert from "node:assert/strict"
import { createHash } from "node:crypto"
import { test } from "node:test"
import { inspectChrReadiness } from "./traffic-flow-chr-readiness.js"
const now = 1700000005000
function fixture() {
const devices = [1, 2].map(serverId => ({
managementAddress: "198.51.100." + serverId, deviceId: "fixture-" + serverId,
version: "7.24.5 (stable)", exporterSource: "192.0.2." + serverId, serverId,
}))
const pins = { vpsHostKeySha256: "a".repeat(64), collectorInstance: "owned-backend", collectorAddress: "192.0.2.100",
interfaceName: "test0", testPort: 4739, baselinePort: 4740, devices }
const evidence = { observedAt: now - 1, vpsHostKeySha256: pins.vpsHostKeySha256, collectorInstance: pins.collectorInstance,
interfaceName: pins.interfaceName, interfaceAddresses: [pins.collectorAddress], collectorPid: 42,
udpOwners: [{ pid: 42, address: pins.collectorAddress, port: pins.testPort }],
worker: { alive: true, bound: true, address: "192.0.2.100:4739" },
settings: { enabled: true, collectorIp: pins.collectorAddress, flowListenPort: pins.testPort,
listenerBound: true, listenerAddress: "192.0.2.100:4739" }, devices: structuredClone(devices) }
return { pins, evidence }
}
function capture() {
const bytes = Buffer.alloc(24 + 2 * 60)
bytes.writeUInt32LE(0xa1b2c3d4)
bytes.writeUInt16LE(2, 4); bytes.writeUInt16LE(4, 6)
bytes.writeUInt32LE(4124, 16); bytes.writeUInt32LE(101, 20)
for (const [index, at] of [24, 84].entries()) {
bytes.writeUInt32LE(1700000000, at); bytes.writeUInt32LE(44, at + 8); bytes.writeUInt32LE(44, at + 12)
const ip = bytes.subarray(at + 16, at + 60)
ip[0] = 0x45; ip.writeUInt16BE(44, 2); ip[9] = 17
Buffer.from([192, 0, 2, index + 1, 192, 0, 2, 100]).copy(ip, 12)
ip.writeUInt16BE(2055, 20); ip.writeUInt16BE(4740, 22); ip.writeUInt16BE(24, 24)
ip.writeUInt16BE(10, 28); ip.writeUInt16BE(16, 30)
}
return { bytes, sha256: createHash("sha256").update(bytes).digest("hex") }
}
test("pinned identity and owned bind do not fill a missing ingress observation", () => {
const { pins, evidence } = fixture()
const result = inspectChrReadiness(pins, evidence, undefined, now)
assert.equal(result.identityMatched, true)
assert.equal(result.collectorBindMatched, true)
assert.equal(result.ingressStatus, "missing")
assert.equal(result.readyForReview, false)
assert.deepEqual(result.targetScopes.map(row => [row.deviceId, row.fields["src-address"], row.fields.port]), [
["fixture-1", "192.0.2.1", "4739"], ["fixture-2", "192.0.2.2", "4739"],
])
})
test("requires fresh pinned observations, exact listener address and exclusive UDP owner", () => {
for (const mutate of [
(f: ReturnType<typeof fixture>) => { f.evidence.devices[0].version = "different" },
(f: ReturnType<typeof fixture>) => { f.evidence.observedAt = now - 120001 },
(f: ReturnType<typeof fixture>) => { f.evidence.udpOwners[0].pid = 999 },
(f: ReturnType<typeof fixture>) => { f.evidence.udpOwners[0].address = "0.0.0.0" },
(f: ReturnType<typeof fixture>) => { f.evidence.udpOwners.push({ ...f.evidence.udpOwners[0] }) },
(f: ReturnType<typeof fixture>) => { f.evidence.worker.alive = false },
]) {
const f = fixture(); mutate(f)
assert.equal(inspectChrReadiness(f.pins, f.evidence, capture(), now).readyForReview, false)
}
})
test("two-source scoped baseline pcap is checked separately from the owned test port", () => {
const { pins, evidence } = fixture(), input = capture()
const result = inspectChrReadiness(pins, evidence, input, now)
assert.equal(result.ingressStatus, "matched")
assert.equal(result.readyForReview, true)
assert.equal(result.actualNativeAcceptance, false)
assert.equal(inspectChrReadiness(pins, evidence, { ...input, sha256: "0".repeat(64) }, now).ingressStatus, "refused")
assert.equal(inspectChrReadiness(pins, { ...evidence, observedAt: now + 600001 }, input, now + 600001).ingressStatus, "refused")
})
@@ -0,0 +1,104 @@
/** Local preflight report from reviewed VPS/CHR observations and optional pcap. */
import { createHash } from "node:crypto"
import { open } from "node:fs/promises"
import { isIPv4 } from "node:net"
import { resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { isDeepStrictEqual } from "node:util"
import { z } from "zod"
import { projectChrWire } from "./traffic-flow-chr-wire.js"
const ip = z.string().max(15).refine(isIPv4)
const port = z.number().int().min(1).max(65535)
const id = z.string().min(1).max(128)
const sha = z.string().regex(/^[a-f0-9]{64}$/)
const device = z.object({ managementAddress: ip, deviceId: id, version: id, exporterSource: ip, serverId: z.number().int().positive().safe() }).strict()
const pinsSchema = z.object({
vpsHostKeySha256: sha, collectorInstance: id, collectorAddress: ip, interfaceName: z.string().regex(/^[A-Za-z0-9_.-]{1,15}$/),
testPort: port, baselinePort: port, devices: z.tuple([device, device]),
}).strict()
const evidenceSchema = z.object({
observedAt: z.number().int().nonnegative().safe(),
vpsHostKeySha256: sha, collectorInstance: id, interfaceName: z.string().max(15),
interfaceAddresses: z.array(ip).max(32),
collectorPid: z.number().int().positive().max(0x7fffffff),
udpOwners: z.array(z.object({ pid: z.number().int().positive().max(0x7fffffff), address: ip, port }).strict()).max(16),
worker: z.object({ alive: z.boolean(), bound: z.boolean(), address: z.string().max(32).nullable() }).strict(),
settings: z.object({ enabled: z.boolean(), collectorIp: ip, flowListenPort: port, listenerBound: z.boolean(), listenerAddress: z.string().max(32).nullable() }).strict(),
devices: z.tuple([device, device]),
}).strict()
type Ingress = { bytes: Buffer; sha256: string }
export function inspectChrReadiness(rawPins: unknown, rawEvidence: unknown, ingress?: Ingress, now = Date.now()) {
const pins = pinsSchema.parse(rawPins), evidence = evidenceSchema.parse(rawEvidence)
const distinct = new Set([...pins.devices.map(row => row.exporterSource), pins.collectorAddress]).size === 3 &&
new Set(pins.devices.map(row => row.deviceId)).size === 2 && new Set(pins.devices.map(row => row.managementAddress)).size === 2 &&
new Set(pins.devices.map(row => row.serverId)).size === 2 && pins.testPort !== pins.baselinePort
const fresh = evidence.observedAt <= now && now - evidence.observedAt <= 120000
const identityMatched = distinct && fresh && pins.vpsHostKeySha256 === evidence.vpsHostKeySha256 &&
pins.collectorInstance === evidence.collectorInstance && isDeepStrictEqual(pins.devices, evidence.devices)
const endpoint = pins.collectorAddress + ":" + pins.testPort
const owners = evidence.udpOwners.filter(row => row.port === pins.testPort)
const collectorBindMatched = identityMatched && pins.interfaceName === evidence.interfaceName &&
evidence.interfaceAddresses.includes(pins.collectorAddress) && owners.length === 1 &&
owners[0].pid === evidence.collectorPid && owners[0].address === pins.collectorAddress &&
evidence.worker.alive && evidence.worker.bound && evidence.worker.address === endpoint &&
evidence.settings.enabled && evidence.settings.collectorIp === pins.collectorAddress &&
evidence.settings.flowListenPort === pins.testPort && evidence.settings.listenerBound && evidence.settings.listenerAddress === endpoint
let ingressStatus: "missing" | "refused" | "matched" = ingress ? "refused" : "missing"
if (identityMatched && ingress) {
try {
const wire = projectChrWire(ingress.bytes, {
sources: [pins.devices[0].exporterSource, pins.devices[1].exporterSource], destination: pins.collectorAddress, port: pins.baselinePort,
})
if (wire.captureSha256 === ingress.sha256 &&
wire.frames.every(frame => frame.receivedMs <= now && frame.receivedMs >= now - 600000)) ingressStatus = "matched"
} catch {
// Keep the failed evidence state explicit without including packet data.
}
}
return {
identityMatched, collectorBindMatched, ingressStatus,
readyForReview: collectorBindMatched && ingressStatus === "matched",
targetScopes: pins.devices.map(row => ({ deviceId: row.deviceId, serverId: row.serverId, method: "PUT", path: "/ip/traffic-flow/target",
fields: { "dst-address": pins.collectorAddress, "src-address": row.exporterSource, port: String(pins.testPort), version: "ipfix" } })),
actualNativeAcceptance: false,
}
}
async function pinnedFile(path: string, digest: string, max: number) {
sha.parse(digest)
const file = await open(path, "r")
try {
const info = await file.stat()
if (!info.isFile() || info.size < 1 || info.size > max) throw Error("Input outside file bound")
const bytes = Buffer.alloc(info.size + 1)
let size = 0
while (size < bytes.length) {
const { bytesRead } = await file.read(bytes, size, bytes.length - size, size)
if (!bytesRead) break
size += bytesRead
}
const body = bytes.subarray(0, size)
if (size !== info.size || createHash("sha256").update(body).digest("hex") !== digest) throw Error("Input digest differs")
return body
} finally {
await file.close()
}
}
async function main() {
const args = process.argv.slice(2)
if (args.length !== 4 && args.length !== 6) throw Error("Expected pinned inputs")
const [pins, pinsSha, evidence, evidenceSha, capture, captureSha] = args
const report = inspectChrReadiness(
JSON.parse((await pinnedFile(pins, pinsSha, 16384)).toString("utf8")),
JSON.parse((await pinnedFile(evidence, evidenceSha, 16384)).toString("utf8")),
capture ? { bytes: await pinnedFile(capture, captureSha, 512 * 1024), sha256: captureSha } : undefined,
)
console.log(JSON.stringify(report))
if (!report.readyForReview) process.exitCode = 2
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch(() => { console.error("CHR readiness inputs refused"); process.exitCode = 1 })
}
@@ -0,0 +1,112 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { createChrRouteConntrackReader } from "./traffic-flow-chr-route-conntrack.js"
function fixture() {
const calls: string[] = []
const catalog: Record<string, Record<string, unknown>[]> = {
"/system/identity": [{ name: "chr-a" }],
"/system/resource": [{ version: "7.24.5 (stable)" }],
"/ip/settings": [{ "ip-forward": "true", "allow-fast-path": "false" }],
"/routing/rule": [],
"/ip/vrf": [{ ".id": "*1", name: "main", interfaces: "all" }],
"/routing/table": [{ ".id": "*1", name: "main", fib: "true" }],
"/interface": [{ ".id": "*2", name: "nfedge", type: "wg", running: "true", disabled: "false" }],
"/ip/address": [{ ".id": "*3", address: "198.18.241.2/30", interface: "nfedge", disabled: "false" }],
"/routing/route": [{ ".id": "*4", "routing-table": "main", "dst-address": "198.18.240.2/32",
gateway: "198.18.241.1", "immediate-gw": "198.18.241.1%nfedge", active: "true", disabled: "false" }],
"/ip/firewall/connection/tracking": [{ enabled: "yes" }],
"/ip/firewall/connection": [],
}
const config = {
identity: "chr-a", version: "7.24.5 (stable)",
routes: [{ routeId: "*4", destination: "198.18.240.2", nextHop: "198.18.241.1",
interfaceName: "nfedge", interfaceId: "*2", addressId: "*3", interfaceCidr: "198.18.241.2/30" }],
udp: [{ source: { address: "198.18.240.1", port: 50001 }, destination: { address: "198.18.240.2", port: 50002 } }],
assertCurrent: async () => {},
client: { async get<T>(path: string, timeout?: number, _signal?: AbortSignal, bytes?: number, fence?: () => Promise<unknown>) {
assert.ok(timeout && timeout <= 1800)
assert.equal(bytes, 512 * 1024)
await fence?.()
calls.push(path)
assert.ok(Object.hasOwn(catalog, path))
return structuredClone(catalog[path]) as T
} },
}
return { calls, catalog, config, read: createChrRouteConntrackReader(config) }
}
const foreignUdp = { ".id": "*1", protocol: "udp", "src-address": "192.0.2.1:50001", "dst-address": "192.0.2.2:50002",
"reply-src-address": "192.0.2.2:50002", "reply-dst-address": "192.0.2.1:50001" }
test("owned two-interface profile permits observed FastPath flag and auto only with empty conntrack", async () => {
const { catalog, config } = fixture()
const [route] = config.routes
const edge = "nfa012bcdeea", middle = "nfa012bcdema"
const owned = createChrRouteConntrackReader({ ...config, profile: "owned-wg-empty-firewall", routes: [
{ ...route, interfaceName: edge },
{ ...route, interfaceName: middle, interfaceId: "*5", addressId: "*6", routeId: "*7",
interfaceCidr: "198.18.241.5/30", destination: "198.18.240.1", nextHop: "198.18.241.6" },
] })
catalog["/ip/settings"][0]["allow-fast-path"] = "true"
catalog["/ip/firewall/connection/tracking"][0].enabled = "auto"
catalog["/interface"][0].name = edge
catalog["/interface"].push({ ...catalog["/interface"][0], ".id": "*5", name: middle })
catalog["/ip/address"][0].interface = edge
catalog["/ip/address"].push({ ...catalog["/ip/address"][0], ".id": "*6", interface: middle, address: "198.18.241.5/30" })
catalog["/routing/route"][0]["immediate-gw"] = "198.18.241.1%" + edge
catalog["/routing/route"].push({ ...catalog["/routing/route"][0], ".id": "*7", "dst-address": "198.18.240.1/32",
gateway: "198.18.241.6", "immediate-gw": "198.18.241.6%" + middle })
assert.deepEqual((await owned()).ownedInterfaces, [edge, middle])
catalog["/ip/firewall/connection"].push(foreignUdp)
await assert.rejects(owned(), /not observed empty/)
})
test("reads complete bounded native catalogs with identity fences and stable exact path fingerprint", async () => {
const { calls, catalog, read } = fixture()
const first = await read()
assert.equal(first.routesVerified, true)
assert.equal(first.freshConntrackTuples, true)
assert.match(first.fingerprint, /^[a-f0-9]{64}$/)
assert.deepEqual(calls, [...Object.keys(catalog).slice(0, 5), "/interface", "/ip/address", "/routing/table",
"/routing/route", "/ip/firewall/connection/tracking", "/ip/firewall/connection", "/system/identity", "/system/resource"])
catalog["/ip/firewall/connection"].push(foreignUdp)
assert.deepEqual(await read(), first)
catalog["/routing/route"][0].distance = "2"
assert.notEqual((await read()).fingerprint, first.fingerprint)
})
test("accepts the owned scoped gateway and skips explicitly typed non-IP link routes", async () => {
const { catalog, read } = fixture()
catalog["/routing/route"][0].gateway = "198.18.241.1%nfedge"
catalog["/routing/route"].push({ ".id": "*5", "routing-table": "main", afi: "link", "dst-address": "nfedge" })
assert.equal((await read()).routesVerified, true)
catalog["/routing/route"][1].afi = "ip"
await assert.rejects(read(), /invalid prefix/)
})
test("refuses original or reply tuple reuse and malformed nonmatching UDP rows", async () => {
for (const fields of [
{ "src-address": "198.18.240.1:50001", "dst-address": "198.18.240.2:50002" },
{ "reply-src-address": "198.18.240.2:50002", "reply-dst-address": "198.18.240.1:50001" },
{ "reply-src-address": undefined },
]) {
const { catalog, read } = fixture()
catalog["/ip/firewall/connection"].push({ ...foreignUdp, ...fields })
await assert.rejects(read(), /tracked|incomplete UDP/)
}
})
test("refuses competing routes, routing policy, unproven conntrack and changed device identity", async () => {
const competitor = fixture()
competitor.catalog["/routing/route"].push({ ...competitor.catalog["/routing/route"][0], ".id": "*5" })
await assert.rejects(competitor.read(), /exact active host route/)
const policy = fixture()
policy.catalog["/routing/rule"].push({ ".id": "*1", disabled: "false" })
await assert.rejects(policy.read(), /active routing policy/)
const tracking = fixture()
tracking.catalog["/ip/firewall/connection/tracking"][0].enabled = "auto"
await assert.rejects(tracking.read(), /not explicitly enabled/)
const identity = fixture()
identity.catalog["/system/identity"][0].name = "foreign"
await assert.rejects(identity.read(), /device identity changed/)
})
@@ -0,0 +1,160 @@
import { createHash } from "node:crypto"
import { isIP, isIPv4 } from "node:net"
import { performance } from "node:perf_hooks"
import type { MikrotikClient } from "../services/mikrotik.js"
import { isChrWireGuardInterfaceType } from "./traffic-flow-chr-transit-fixture.js"
type Row = Record<string, unknown>
type Socket = { address: string; port: number }
type UdpTuple = { source: Socket; destination: Socket }
type RoutePin = { routeId: string; destination: string; nextHop: string; interfaceName: string;
interfaceId: string; addressId: string; interfaceCidr: string }
type Options = {
client: Pick<MikrotikClient, "get">
identity: string
version: string
routes: readonly RoutePin[]
udp: readonly UdpTuple[]
assertCurrent: () => Promise<void>
profile?: "owned-wg-empty-firewall"
}
const deny = (message: string): never => { throw Error("RouterOS route/conntrack proof refused: " + message) }
const id = (value: unknown) => typeof value === "string" && /^\*[a-f0-9]{1,16}$/i.test(value)
const yes = (value: unknown) => value === true || value === "true" || value === "yes"
const no = (value: unknown) => value === false || value === "false" || value === "no"
const port = (value: number) => Number.isInteger(value) && value >= 1 && value <= 65535
const socketKey = (value: Socket) => value.address + ":" + value.port
const endpointsKey = (a: string, b: string) => [a, b].sort().join("/")
const canonicalRow = (row: Row) => Object.fromEntries(Object.keys(row).sort().map(key => [key, row[key]]))
const ipNumber = (address: string) => address.split(".").reduce((sum, octet) => sum * 256 + Number(octet), 0)
function prefix(value: unknown) {
if (typeof value !== "string") return deny("invalid prefix")
const [address, length, ...rest] = value.split("/")
const family = isIP(address)
if (!family || rest.length || !/^(0|[1-9][0-9]{0,2})$/.test(length) || Number(length) > (family === 4 ? 32 : 128)) {
return deny("invalid prefix")
}
return { address, length: Number(length), family }
}
function endpoint(value: unknown) {
if (typeof value !== "string") return deny("incomplete UDP conntrack row")
const match = /^([0-9.]+):([1-9][0-9]{0,4})$/.exec(value)
if (!match || !isIPv4(match[1]) || !port(Number(match[2]))) return deny("unsupported UDP conntrack endpoint")
return socketKey({ address: match[1], port: Number(match[2]) })
}
function sameSubnet(cidr: string, nextHop: string) {
const net = prefix(cidr)
const size = 2 ** (32 - net.length)
return net.family === 4 && net.length < 32 && net.address !== nextHop &&
Math.floor(ipNumber(net.address) / size) === Math.floor(ipNumber(nextHop) / size)
}
/** Narrow main-table reader for an already provisioned fixture. An authoritative
* caller brackets policy evaluation with independent fresh calls to this reader. */
export function createChrRouteConntrackReader(options: Options) {
const { client, assertCurrent, profile } = options
const { identity, version, routes, udp } = structuredClone({
identity: options.identity, version: options.version, routes: options.routes, udp: options.udp,
})
if (!identity || !version || !routes.length || routes.length > 16 || !udp.length || udp.length > 32 ||
new Set(routes.map(pin => pin.destination)).size !== routes.length || routes.some(pin =>
!id(pin.routeId) || !id(pin.interfaceId) || !id(pin.addressId) ||
!isIPv4(pin.destination) || !isIPv4(pin.nextHop) || !/^[\w.-]{1,64}$/.test(pin.interfaceName) ||
!sameSubnet(pin.interfaceCidr, pin.nextHop)) ||
udp.some(tuple => [tuple.source, tuple.destination].some(value => !isIPv4(value.address) || !port(value.port)))) deny("invalid fixture scope")
const pinnedTuples = new Set(udp.map(tuple => endpointsKey(socketKey(tuple.source), socketKey(tuple.destination))))
const ownedWg = profile === "owned-wg-empty-firewall"
const ownedInterfaces = [...new Set(routes.map(pin => pin.interfaceName))].sort()
if (ownedWg && (routes.some(pin => !/^nf[a-f0-9]{8}(ea|ma|mb|eb)$/.test(pin.interfaceName)) ||
new Set(routes.map(pin => pin.interfaceName.slice(0, -2))).size !== 1 ||
!["ea,ma", "eb,mb"].includes(ownedInterfaces.map(name => name.slice(-2)).join(",")) ||
new Set(routes.map(pin => pin.interfaceId)).size !== 2)) deny("owned WireGuard interface scope differs")
return async function readPathEvidence() {
const deadline = performance.now() + 12000
async function fence() {
if (performance.now() >= deadline) deny("expired evidence")
await assertCurrent()
if (performance.now() >= deadline) deny("expired evidence")
}
async function read(path: string, limit: number, single = false) {
await fence()
const response = await client.get<unknown>(path, Math.min(1800, Math.ceil(deadline - performance.now())),
undefined, 512 * 1024, fence)
await fence()
const rows = Array.isArray(response) ? response : single ? [response] : null
if (!rows || rows.length > limit || single && rows.length !== 1 ||
rows.some(row => !row || typeof row !== "object" || Array.isArray(row))) return deny("incomplete catalog")
return rows as Row[]
}
async function identityFence() {
const [device] = await read("/system/identity", 1, true)
const [resource] = await read("/system/resource", 1, true)
if (device.name !== identity || resource.version !== version) deny("device identity changed")
}
function requireCatalogIds(rows: Row[]) {
if (rows.some(row => !id(row[".id"])) || new Set(rows.map(row => row[".id"])).size !== rows.length) deny("invalid catalog IDs")
return rows
}
await identityFence()
const [ip] = await read("/ip/settings", 1, true)
if (!yes(ip["ip-forward"]) || (!no(ip["allow-fast-path"]) && !(ownedWg && yes(ip["allow-fast-path"])))) {
deny("forwarding or fast-path state unknown")
}
const policy = requireCatalogIds(await read("/routing/rule", 128))
if (policy.some(row => !yes(row.disabled))) deny("active routing policy")
const vrfs = requireCatalogIds(await read("/ip/vrf", 64))
if (vrfs.length !== 1 || vrfs[0].name !== "main" || vrfs[0].interfaces !== "all" || yes(vrfs[0].disabled)) deny("non-default VRF policy")
const interfaces = requireCatalogIds(await read("/interface", 256))
const addresses = requireCatalogIds(await read("/ip/address", 256))
const main = requireCatalogIds(await read("/routing/table", 64)).filter(row => row.name === "main")
if (main.length !== 1 || !yes(main[0].fib) || yes(main[0].disabled)) deny("main FIB unconfirmed")
const candidates = requireCatalogIds(await read("/routing/route", 2048))
for (const row of candidates) {
if (typeof row["routing-table"] !== "string" || !row["routing-table"]) deny("incomplete route catalog")
if (row.afi === "link") {
if (typeof row["dst-address"] !== "string" || !/^[\w.-]{1,64}$/.test(row["dst-address"])) deny("invalid link route")
} else prefix(row["dst-address"])
}
const bindings = routes.map(pin => {
const interfaceMatches = interfaces.filter(row => row.name === pin.interfaceName)
const iface = interfaceMatches[0]
const assigned = addresses.filter(row => row.address === pin.interfaceCidr)
const local = assigned[0]
if (interfaceMatches.length !== 1 || iface[".id"] !== pin.interfaceId || !isChrWireGuardInterfaceType(iface.type) ||
!yes(iface.running) || !no(iface.disabled) || assigned.length !== 1 || local[".id"] !== pin.addressId ||
local.interface !== pin.interfaceName || !no(local.disabled) || yes(local.invalid) ||
(local["actual-interface"] !== undefined && local["actual-interface"] !== pin.interfaceName)) deny("interface address binding differs")
const exact = candidates.filter(row => row["routing-table"] === "main" && row["dst-address"] === pin.destination + "/32")
const row = exact[0]
if (exact.length !== 1 || row[".id"] !== pin.routeId || !yes(row.active) || !no(row.disabled) ||
![pin.nextHop, pin.nextHop + "%" + pin.interfaceName].includes(String(row.gateway)) ||
row["immediate-gw"] !== pin.nextHop + "%" + pin.interfaceName ||
yes(row.blackhole) || yes(row.unreachable) || yes(row.prohibit) || yes(row.ecmp)) deny("exact active host route unavailable")
return [pin, canonicalRow(row), canonicalRow(local), iface.type, iface.running, iface.disabled]
})
const [tracking] = await read("/ip/firewall/connection/tracking", 1, true)
if (!yes(tracking.enabled) && !(ownedWg && tracking.enabled === "auto")) deny("conntrack is not explicitly enabled")
const connections = requireCatalogIds(await read("/ip/firewall/connection", 2048))
if (ownedWg && tracking.enabled === "auto" && connections.length !== 0) deny("automatic conntrack is not observed empty")
for (const row of connections) {
const protocol = row.protocol
if (protocol === "udp" || protocol === "17") {
const original = endpointsKey(endpoint(row["src-address"]), endpoint(row["dst-address"]))
const reply = endpointsKey(endpoint(row["reply-src-address"]), endpoint(row["reply-dst-address"]))
if (pinnedTuples.has(original) || pinnedTuples.has(reply)) deny("UDP fixture tuple already tracked")
} else if (typeof protocol !== "string" || !/^(tcp|icmp|gre|ipsec-esp|ipsec-ah|sctp|udplite|ospf|ipv6-encap|0|[1-9][0-9]{0,2})$/.test(protocol)) {
deny("unsupported conntrack protocol")
}
}
await identityFence()
const fingerprint = createHash("sha256").update(JSON.stringify([identity, version, profile, ip["allow-fast-path"], tracking.enabled, bindings,
canonicalRow(main[0]), canonicalRow(vrfs[0]), [...pinnedTuples].sort()])).digest("hex")
return { routesVerified: true as const, freshConntrackTuples: true as const, fingerprint,
...(profile && { profile, ownedInterfaces,
ownedRoutes: routes.map(pin => ({ destination: pin.destination, interfaceName: pin.interfaceName })),
udpPairs: [...pinnedTuples].sort() }) }
}
}
@@ -0,0 +1,9 @@
import { z } from "zod"
import { CHR_LEGACY_FIXTURE_DEADLINE_MAX_MS, CHR_STAGED_TARGET_CAPTURE_MAX_MS } from "./traffic-flow-chr-target-timing-profile.js"
export const chrStagedCaptureProfileSchema = z.object({
kind: z.literal("staged-disabled-target"),
captureMs: z.number().int().min(90000).max(CHR_STAGED_TARGET_CAPTURE_MAX_MS),
requiredCollectorWindowMs: z.number().int().positive().lt(CHR_LEGACY_FIXTURE_DEADLINE_MAX_MS),
}).strict().refine(profile => profile.requiredCollectorWindowMs > profile.captureMs + 210000,
"Staged collector window must include both measured phases and the cleanup reserve")
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more