Compare commits

...
Author SHA1 Message Date
Codex 5d181e7cb0 fix(netflow): пропустить загрузку флага неизвестной страны 2026-11-11 23:46:51 +07:00
Codex cf211f7418 fix(netflow): восстановить открытие потока после временного ограничения 2026-11-11 22:54:42 +07:00
Codex 04225886e0 fix(mcp): довести базовые сценарии и проверку маршрутизации 2026-11-11 20:28:37 +07:00
Denozordec 1f0704add0 fix(mcp): включить проверяемый контроллер маршрутизации 2026-11-05 05:36:19 +07:00
Codex 6569dbfc60 fix(mcp): привязать OAuth-согласие и завершать чтение логов при обслуживании 2026-11-05 03:56:17 +07:00
Denozordec 78b268cc39 fix(wireguard): безопасно повторить чтение зависимостей 2026-11-05 01:39:26 +07:00
Denozordec 3d5b7f0ea6 fix(traffic): загружать данные потоков последовательно 2026-11-04 23:51:03 +07:00
Denozordec b4d2d4bc20 fix(mcp): ограничить очередь NetFlow и защитить чтение истории 2026-11-04 21:58:22 +07:00
Denozordec c90a6ca3b8 fix(test): сравнивать тела миграций с учётом окончаний строк 2026-11-04 19:09:45 +07:00
Denozordec bf4b9b5443 fix(mcp): уточнить переключение маршрутов и хранение операций 2026-11-04 19:04:51 +07:00
Denozordec 5ce56b5168 fix(mcp): обновить проверенный снимок маршрутизации и хранения операций 2026-11-04 17:14:57 +07:00
Denozordec f1af1a85e5 fix(mcp): применять права к прямым переходам и проверять маршрутизацию 2026-11-04 01:57:07 +07:00
Denozordec 414a419f18 fix(ci): синхронизировать состав оператора базы для образов 2026-11-03 23:56:14 +07:00
Denozordec 6a286a1f66 fix(mcp): обновить снимок приёмки маршрутизации и базы данных 2026-11-03 23:38:19 +07:00
313 changed files with 29072 additions and 781 deletions

No files matched your search

+1 -3
View File
@@ -155,9 +155,7 @@ jobs:
cp -R packages/contracts/src "$STAGING/packages/contracts/" cp -R packages/contracts/src "$STAGING/packages/contracts/"
cp backend/package.json backend/tsconfig.json backend/mcp-coverage.json "$STAGING/backend/" cp backend/package.json backend/tsconfig.json backend/mcp-coverage.json "$STAGING/backend/"
cp -R backend/src "$STAGING/backend/" cp -R backend/src "$STAGING/backend/"
mkdir -p "$STAGING/backend/scripts/writer-profile" node backend/scripts/stage-database-operator.mjs "$STAGING/backend/scripts"
cp backend/scripts/build-database-operator.mjs backend/scripts/database-writer-bootstrap-operator.mjs backend/scripts/database-writer-profile-operator.mts "$STAGING/backend/scripts/"
cp backend/scripts/writer-profile/operator-files.mjs backend/scripts/writer-profile/operator-contract.mjs backend/scripts/writer-profile/publisher.mjs backend/scripts/writer-profile/context.mjs backend/scripts/writer-profile/catalogue.mts backend/scripts/writer-profile/catalogue-value-bridge.ts backend/scripts/writer-profile/catalogue-projection.ts backend/scripts/writer-profile/catalogue-leaves.ts backend/scripts/writer-profile/application-writer-profile-catalogue.ts backend/scripts/writer-profile/application-writer-profile-catalogue-values.ts backend/scripts/writer-profile/application-writer-profile-catalogue-parent-policy.ts backend/scripts/writer-profile/application-writer-profile-canonical.ts backend/scripts/writer-profile/fixed44-build-sql.ts backend/scripts/writer-profile/fixed44-build-definition.ts backend/scripts/writer-profile/expanded44-data-policy.ts "${STAGING}/backend/scripts/writer-profile/"
mkdir -p "$STAGING/deploy/netflow-agent" mkdir -p "$STAGING/deploy/netflow-agent"
cp deploy/netflow-agent/install.sh "$STAGING/deploy/netflow-agent/" cp deploy/netflow-agent/install.sh "$STAGING/deploy/netflow-agent/"
if [ -d backend/drizzle ]; then if [ -d backend/drizzle ]; then
+7 -5
View File
@@ -16,7 +16,7 @@ permissions:
packages: write packages: write
jobs: jobs:
backend-preview: backend-preview:
if: ${{ gitea.event_name == 'workflow_dispatch' }} if: ${{ gitea.event_name == 'workflow_dispatch' && gitea.ref == 'refs/heads/codex/mcp-acceptance-current-20261103' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 40 timeout-minutes: 40
env: env:
@@ -24,6 +24,7 @@ jobs:
EXPECTED_SHA: ${{ inputs.expected_sha }} EXPECTED_SHA: ${{ inputs.expected_sha }}
DISPATCH_SHA: ${{ gitea.sha }} DISPATCH_SHA: ${{ gitea.sha }}
DISPATCH_EVENT: ${{ gitea.event_name }} DISPATCH_EVENT: ${{ gitea.event_name }}
DISPATCH_REF: ${{ gitea.ref }}
REPOSITORY: ${{ gitea.repository }} REPOSITORY: ${{ gitea.repository }}
RUN_ID: ${{ gitea.run_id }} RUN_ID: ${{ gitea.run_id }}
RUN_ATTEMPT: ${{ gitea.run_attempt }} RUN_ATTEMPT: ${{ gitea.run_attempt }}
@@ -42,6 +43,7 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
test "$DISPATCH_EVENT" = workflow_dispatch test "$DISPATCH_EVENT" = workflow_dispatch
test "$DISPATCH_REF" = refs/heads/codex/mcp-acceptance-current-20261103
[[ "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]] [[ "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]
test "$EXPECTED_SHA" = "$DISPATCH_SHA" test "$EXPECTED_SHA" = "$DISPATCH_SHA"
test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
@@ -88,9 +90,7 @@ jobs:
if [ -d backend/drizzle ]; then if [ -d backend/drizzle ]; then
cp -R backend/drizzle "$STAGING/backend/" cp -R backend/drizzle "$STAGING/backend/"
fi fi
mkdir -p "$STAGING/backend/scripts/writer-profile" node backend/scripts/stage-database-operator.mjs "$STAGING/backend/scripts"
cp backend/scripts/build-database-operator.mjs backend/scripts/database-writer-bootstrap-operator.mjs backend/scripts/database-writer-profile-operator.mts "$STAGING/backend/scripts/"
cp backend/scripts/writer-profile/operator-files.mjs backend/scripts/writer-profile/operator-contract.mjs backend/scripts/writer-profile/publisher.mjs backend/scripts/writer-profile/context.mjs backend/scripts/writer-profile/catalogue.mts backend/scripts/writer-profile/catalogue-value-bridge.ts backend/scripts/writer-profile/catalogue-projection.ts backend/scripts/writer-profile/catalogue-leaves.ts backend/scripts/writer-profile/application-writer-profile-catalogue.ts backend/scripts/writer-profile/application-writer-profile-catalogue-values.ts backend/scripts/writer-profile/application-writer-profile-catalogue-parent-policy.ts backend/scripts/writer-profile/application-writer-profile-canonical.ts backend/scripts/writer-profile/fixed44-build-sql.ts backend/scripts/writer-profile/fixed44-build-definition.ts backend/scripts/writer-profile/expanded44-data-policy.ts "${STAGING}/backend/scripts/writer-profile/"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Log in to existing Gitea registry - name: Log in to existing Gitea registry
@@ -139,7 +139,7 @@ jobs:
if-no-files-found: error if-no-files-found: error
retention-days: 14 retention-days: 14
frontend-preview: frontend-preview:
if: ${{ gitea.event_name == 'workflow_dispatch' }} if: ${{ gitea.event_name == 'workflow_dispatch' && gitea.ref == 'refs/heads/codex/mcp-acceptance-current-20261103' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 40 timeout-minutes: 40
env: env:
@@ -147,6 +147,7 @@ jobs:
EXPECTED_SHA: ${{ inputs.expected_sha }} EXPECTED_SHA: ${{ inputs.expected_sha }}
DISPATCH_SHA: ${{ gitea.sha }} DISPATCH_SHA: ${{ gitea.sha }}
DISPATCH_EVENT: ${{ gitea.event_name }} DISPATCH_EVENT: ${{ gitea.event_name }}
DISPATCH_REF: ${{ gitea.ref }}
REPOSITORY: ${{ gitea.repository }} REPOSITORY: ${{ gitea.repository }}
RUN_ID: ${{ gitea.run_id }} RUN_ID: ${{ gitea.run_id }}
RUN_ATTEMPT: ${{ gitea.run_attempt }} RUN_ATTEMPT: ${{ gitea.run_attempt }}
@@ -178,6 +179,7 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
test "$DISPATCH_EVENT" = workflow_dispatch test "$DISPATCH_EVENT" = workflow_dispatch
test "$DISPATCH_REF" = refs/heads/codex/mcp-acceptance-current-20261103
[[ "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]] [[ "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]
test "$EXPECTED_SHA" = "$DISPATCH_SHA" test "$EXPECTED_SHA" = "$DISPATCH_SHA"
test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
+9 -6
View File
@@ -924,18 +924,21 @@ export default function TrafficPage() {
}) })
return () => { cancelled = true } return () => { cancelled = true }
} }
void Promise.all([ const loadFlowCards = async () => {
getTrafficFlows(backendUrl, range), // These reads share the collector's constrained admission budget.
getFlowExporters(backendUrl, range), const stats = await getTrafficFlows(backendUrl, range)
getFlowClients(backendUrl, range), if (cancelled) return
]).then(([stats, exporters, clients]) => { const exporters = await getFlowExporters(backendUrl, range)
if (cancelled) return
const clients = await getFlowClients(backendUrl, range)
if (cancelled) return if (cancelled) return
setFlowResult({ key: flowKey, receivedAt: Date.now(), stats, exporters: exporters.exporters, clients: clients.clients }) setFlowResult({ key: flowKey, receivedAt: Date.now(), stats, exporters: exporters.exporters, clients: clients.clients })
setSelectedId(prev => { setSelectedId(prev => {
const list = flowScope === "users" ? clients.clients : exporters.exporters const list = flowScope === "users" ? clients.clients : exporters.exporters
return list.some(x => x.id === prev) ? prev : (list[0]?.id ?? "") return list.some(x => x.id === prev) ? prev : (list[0]?.id ?? "")
}) })
}).catch(e => { }
void loadFlowCards().catch(e => {
if (!cancelled) setFlowResult({ key: flowKey, error: e instanceof Error ? e.message : "Не удалось загрузить потоки" }) if (!cancelled) setFlowResult({ key: flowKey, error: e instanceof Error ? e.message : "Не удалось загрузить потоки" })
}) })
return () => { cancelled = true } return () => { cancelled = true }
+10 -1
View File
@@ -1,7 +1,16 @@
@import "tailwindcss"; @import "tailwindcss" source(none);
@import "tw-animate-css"; @import "tw-animate-css";
@import "shadcn/tailwind.css"; @import "shadcn/tailwind.css";
/* Scan application sources only; local test archives and generated bundles
must not become utility classes in the production stylesheet. */
@source "./";
@source "../components";
@source "../entities";
@source "../hooks";
@source "../lib";
@source "../shared";
/* SVG focus follows the object shape, never the text/foreignObject bounding box. */ /* SVG focus follows the object shape, never the text/foreignObject bounding box. */
.network-map-focusable { .network-map-focusable {
outline: none; outline: none;
@@ -0,0 +1,13 @@
BEGIN;
CREATE TABLE public.mcp_operation_replay_tombstones (
operation_id uuid PRIMARY KEY,
plan_id uuid NOT NULL UNIQUE,
connection_id uuid NOT NULL REFERENCES public.mcp_connections(id),
idempotency_digest text NOT NULL CHECK (idempotency_digest ~ '^[0-9a-f]{64}$'),
action_id text NOT NULL CHECK (length(action_id) BETWEEN 1 AND 200),
target_server_ids bigint[] NOT NULL CHECK (array_position(target_server_ids, NULL) IS NULL),
target_global boolean NOT NULL,
UNIQUE (connection_id,idempotency_digest)
);
INSERT INTO public.schema_migrations(id) VALUES ('0045_mcp_operation_replay_tombstones') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,186 @@
-- Inactive poll-history V1-to-V2 retirement. Application owner authenticates
-- both ciphertext versions; SQL independently fences identity and lifetime.
BEGIN;
LOCK TABLE public.server_poll_archives,public.server_poll_storage_admissions IN ACCESS EXCLUSIVE MODE;
ALTER TABLE public.server_poll_archives DROP CONSTRAINT server_poll_archives_version_check;
ALTER TABLE public.server_poll_archives ADD CONSTRAINT server_poll_archives_version_check
CHECK(version=1 OR (version=2 AND decoded_bytes<=24576 AND compressed_bytes<=24576 AND ciphertext_bytes<=33000));
CREATE OR REPLACE FUNCTION public.server_poll_archive_immutable() RETURNS trigger
LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='DELETE' AND public.server_poll_storage_gc_allowed(OLD.receipt_id) THEN RETURN OLD; END IF;
IF TG_OP='UPDATE' AND OLD.version=1 AND NEW.version=2
AND current_setting('mm.poll_retire_receipt',true)=OLD.receipt_id::text
AND NEW.ciphertext_bytes<=OLD.ciphertext_bytes
AND NEW.ciphertext ~ '^[A-Za-z0-9+/]+={0,2}$'
AND replace(encode(decode(NEW.ciphertext,'base64'),'base64'),E'\n','')=NEW.ciphertext
AND octet_length(decode(NEW.ciphertext,'base64'))=NEW.compressed_bytes+28
AND (to_jsonb(OLD)-ARRAY['version','source_digest','source_sha256','decoded_bytes','compressed_bytes','ciphertext','ciphertext_bytes','archive_proof'])=
(to_jsonb(NEW)-ARRAY['version','source_digest','source_sha256','decoded_bytes','compressed_bytes','ciphertext','ciphertext_bytes','archive_proof'])
AND EXISTS(
SELECT 1 FROM public.server_poll_storage_admissions a JOIN public.mcp_operations o ON o.id=a.execution_id AND o.connection_id=a.connection_id
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=0
JOIN public.mcp_operation_replay_tombstones t ON t.operation_id=o.id AND t.plan_id=p.id AND t.connection_id=o.connection_id
JOIN public.mcp_connections c ON c.id=o.connection_id AND c.owner_id=a.owner_id
WHERE a.receipt_id=OLD.receipt_id AND a.identity_kind='mcp' AND a.storage_state='compacted'
AND a.execution_id=OLD.execution_id AND a.connection_id=OLD.connection_id AND a.owner_id=OLD.owner_id
AND a.binding_proof=OLD.binding_proof AND a.context_proof=OLD.context_proof AND a.archive_bytes=OLD.ciphertext_bytes
AND a.original_activity_at=OLD.original_activity_at AND a.authority_expires_at=OLD.original_expires_at
AND o.state='applied' AND o.verification='passed' AND o.error_code IS NULL AND isfinite(o.finished_at)
AND o.finished_at<transaction_timestamp()-interval '180 days'
AND p.state='completed' AND p.operation_id='servers.poll' AND p.operation_version=1 AND p.approved_by IS NOT NULL
AND s.state='verified' AND s.finished_at IS NOT NULL AND t.action_id=p.operation_id
AND t.target_global AND cardinality(t.target_server_ids)>0
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'poll archive immutable outside exact retirement or original lifetime GC';
END $$;
CREATE FUNCTION public.server_poll_retirement_final_guard() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
IF NOT EXISTS(SELECT 1 FROM public.mcp_operation_replay_tombstones t
WHERE t.operation_id=NEW.execution_id AND t.connection_id=NEW.connection_id AND t.action_id='servers.poll'
AND t.target_global AND NOT EXISTS(SELECT 1 FROM public.mcp_plans p WHERE p.id=t.plan_id))
OR EXISTS(SELECT 1 FROM public.mcp_operations o WHERE o.id=NEW.execution_id)
OR NOT public.server_poll_storage_accounting_valid()
THEN RAISE EXCEPTION 'poll retirement is incomplete'; END IF;
RETURN NULL;
END $$;
CREATE CONSTRAINT TRIGGER server_poll_retirement_final_guard AFTER UPDATE ON public.server_poll_archives
DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN(OLD.version=1 AND NEW.version=2)
EXECUTE FUNCTION public.server_poll_retirement_final_guard();
CREATE OR REPLACE FUNCTION server_poll_storage_admission_guard() RETURNS trigger LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
DECLARE retention integer; charged integer; r public.server_poll_archives%ROWTYPE;
BEGIN
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
IF TG_OP='DELETE' THEN
IF NOT public.server_poll_storage_gc_allowed(OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_executions WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_intents WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_acks WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_receipts WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_quarantines WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_archives WHERE receipt_id=OLD.receipt_id) THEN RAISE EXCEPTION 'poll lifetime or children prevent admission deletion'; END IF;
UPDATE public.server_poll_storage_usage SET admissions=admissions-CASE WHEN OLD.storage_state='full' THEN 1 ELSE 0 END,
compacted_admissions=compacted_admissions-CASE WHEN OLD.storage_state='compacted' THEN 1 ELSE 0 END,
archive_bytes=archive_bytes-COALESCE(OLD.archive_bytes,0),
reserved_rows=reserved_rows-CASE WHEN OLD.storage_state='full' THEN 4019 ELSE 2 END,
reserved_bytes=reserved_bytes-CASE WHEN OLD.storage_state='full' THEN 8421376 ELSE 32768+OLD.archive_bytes END
WHERE scope IN('global','owner:'||OLD.owner_id);
GET DIAGNOSTICS charged=ROW_COUNT; IF charged<>2 THEN RAISE EXCEPTION 'poll quota refund mismatch'; END IF; RETURN OLD;
END IF;
IF TG_OP='UPDATE' THEN
IF OLD.storage_state='compacted' AND NEW.storage_state='compacted'
AND current_setting('mm.poll_retire_receipt',true)=OLD.receipt_id::text
AND (to_jsonb(OLD)-'archive_bytes')=(to_jsonb(NEW)-'archive_bytes') THEN
SELECT * INTO r FROM public.server_poll_archives WHERE receipt_id=OLD.receipt_id AND version=2;
IF NOT FOUND OR r.ciphertext_bytes<>NEW.archive_bytes OR NEW.archive_bytes>OLD.archive_bytes
OR r.execution_id<>OLD.execution_id OR r.connection_id<>OLD.connection_id OR r.owner_id<>OLD.owner_id
THEN RAISE EXCEPTION 'retired poll charge source mismatch'; END IF;
UPDATE public.server_poll_storage_usage
SET archive_bytes=archive_bytes-OLD.archive_bytes+NEW.archive_bytes,
reserved_bytes=reserved_bytes-OLD.archive_bytes+NEW.archive_bytes
WHERE scope IN ('global','owner:'||OLD.owner_id) AND compacted_admissions>=1 AND archive_bytes>=OLD.archive_bytes;
GET DIAGNOSTICS charged=ROW_COUNT;
IF charged<>2 THEN RAISE EXCEPTION 'retired poll charge adjustment mismatch'; END IF;
RETURN NEW;
END IF;
-- This durable one-shot marker is written and ACKed in its OWN transaction
-- before archive construction. A committed/unknown claim never resets and
-- the worker never retries destructive work against an already claimed full
-- source. Claim time is maintenance metadata, not renewed execution activity.
IF OLD.storage_state='full' AND NEW.storage_state='full' AND OLD.compaction_claim IS NULL AND OLD.compaction_claimed_at IS NULL AND OLD.compaction_claim_xid IS NULL
AND NEW.compaction_claim IS NOT NULL AND NEW.compaction_claim_xid IS NULL
AND(to_jsonb(OLD)-ARRAY['compaction_claim','compaction_claimed_at','compaction_claim_xid'])=(to_jsonb(NEW)-ARRAY['compaction_claim','compaction_claimed_at','compaction_claim_xid'])
AND public.server_poll_storage_compact_allowed(OLD.receipt_id)
THEN NEW.compaction_claimed_at:=clock_timestamp(); NEW.compaction_claim_xid:=pg_current_xact_id(); RETURN NEW; END IF;
IF(to_jsonb(OLD)-ARRAY['claim_token','claim_attempted_at','used_rows','used_bytes','storage_state','compact_at','archive_bytes','original_activity_at']) IS DISTINCT FROM
(to_jsonb(NEW)-ARRAY['claim_token','claim_attempted_at','used_rows','used_bytes','storage_state','compact_at','archive_bytes','original_activity_at']) THEN RAISE EXCEPTION 'poll admission identity is immutable'; END IF;
IF OLD.storage_state<>'full' THEN RAISE EXCEPTION 'compacted poll identity cannot reopen or change'; END IF;
IF NEW.storage_state='compacted' THEN
SELECT * INTO r FROM public.server_poll_archives WHERE receipt_id=OLD.receipt_id AND compaction_xid=pg_current_xact_id();
IF NOT FOUND OR NEW.used_rows<>OLD.used_rows OR NEW.used_bytes<>OLD.used_bytes OR NEW.claim_token IS DISTINCT FROM OLD.claim_token
OR NEW.claim_attempted_at IS DISTINCT FROM OLD.claim_attempted_at OR NEW.archive_bytes<>r.ciphertext_bytes OR NEW.original_activity_at<>r.original_activity_at
OR EXISTS(SELECT 1 FROM public.server_poll_executions WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_intents WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_acks WHERE receipt_id=OLD.receipt_id)
OR EXISTS(SELECT 1 FROM public.server_poll_receipts WHERE receipt_id=OLD.receipt_id) OR EXISTS(SELECT 1 FROM public.server_poll_quarantines WHERE receipt_id=OLD.receipt_id)
THEN RAISE EXCEPTION 'poll compaction requires the complete current-transaction archive'; END IF;
NEW.compact_at:=r.created_at;
UPDATE public.server_poll_storage_usage SET admissions=admissions-1,compacted_admissions=compacted_admissions+1,
archive_bytes=archive_bytes+r.ciphertext_bytes,reserved_rows=reserved_rows-4019+2,reserved_bytes=reserved_bytes-8421376+32768+r.ciphertext_bytes
WHERE scope IN('global','owner:'||OLD.owner_id) AND admissions>=1;
GET DIAGNOSTICS charged=ROW_COUNT; IF charged<>2 THEN RAISE EXCEPTION 'poll compaction charge mismatch'; END IF;
RETURN NEW;
END IF;
IF NEW.compact_at IS NOT NULL OR NEW.archive_bytes IS NOT NULL OR NEW.original_activity_at IS NOT NULL THEN RAISE EXCEPTION 'full poll cannot carry archive metadata'; END IF;
IF OLD.claim_token IS NOT NULL AND NEW.claim_token IS NULL AND OLD.claim_attempted_at IS NULL AND NEW.claim_attempted_at IS NOT NULL
AND NEW.used_rows=OLD.used_rows AND NEW.used_bytes=OLD.used_bytes AND OLD.authority_expires_at>clock_timestamp()
THEN NEW.claim_attempted_at:=clock_timestamp(); RETURN NEW; END IF;
IF OLD.claim_token IS NOT DISTINCT FROM NEW.claim_token AND OLD.claim_attempted_at IS NOT DISTINCT FROM NEW.claim_attempted_at
AND NEW.used_rows=OLD.used_rows+1 AND NEW.used_bytes>OLD.used_bytes THEN RETURN NEW; END IF;
RAISE EXCEPTION 'poll admission cannot refresh, reopen or refund itself';
END IF;
IF NEW.storage_state<>'full' OR NEW.compact_at IS NOT NULL OR NEW.archive_bytes IS NOT NULL OR NEW.original_activity_at IS NOT NULL
OR NEW.compaction_claim IS NOT NULL OR NEW.compaction_claimed_at IS NOT NULL OR NEW.compaction_claim_xid IS NOT NULL
OR NOT public.server_poll_storage_accounting_valid() THEN RAISE EXCEPTION 'new poll requires full reservation and exact ledger'; END IF;
SELECT requests_retention_days INTO retention FROM public.mcp_settings WHERE id=1;
IF retention IS NULL OR retention NOT BETWEEN 1 AND 365 OR NEW.authority_issued_at>clock_timestamp() OR NEW.authority_expires_at<=clock_timestamp()
OR NEW.claim_token IS NULL OR NEW.claim_attempted_at IS NOT NULL OR NEW.used_rows<>0 OR NEW.used_bytes<>0 THEN RAISE EXCEPTION 'new poll admission needs original live authority'; END IF;
IF EXISTS(SELECT 1 FROM public.server_poll_storage_admissions WHERE receipt_id=NEW.receipt_id OR(identity_kind=NEW.identity_kind AND execution_id=NEW.execution_id)) THEN RAISE EXCEPTION 'poll identity already spent'; END IF;
NEW.created_at:=clock_timestamp(); NEW.retention_days:=retention; NEW.safety_ms:=60000;
NEW.gc_not_before:=NEW.authority_expires_at+make_interval(days=>retention)+interval '60 seconds';
INSERT INTO public.server_poll_storage_usage(scope) VALUES('owner:'||NEW.owner_id) ON CONFLICT DO NOTHING;
IF(SELECT count(*) FROM public.server_poll_storage_usage WHERE scope IN('global','owner:'||NEW.owner_id)
AND admissions<CASE WHEN scope='global' THEN 128 ELSE 8 END
AND reserved_rows<=CASE WHEN scope='global' THEN 514432 ELSE 32152 END-4019
AND reserved_bytes<=CASE WHEN scope='global' THEN 1077936128 ELSE 67371008 END-8421376)<>2 THEN RAISE EXCEPTION 'poll admission quota exhausted'; END IF;
UPDATE public.server_poll_storage_usage SET admissions=admissions+1,reserved_rows=reserved_rows+4019,reserved_bytes=reserved_bytes+8421376 WHERE scope IN('global','owner:'||NEW.owner_id);
GET DIAGNOSTICS charged=ROW_COUNT; IF charged<>2 THEN RAISE EXCEPTION 'poll admission quota unavailable'; END IF;
RETURN NEW;
END $$;
CREATE OR REPLACE FUNCTION server_poll_storage_gc_allowed(id uuid) RETURNS boolean LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
DECLARE a public.server_poll_storage_admissions%ROWTYPE; retention integer; activity timestamptz; terminal_at timestamptz;
BEGIN
IF current_setting('mm.poll_storage_gc',true) IS DISTINCT FROM 'on' THEN RETURN false; END IF;
PERFORM pg_advisory_xact_lock(hashtextextended('server-poll-storage-v1',0));
SELECT * INTO a FROM public.server_poll_storage_admissions WHERE receipt_id=id FOR UPDATE;
IF NOT FOUND OR a.authority_expires_at>=clock_timestamp() OR a.gc_not_before>=clock_timestamp() THEN RETURN false; END IF;
SELECT requests_retention_days INTO retention FROM public.mcp_settings WHERE mcp_settings.id=1 FOR SHARE;
IF retention IS NULL OR retention NOT BETWEEN 1 AND 365 THEN RETURN false; END IF;
terminal_at:=a.created_at;
IF a.identity_kind='mcp' THEN
IF NOT pg_try_advisory_xact_lock(hashtextextended('mcp-operation:'||a.execution_id::text,0)) THEN RETURN false; END IF;
BEGIN
SELECT GREATEST(o.finished_at,p.expires_at) INTO terminal_at FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id JOIN public.mcp_connections c ON c.id=o.connection_id
WHERE o.id=a.execution_id AND o.connection_id=a.connection_id AND c.owner_id=a.owner_id AND p.operation_id='servers.poll'
AND o.state IN('applied','failed','unknown','rejected','partial','cancelled') AND o.finished_at IS NOT NULL FOR UPDATE OF o,p NOWAIT;
IF NOT FOUND THEN
IF a.storage_state<>'compacted' OR EXISTS(SELECT 1 FROM public.mcp_operations o WHERE o.id=a.execution_id) OR NOT EXISTS (
SELECT 1 FROM public.mcp_operation_replay_tombstones t JOIN public.mcp_connections c ON c.id=t.connection_id
WHERE t.operation_id=a.execution_id AND t.connection_id=a.connection_id AND c.owner_id=a.owner_id
AND t.action_id='servers.poll' AND t.target_global AND cardinality(t.target_server_ids)>0
AND NOT EXISTS(SELECT 1 FROM public.mcp_plans WHERE mcp_plans.id=t.plan_id)
) OR EXISTS(SELECT 1 FROM public.server_poll_archives r WHERE r.receipt_id=id AND
(r.version<>2 OR r.execution_id<>a.execution_id OR r.connection_id<>a.connection_id OR r.owner_id<>a.owner_id
OR r.original_activity_at IS DISTINCT FROM a.original_activity_at OR r.original_expires_at<>a.authority_expires_at
OR r.binding_proof<>a.binding_proof OR r.context_proof<>a.context_proof OR r.ciphertext_bytes<>a.archive_bytes))
THEN RETURN false; END IF;
terminal_at:=a.original_activity_at;
END IF;
IF terminal_at IS NULL THEN RETURN false; END IF;
EXCEPTION WHEN lock_not_available THEN RETURN false; END;
END IF;
IF a.storage_state='compacted' THEN activity:=GREATEST(a.original_activity_at,a.authority_expires_at,terminal_at);
ELSE
SELECT GREATEST(a.authority_expires_at,a.created_at,COALESCE(a.claim_attempted_at,a.created_at),terminal_at,
COALESCE((SELECT created_at FROM public.server_poll_executions WHERE receipt_id=id),a.created_at),
COALESCE((SELECT max(created_at) FROM public.server_poll_intents WHERE receipt_id=id),a.created_at),
COALESCE((SELECT max(created_at) FROM public.server_poll_acks WHERE receipt_id=id),a.created_at),
COALESCE((SELECT created_at FROM public.server_poll_receipts WHERE receipt_id=id),a.created_at),
COALESCE((SELECT created_at FROM public.server_poll_quarantines WHERE receipt_id=id),a.created_at)) INTO activity;
END IF;
RETURN isfinite(activity) AND activity+make_interval(days=>GREATEST(a.retention_days,retention))+a.safety_ms*interval '1 millisecond'<clock_timestamp();
END $$;
INSERT INTO public.schema_migrations(id) VALUES ('0046_server_poll_operation_retirement') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,40 @@
BEGIN;
CREATE TABLE public.server_poll_retirement_claims (
receipt_id uuid PRIMARY KEY REFERENCES public.server_poll_storage_admissions(receipt_id) ON DELETE CASCADE,
old_archive_proof text NOT NULL CHECK(old_archive_proof ~ '^[0-9a-f]{64}$'),
claim_token uuid NOT NULL,
claim_xid xid8 NOT NULL DEFAULT pg_current_xact_id()
);
CREATE FUNCTION public.server_poll_retirement_claim_immutable() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.server_poll_archives r WHERE r.receipt_id=NEW.receipt_id
AND r.version=1 AND r.archive_proof=NEW.old_archive_proof
) THEN RETURN NEW; END IF;
IF TG_OP='DELETE' AND pg_trigger_depth()>1 AND NOT EXISTS(
SELECT 1 FROM public.server_poll_storage_admissions a WHERE a.receipt_id=OLD.receipt_id
) THEN RETURN OLD; END IF;
RAISE EXCEPTION 'poll retirement claim cannot be replaced or reopened';
END $$;
CREATE TRIGGER server_poll_retirement_claim_immutable
BEFORE INSERT OR UPDATE OR DELETE ON public.server_poll_retirement_claims
FOR EACH ROW EXECUTE FUNCTION public.server_poll_retirement_claim_immutable();
CREATE TRIGGER server_poll_retirement_claim_no_truncate
BEFORE TRUNCATE ON public.server_poll_retirement_claims
FOR EACH STATEMENT EXECUTE FUNCTION public.server_poll_retirement_claim_immutable();
CREATE FUNCTION public.server_poll_retirement_requires_claim() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
IF NOT EXISTS(SELECT 1 FROM public.server_poll_retirement_claims c
WHERE c.receipt_id=OLD.receipt_id AND c.old_archive_proof=OLD.archive_proof
AND c.claim_token::text=current_setting('mm.poll_retire_claim',true)
AND c.claim_xid<>pg_current_xact_id())
THEN RAISE EXCEPTION 'poll retirement requires prior exact claim'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER server_poll_retirement_requires_claim BEFORE UPDATE ON public.server_poll_archives
FOR EACH ROW WHEN(OLD.version=1 AND NEW.version=2)
EXECUTE FUNCTION public.server_poll_retirement_requires_claim();
INSERT INTO public.schema_migrations(id) VALUES ('0047_server_poll_retirement_claims') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,27 @@
BEGIN;
CREATE TABLE public.mcp_ordinary_retirement_claims (
operation_id uuid PRIMARY KEY,
plan_id uuid NOT NULL UNIQUE,
connection_id uuid NOT NULL REFERENCES public.mcp_connections(id),
source_proof text NOT NULL CHECK(source_proof ~ '^[0-9a-f]{64}$'),
claim_token uuid NOT NULL,
claim_xid xid8 NOT NULL DEFAULT pg_current_xact_id()
);
CREATE FUNCTION public.mcp_ordinary_retirement_claim_immutable() RETURNS trigger
LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.mcp_operations o JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
WHERE o.id=NEW.operation_id AND o.plan_id=NEW.plan_id AND o.connection_id=NEW.connection_id
AND o.state='applied' AND o.verification='passed' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND p.operation_id='logs.settings.update' AND p.operation_version=1 AND p.state='completed'
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'ordinary operation retirement claim cannot be replaced or reopened';
END $$;
CREATE TRIGGER mcp_ordinary_retirement_claim_immutable BEFORE INSERT OR UPDATE OR DELETE
ON public.mcp_ordinary_retirement_claims FOR EACH ROW EXECUTE FUNCTION public.mcp_ordinary_retirement_claim_immutable();
CREATE TRIGGER mcp_ordinary_retirement_claim_no_truncate BEFORE TRUNCATE
ON public.mcp_ordinary_retirement_claims FOR EACH STATEMENT EXECUTE FUNCTION public.mcp_ordinary_retirement_claim_immutable();
INSERT INTO public.schema_migrations(id) VALUES ('0048_mcp_ordinary_retirement_claims') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,18 @@
BEGIN;
CREATE OR REPLACE FUNCTION public.mcp_ordinary_retirement_claim_immutable() RETURNS trigger
LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.mcp_operations o JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
WHERE o.id=NEW.operation_id AND o.plan_id=NEW.plan_id AND o.connection_id=NEW.connection_id
AND o.state='applied' AND o.verification='passed' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND (p.operation_id,p.operation_version) IN (
('logs.settings.update',1),('traffic.settings.update',2),('uptime.settings.update',2),
('servers.api-ping.settings.update',2),('internet-path.settings.update',1)
) AND p.state='completed'
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'ordinary operation retirement claim cannot be replaced or reopened';
END $$;
INSERT INTO public.schema_migrations(id) VALUES ('0049_mcp_ordinary_telemetry_retirement') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,22 @@
BEGIN;
CREATE TABLE public.server_probe_retired_sources (
receipt_id uuid PRIMARY KEY REFERENCES public.server_probe_storage_admissions(receipt_id),
operation_id uuid NOT NULL UNIQUE REFERENCES public.mcp_operation_replay_tombstones(operation_id),
source jsonb NOT NULL CHECK(jsonb_typeof(source)='object' AND octet_length(source::text) BETWEEN 1 AND 8192),
proof text NOT NULL CHECK(proof ~ '^[0-9a-f]{64}$'),
CHECK((source->>'version') IS NOT DISTINCT FROM '1'
AND (source->>'receiptId') IS NOT DISTINCT FROM receipt_id::text
AND (source->>'executionId') IS NOT DISTINCT FROM operation_id::text)
);
CREATE FUNCTION public.server_probe_retired_source_immutable() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
RAISE EXCEPTION 'retired diagnostic source is immutable';
END $$;
CREATE TRIGGER server_probe_retired_source_immutable
BEFORE UPDATE OR DELETE ON public.server_probe_retired_sources
FOR EACH ROW EXECUTE FUNCTION public.server_probe_retired_source_immutable();
CREATE TRIGGER server_probe_retired_source_no_truncate BEFORE TRUNCATE ON public.server_probe_retired_sources
FOR EACH STATEMENT EXECUTE FUNCTION public.server_probe_retired_source_immutable();
INSERT INTO public.schema_migrations(id) VALUES ('0050_server_probe_retired_sources') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,52 @@
BEGIN;
CREATE TABLE public.server_probe_retirement_claims (
receipt_id uuid PRIMARY KEY,
operation_id uuid NOT NULL UNIQUE,
plan_id uuid NOT NULL UNIQUE,
connection_id uuid NOT NULL,
source_proof text NOT NULL CHECK(source_proof ~ '^[a-f0-9]{64}$'),
claim_token uuid NOT NULL,
claim_xid xid8 NOT NULL DEFAULT pg_current_xact_id()
);
CREATE FUNCTION public.server_probe_retirement_claim_guard() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public SET TimeZone='UTC' AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.server_probe_storage_admissions a
JOIN public.mcp_operations o ON o.id=a.execution_id
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_connections c ON c.id=o.connection_id AND c.owner_id=a.owner_id
WHERE a.receipt_id=NEW.receipt_id AND a.identity_kind='mcp' AND a.claim_token IS NULL
AND a.claim_attempted_at IS NOT NULL AND a.cancelled_at IS NULL AND a.purged_at IS NULL
AND o.id=NEW.operation_id AND p.id=NEW.plan_id AND c.id=NEW.connection_id
AND o.state='applied' AND o.verification='inconclusive' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND p.operation_id='servers.probes.run' AND p.operation_version=1 AND p.state='completed'
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'diagnostic retirement claim cannot be replaced or reopened';
END $$;
CREATE TRIGGER server_probe_retirement_claim_guard BEFORE INSERT OR UPDATE OR DELETE
ON public.server_probe_retirement_claims FOR EACH ROW EXECUTE FUNCTION public.server_probe_retirement_claim_guard();
CREATE TRIGGER server_probe_retirement_claim_no_truncate BEFORE TRUNCATE
ON public.server_probe_retirement_claims FOR EACH STATEMENT EXECUTE FUNCTION public.server_probe_retirement_claim_guard();
CREATE FUNCTION public.server_probe_retired_source_insert_guard() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
IF EXISTS(
SELECT 1 FROM public.server_probe_retirement_claims r
JOIN public.mcp_operation_replay_tombstones t ON t.operation_id=r.operation_id AND t.plan_id=r.plan_id
AND t.connection_id=r.connection_id AND t.action_id='servers.probes.run' AND NOT t.target_global
JOIN public.mcp_operations o ON o.id=r.operation_id AND o.plan_id=r.plan_id AND o.connection_id=r.connection_id
JOIN public.server_probe_storage_admissions a ON a.receipt_id=r.receipt_id AND a.execution_id=r.operation_id
WHERE r.receipt_id=NEW.receipt_id AND r.operation_id=NEW.operation_id AND r.source_proof=NEW.proof
AND r.claim_xid<>pg_current_xact_id() AND r.claim_token::text=current_setting('mm.probe_retirement_claim',true)
AND NEW.source->>'planId'=r.plan_id::text AND NEW.source->>'connectionId'=r.connection_id::text
AND NEW.source->>'ownerId'=a.owner_id AND NEW.source->>'bindingProof'=a.binding_proof
AND NEW.source->'serverIds'=to_jsonb(t.target_server_ids)
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'diagnostic archive requires an exact committed retirement claim';
END $$;
CREATE TRIGGER server_probe_retired_source_insert_guard BEFORE INSERT ON public.server_probe_retired_sources
FOR EACH ROW EXECUTE FUNCTION public.server_probe_retired_source_insert_guard();
INSERT INTO public.schema_migrations(id) VALUES ('0051_server_probe_retirement_claims') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,96 @@
BEGIN;
ALTER TABLE public.server_probe_retired_sources
DROP CONSTRAINT server_probe_retired_sources_receipt_id_fkey,
ADD CONSTRAINT server_probe_retired_sources_receipt_id_fkey
FOREIGN KEY(receipt_id) REFERENCES public.server_probe_storage_admissions(receipt_id) ON DELETE CASCADE;
CREATE FUNCTION public.server_probe_retired_gc_allowed(target_receipt uuid,current_retention integer) RETURNS boolean
LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
DECLARE activity timestamptz; retained_days integer; safety integer;
BEGIN
IF current_setting('mm.probe_storage_gc',true) IS DISTINCT FROM 'on'
OR current_setting('mm.probe_retired_gc_receipt',true) IS DISTINCT FROM target_receipt::text THEN RETURN false; END IF;
SELECT (r.source->>'activityAt')::timestamptz,a.retention_days,a.safety_ms
INTO activity,retained_days,safety
FROM public.server_probe_retired_sources r
JOIN public.server_probe_storage_admissions a ON a.receipt_id=r.receipt_id AND a.execution_id=r.operation_id
JOIN public.mcp_operation_replay_tombstones t ON t.operation_id=r.operation_id
JOIN public.mcp_connections c ON c.id=t.connection_id AND c.owner_id=a.owner_id
WHERE r.receipt_id=target_receipt AND r.proof=current_setting('mm.probe_retired_gc_proof',true)
AND a.identity_kind='mcp' AND a.cancelled_at IS NULL AND a.claim_token IS NULL AND isfinite(a.claim_attempted_at)
AND t.action_id='servers.probes.run' AND NOT t.target_global
AND r.source->>'planId'=t.plan_id::text AND r.source->>'connectionId'=c.id::text
AND r.source->'serverIds'=to_jsonb(t.target_server_ids) AND r.source->>'ownerId'=a.owner_id
AND r.source->>'bindingProof'=a.binding_proof
AND (r.source->>'authorityExpiresAt')::timestamptz=a.authority_expires_at
AND (r.source->>'gcNotBefore')::timestamptz=a.gc_not_before
AND (r.source->>'retentionDays')::int=a.retention_days AND (r.source->>'safetyMs')::int=a.safety_ms
AND isfinite(a.authority_expires_at) AND isfinite(a.gc_not_before)
AND a.authority_expires_at<clock_timestamp() AND a.gc_not_before<clock_timestamp()
AND NOT EXISTS(SELECT 1 FROM public.mcp_operations WHERE id=t.operation_id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_plans WHERE id=t.plan_id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_operation_steps WHERE operation_id=t.operation_id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_operation_checkpoints WHERE operation_id=t.operation_id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_diagnostic_cancellations WHERE operation_id=t.operation_id)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_execution_quarantines q
WHERE q.receipt_id=r.receipt_id OR(q.identity_kind='mcp' AND q.execution_id=r.operation_id));
IF NOT FOUND OR activity IS NULL OR NOT isfinite(activity)
OR current_retention IS NULL OR current_retention NOT BETWEEN 1 AND 365 THEN RETURN false; END IF;
IF EXISTS(SELECT 1 FROM public.server_probe_executions j WHERE j.receipt_id=target_receipt
AND (j.finished_at IS NULL OR j.finished_at>activity))
OR EXISTS(SELECT 1 FROM public.server_probe_execution_stages s WHERE s.receipt_id=target_receipt
AND GREATEST(s.created_at,COALESCE(s.received_at,s.created_at))>activity)
THEN RETURN false; END IF;
RETURN activity+make_interval(days=>GREATEST(retained_days,current_retention))
+GREATEST(safety,60000)*interval '1 millisecond'<clock_timestamp();
END $$;
CREATE OR REPLACE FUNCTION public.server_probe_retired_source_immutable() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='DELETE' THEN
IF current_setting('mm.probe_storage_gc',true)='on'
AND current_setting('mm.probe_retired_gc_receipt',true)=OLD.receipt_id::text
AND current_setting('mm.probe_retired_gc_proof',true)=OLD.proof
AND NOT EXISTS(SELECT 1 FROM public.server_probe_storage_admissions WHERE receipt_id=OLD.receipt_id)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_executions WHERE receipt_id=OLD.receipt_id)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_execution_payloads WHERE receipt_id=OLD.receipt_id)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_execution_stages WHERE receipt_id=OLD.receipt_id)
THEN RETURN OLD; END IF;
END IF;
RAISE EXCEPTION 'retired diagnostic source is immutable';
END $$;
CREATE OR REPLACE FUNCTION public.server_probe_storage_gc_allowed(id uuid) RETURNS boolean
LANGUAGE plpgsql SET TimeZone='UTC' AS $$
DECLARE a server_probe_storage_admissions%ROWTYPE; activity timestamptz; current_retention integer; terminal_at timestamptz;
BEGIN
IF current_setting('mm.probe_storage_gc',true) IS DISTINCT FROM 'on' THEN RETURN false; END IF;
PERFORM pg_advisory_xact_lock(hashtextextended('server-probe-storage-v2',0));
SELECT * INTO a FROM server_probe_storage_admissions WHERE receipt_id=id FOR UPDATE;
IF NOT FOUND OR a.gc_not_before>=clock_timestamp() OR a.authority_expires_at>=clock_timestamp() THEN RETURN false; END IF;
SELECT settings.requests_retention_days INTO current_retention FROM mcp_settings settings WHERE settings.id=1;
IF current_retention IS NULL OR current_retention NOT BETWEEN 1 AND 365 THEN RETURN false; END IF;
IF EXISTS(SELECT 1 FROM public.server_probe_retired_sources WHERE receipt_id=id) THEN
RETURN public.server_probe_retired_gc_allowed(id,current_retention);
END IF;
terminal_at:=a.created_at;
IF a.identity_kind='mcp' THEN
SELECT GREATEST(o.finished_at,p.expires_at,COALESCE(cancellation.created_at,o.finished_at)) INTO terminal_at
FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN mcp_connections owner_connection ON owner_connection.id=o.connection_id
LEFT JOIN mcp_diagnostic_cancellations cancellation ON cancellation.operation_id=o.id
WHERE o.id=a.execution_id AND owner_connection.owner_id=a.owner_id AND p.operation_id='servers.probes.run'
AND o.state IN ('applied','failed','unknown','rejected','partial','cancelled') AND o.finished_at IS NOT NULL
AND (cancellation.operation_id IS NULL OR(cancellation.connection_id=o.connection_id AND cancellation.actor_id=a.owner_id))
FOR UPDATE OF o,p,owner_connection;
IF NOT FOUND OR terminal_at IS NULL THEN RETURN false; END IF;
END IF;
SELECT GREATEST(a.created_at,a.authority_expires_at,terminal_at,COALESCE(a.cancelled_at,a.created_at),
COALESCE((SELECT finished_at FROM server_probe_executions WHERE receipt_id=id),a.created_at),
COALESCE((SELECT created_at FROM server_probe_execution_quarantines WHERE receipt_id=id),a.created_at),
COALESCE((SELECT max(GREATEST(created_at,COALESCE(received_at,created_at))) FROM server_probe_execution_stages WHERE receipt_id=id),a.created_at)) INTO activity;
RETURN activity+make_interval(days=>GREATEST(a.retention_days,current_retention))+a.safety_ms*interval '1 millisecond'<clock_timestamp();
END $$;
INSERT INTO public.schema_migrations(id) VALUES ('0052_server_probe_retired_gc') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,74 @@
BEGIN;
CREATE TABLE public.server_probe_terminal_proofs (
operation_id uuid PRIMARY KEY REFERENCES public.mcp_operations(id),
receipt_id uuid NOT NULL UNIQUE,
source jsonb NOT NULL CHECK(jsonb_typeof(source)='object' AND octet_length(source::text) BETWEEN 1 AND 8192),
source_proof text NOT NULL CHECK(source_proof ~ '^[a-f0-9]{64}$'),
idempotency_digest text NOT NULL CHECK(idempotency_digest ~ '^[a-f0-9]{64}$'),
capture_proof text NOT NULL CHECK(capture_proof ~ '^[a-f0-9]{64}$'),
CHECK((source->>'version') IS NOT DISTINCT FROM '1'
AND (source->>'executionId') IS NOT DISTINCT FROM operation_id::text
AND (source->>'receiptId') IS NOT DISTINCT FROM receipt_id::text)
);
CREATE FUNCTION public.server_probe_terminal_proof_guard() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='INSERT' THEN
IF EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_connections c ON c.id=o.connection_id
JOIN public.server_probe_storage_admissions a ON a.execution_id=o.id AND a.identity_kind='mcp' AND a.owner_id=c.owner_id
JOIN public.server_probe_executions j ON j.receipt_id=a.receipt_id
JOIN public.server_probe_execution_payloads b ON b.receipt_id=a.receipt_id AND b.receipt_proof=j.receipt_proof
WHERE o.id=NEW.operation_id AND a.receipt_id=NEW.receipt_id AND a.purged_at IS NULL AND a.cancelled_at IS NULL
AND a.claim_token IS NULL AND a.claim_attempted_at IS NOT NULL
AND o.state='applied' AND o.verification='inconclusive' AND o.error_code IS NULL
AND p.state='completed' AND p.operation_id='servers.probes.run' AND p.operation_version=1
AND j.state='completed' AND j.execution_id=o.id AND j.mcp_connection_id=c.id AND j.actor_id=c.owner_id
AND NEW.source->>'connectionId'=c.id::text AND NEW.source->>'planId'=p.id::text
AND NEW.source->>'ownerId'=a.owner_id AND NEW.source->>'bindingProof'=a.binding_proof
AND NEW.source->>'receiptProof'=b.receipt_proof
AND NEW.source->>'ciphertextSha256'=encode(sha256(convert_to(b.receipt_ciphertext,'UTF8')),'hex')
) THEN RETURN NEW; END IF;
ELSIF TG_OP='DELETE' THEN
IF EXISTS(
SELECT 1 FROM public.server_probe_retirement_claims r
JOIN public.mcp_operation_replay_tombstones t ON t.operation_id=r.operation_id AND t.plan_id=r.plan_id AND t.connection_id=r.connection_id
WHERE r.operation_id=OLD.operation_id AND r.receipt_id=OLD.receipt_id AND r.source_proof=OLD.source_proof
AND r.claim_xid<>pg_current_xact_id() AND r.claim_token::text=current_setting('mm.probe_retirement_claim',true)
AND t.action_id='servers.probes.run' AND NOT t.target_global AND t.idempotency_digest=OLD.idempotency_digest
AND OLD.source->'serverIds'=to_jsonb(t.target_server_ids)
) THEN RETURN OLD; END IF;
END IF;
RAISE EXCEPTION 'diagnostic terminal proof must match its original source or exact retirement claim';
END $$;
CREATE TRIGGER server_probe_terminal_proof_guard BEFORE INSERT OR UPDATE OR DELETE
ON public.server_probe_terminal_proofs FOR EACH ROW EXECUTE FUNCTION public.server_probe_terminal_proof_guard();
CREATE TRIGGER server_probe_terminal_proof_no_truncate BEFORE TRUNCATE
ON public.server_probe_terminal_proofs FOR EACH STATEMENT EXECUTE FUNCTION public.server_probe_terminal_proof_guard();
CREATE OR REPLACE FUNCTION public.server_probe_retirement_claim_guard() RETURNS trigger
LANGUAGE plpgsql SET search_path=pg_catalog,public SET TimeZone='UTC' AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_connections c ON c.id=o.connection_id
WHERE o.id=NEW.operation_id AND p.id=NEW.plan_id AND c.id=NEW.connection_id
AND o.state='applied' AND o.verification='inconclusive' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND p.operation_id='servers.probes.run' AND p.operation_version=1 AND p.state='completed'
AND (EXISTS(SELECT 1 FROM public.server_probe_storage_admissions a
WHERE a.receipt_id=NEW.receipt_id AND a.execution_id=o.id AND a.identity_kind='mcp' AND a.owner_id=c.owner_id
AND a.claim_token IS NULL AND a.claim_attempted_at IS NOT NULL AND a.cancelled_at IS NULL AND a.purged_at IS NULL)
OR(EXISTS(SELECT 1 FROM public.server_probe_terminal_proofs f
WHERE f.operation_id=o.id AND f.receipt_id=NEW.receipt_id AND f.source_proof=NEW.source_proof
AND f.source->>'ownerId'=c.owner_id AND f.source->>'connectionId'=c.id::text AND f.source->>'planId'=p.id::text)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_storage_admissions a
WHERE a.receipt_id=NEW.receipt_id OR(a.identity_kind='mcp' AND a.execution_id=o.id))))
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'diagnostic retirement claim cannot be replaced or reopened';
END $$;
INSERT INTO public.schema_migrations(id) VALUES ('0053_server_probe_terminal_proofs') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,18 @@
BEGIN;
CREATE OR REPLACE FUNCTION public.mcp_ordinary_retirement_claim_immutable() RETURNS trigger
LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.mcp_operations o JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
WHERE o.id=NEW.operation_id AND o.plan_id=NEW.plan_id AND o.connection_id=NEW.connection_id
AND o.state='applied' AND o.verification='passed' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND (p.operation_id,p.operation_version) IN (
('logs.settings.update',1),('traffic.settings.update',2),('uptime.settings.update',2),
('servers.api-ping.settings.update',2),('internet-path.settings.update',1),('geoip.settings.update',1)
) AND p.state='completed'
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'ordinary operation retirement claim cannot be replaced or reopened';
END $$;
INSERT INTO public.schema_migrations(id) VALUES ('0054_mcp_geoip_retirement') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,18 @@
BEGIN;
CREATE OR REPLACE FUNCTION public.mcp_ordinary_retirement_claim_immutable() RETURNS trigger
LANGUAGE plpgsql SET TimeZone='UTC' SET search_path=pg_catalog,public AS $$
BEGIN
IF TG_OP='INSERT' AND NEW.claim_xid=pg_current_xact_id() AND EXISTS(
SELECT 1 FROM public.mcp_operations o JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
WHERE o.id=NEW.operation_id AND o.plan_id=NEW.plan_id AND o.connection_id=NEW.connection_id
AND o.state='applied' AND o.verification='passed' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND (p.operation_id,p.operation_version) IN (
('logs.settings.update',1),('traffic.settings.update',2),('uptime.settings.update',2),
('servers.api-ping.settings.update',2),('internet-path.settings.update',1),('geoip.settings.update',1),('backups.schedule.update',1)
) AND p.state='completed'
) THEN RETURN NEW; END IF;
RAISE EXCEPTION 'ordinary operation retirement claim cannot be replaced or reopened';
END $$;
INSERT INTO public.schema_migrations(id) VALUES ('0055_mcp_backup_schedule_retirement') ON CONFLICT(id) DO NOTHING;
COMMIT;
@@ -0,0 +1,43 @@
-- Keep the live charge and intent fences in sync with recovery action v8.
-- Existing authenticated v6/v7 history remains unchanged.
BEGIN;
CREATE OR REPLACE FUNCTION public.mcp_routing_manual_inverse_charge_source() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_routing_profiles r ON r.id=NEW.profile_id AND r.connection_id=o.connection_id
JOIN public.mcp_operations source ON source.id=NEW.source_operation_id AND source.connection_id=o.connection_id
JOIN public.mcp_plans sp ON sp.id=source.plan_id AND sp.connection_id=source.connection_id
JOIN public.mcp_connections c ON c.id=o.connection_id
WHERE o.id=NEW.recovery_operation_id AND p.id=NEW.recovery_plan_id
AND o.connection_id=NEW.connection_id AND c.owner_id=NEW.owner_id
AND r.version=NEW.profile_version AND o.state='pending' AND p.state='executing'
AND p.operation_id='operations.recover' AND p.operation_version=8
AND sp.id=NEW.source_plan_id AND sp.operation_id='routing.community.switch' AND sp.operation_version=7
AND source.state IN('applied','partial')
) THEN RAISE EXCEPTION 'manual inverse charge differs from pending admission'; END IF;
RETURN NEW;
END $$;
CREATE OR REPLACE FUNCTION public.mcp_scoped_recovery_manual_version() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NEW.mode='manual' AND NOT pg_try_advisory_xact_lock(hashtextextended('mcp-runner-admission',0))
THEN RAISE EXCEPTION 'manual intent admission snapshot busy'; END IF;
IF NEW.mode='manual' AND NOT EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=0
JOIN public.mcp_operations source ON source.id=NEW.source_operation_id AND source.connection_id=o.connection_id
WHERE o.id=NEW.recovery_operation_id AND o.state='running'
AND p.state='executing' AND p.operation_id='operations.recover' AND p.operation_version=8
AND NEW.source_step_index=0 AND s.state='intent'
AND s.intended_state->>'kind'='service'
AND s.intended_state->>'service'='routing.community.scoped.recover'
AND s.intended_state->'input'->>'mode'='manual'
AND s.intended_state->'input'->>'sourceOperationId'=NEW.source_operation_id::text
AND s.intended_state->'input'->>'receiptId'=NEW.recovery_receipt_id::text
) THEN RAISE EXCEPTION 'manual inverse requires current exact running recovery'; END IF;
RETURN NEW;
END $$;
INSERT INTO public.schema_migrations(id) VALUES('0056_mcp_manual_inverse_recovery_v8') ON CONFLICT(id) DO NOTHING;
COMMIT;
+21
View File
@@ -576,6 +576,13 @@
"operations": [], "operations": [],
"reason": "Управление самим MCP, транспорт или машинная авторизация: отдельного AI-инструмента нет." "reason": "Управление самим MCP, транспорт или машинная авторизация: отдельного AI-инструмента нет."
}, },
{
"method": "GET",
"path": "/api/mcp/database/receipts/:id/audit-status",
"status": "excluded",
"operations": [],
"reason": "Человеческая административная сверка аудита внешней квитанции восстановления: управление самим MCP, без отдельного AI-инструмента."
},
{ {
"method": "GET", "method": "GET",
"path": "/api/mcp/devices/:serverId/:resource", "path": "/api/mcp/devices/:serverId/:resource",
@@ -590,6 +597,13 @@
"operations": [], "operations": [],
"reason": "Управление самим MCP, транспорт или машинная авторизация: отдельного AI-инструмента нет." "reason": "Управление самим MCP, транспорт или машинная авторизация: отдельного AI-инструмента нет."
}, },
{
"method": "GET",
"path": "/api/mcp/devices/:serverId/bridge-vlan/configuration",
"status": "mediated",
"operations": ["network.bridge-vlan.read"],
"reason": "Чтение связанной конфигурации bridge/VLAN через общую типизированную модель и проверку доступа MCP."
},
{ {
"method": "GET", "method": "GET",
"path": "/api/mcp/drift", "path": "/api/mcp/drift",
@@ -1604,6 +1618,13 @@
"operations": [], "operations": [],
"reason": "Управление самим MCP, транспорт или машинная авторизация: отдельного AI-инструмента нет." "reason": "Управление самим MCP, транспорт или машинная авторизация: отдельного AI-инструмента нет."
}, },
{
"method": "POST",
"path": "/api/mcp/devices/:serverId/bridge-vlan/configuration/plan",
"status": "mediated",
"operations": ["network.bridge-vlan.configure"],
"reason": "Подготовка связанного плана bridge/VLAN через тот же MCP-инструмент; выполнение требует человеческого подтверждения."
},
{ {
"method": "POST", "method": "POST",
"path": "/api/mcp/management-channels", "path": "/api/mcp/management-channels",
+8 -5
View File
@@ -12,6 +12,7 @@
"test:traffic-flow:rebuild:pg": "node scripts/test-mcp-postgres.mjs --flow-rebuild", "test:traffic-flow:rebuild:pg": "node scripts/test-mcp-postgres.mjs --flow-rebuild",
"test:traffic-flow:pg": "node scripts/test-mcp-postgres.mjs --flow-writes", "test:traffic-flow:pg": "node scripts/test-mcp-postgres.mjs --flow-writes",
"test:traffic-flow:quality:pg": "node scripts/test-mcp-postgres.mjs --flow-quality", "test:traffic-flow:quality:pg": "node scripts/test-mcp-postgres.mjs --flow-quality",
"test:traffic-flow:chr-evidence": "tsx --test src/test/traffic-flow-chr-capture-check.test.ts src/test/traffic-flow-chr-wire.test.ts src/test/traffic-flow-chr-target-harness.test.ts src/test/traffic-flow-chr-owned-sequence.test.ts src/test/traffic-flow-chr-reconcile.test.ts",
"test:mcp:pg:all": "node scripts/test-mcp-postgres.mjs --all", "test:mcp:pg:all": "node scripts/test-mcp-postgres.mjs --all",
"mcp:stdio": "node dist/scripts/mcp-stdio.js", "mcp:stdio": "node dist/scripts/mcp-stdio.js",
"mcp:stdio:dev": "tsx src/scripts/mcp-stdio.ts", "mcp:stdio:dev": "tsx src/scripts/mcp-stdio.ts",
@@ -34,7 +35,7 @@
"test:exec": "tsx src/services/mikrotik-logs.test.ts && tsx src/services/terminal-diagnostics.test.ts && tsx src/routes/exec.test.ts && tsx src/plugins/auth.exec.test.ts", "test:exec": "tsx src/services/mikrotik-logs.test.ts && tsx src/services/terminal-diagnostics.test.ts && tsx src/routes/exec.test.ts && tsx src/plugins/auth.exec.test.ts",
"test:auth": "tsx --test src/modules/mcp/routing-manual-inverse-history.test.ts && tsx src/db/application-writer-profile-registry-structure.test.ts && tsx src/db/application-writer-profile-registry-read.test.ts && tsx src/db/application-writer-profile-registry-read-parity.test.ts && tsx src/db/application-writer-profile-format.test.ts && tsx src/plugins/auth-path.test.ts && tsx src/lib/permissions.test.ts && tsx src/plugins/auth-provenance.test.ts && tsx src/services/bandwidth-rest-speed-original.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts && npm run test:mcp", "test:auth": "tsx --test src/modules/mcp/routing-manual-inverse-history.test.ts && tsx src/db/application-writer-profile-registry-structure.test.ts && tsx src/db/application-writer-profile-registry-read.test.ts && tsx src/db/application-writer-profile-registry-read-parity.test.ts && tsx src/db/application-writer-profile-format.test.ts && tsx src/plugins/auth-path.test.ts && tsx src/lib/permissions.test.ts && tsx src/plugins/auth-provenance.test.ts && tsx src/services/bandwidth-rest-speed-original.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts && npm run test:mcp",
"test:mcp": "tsx --test src/services/server-poll-error-provenance.test.ts && tsx --test src/modules/mcp/plan-expiry.test.ts src/modules/mcp/result-budget.test.ts src/modules/mcp/result-delivery.test.ts src/modules/mcp/wire-admission.test.ts src/modules/mcp/flow-envelope-allowance.test.ts src/modules/mcp/sdk-terminal-budget.test.ts src/modules/mcp/inflight-owner.test.ts && npm run test:database-source-binding && tsx --test src/modules/mcp/integration-fixture-expiry.test.ts && npm run test:mcp:operation-origin && npm run test:mcp:service-context && npm run test:mcp:operation-locks && tsx --test src/modules/mcp/routing-community-public.test.ts src/modules/mcp/management-transport.test.ts src/modules/mcp/routing-community-context-model.test.ts src/modules/mcp/routing-community-context-native.test.ts ../packages/contracts/src/mcp-routing.test.ts && tsx src/modules/mcp/human-artifact-receipts.test.ts && tsx src/modules/mcp/chr-overlay-readiness.test.ts && tsx src/modules/mcp/chr-recovery-source.test.ts && tsx src/modules/mcp/chr-native-failure.test.ts && tsx src/modules/mcp/ospf-owner-graph.test.ts && tsx src/modules/mcp/routing-measurement-address.test.ts && tsx src/modules/mcp/routing-observation-source.test.ts && tsx src/services/traffic-flow-talkers-model.test.ts && tsx src/services/bgp-prefix-model.test.ts && tsx src/services/vrf-members.test.ts && tsx src/services/routing-gateway-reference.test.ts && tsx src/services/bgp-export-model.test.ts && tsx src/services/traffic-history-model.test.ts && tsx src/services/internet-path-snapshot.test.ts && tsx src/services/internet-path-observation.test.ts && tsx src/modules/mcp/coverage.test.ts && tsx src/modules/mcp/policy.test.ts && tsx src/modules/mcp/events-batch-model.test.ts && tsx src/modules/mcp/operation-revisions.test.ts && tsx src/modules/mcp/native-create-identity.test.ts && tsx src/modules/mcp/ros-step-verification.test.ts && tsx src/modules/mcp/ros-prefix.test.ts && tsx src/modules/mcp/native-conservation-http.test.ts && tsx src/modules/mcp/security.test.ts && tsx src/modules/mcp/state-file-publish.test.ts && tsx src/modules/mcp/routing-model.test.ts && tsx src/modules/mcp/routing-observations-model.test.ts && tsx src/modules/mcp/ospf-health.test.ts && tsx src/modules/mcp/ecmp-preflight.test.ts && tsx src/modules/mcp/recursive-recovery.test.ts && tsx src/modules/mcp/control-path-model.test.ts && tsx src/modules/mcp/routing-community-scoped-model.test.ts && tsx src/modules/mcp/routing-community-scoped-native.test.ts && tsx src/modules/mcp/path-verification.test.ts && tsx src/modules/mcp/database-maintenance.test.ts && tsx src/routes/mcp.test.ts && tsx ../lib/auth-mcp.test.ts && tsx --test src/modules/mcp/routing-controller-fence.test.ts && tsx src/test/chr-vrf-companion-proof.test.ts && tsx src/test/chr-vrf-companion-telemetry.test.ts && tsx src/test/chr-vrf-companion-address-space.test.ts && tsx src/modules/mcp/flow-facts-rebuild-actions.test.ts && tsx src/modules/mcp/ipsec-peer-create-dependencies.test.ts && tsx src/services/ipsec-certificate-update-model.test.ts && tsx src/modules/mcp/ipsec-native-dispatch.test.ts && tsx src/modules/mcp/ipsec-user-update-actions.test.ts && tsx src/modules/mcp/ipsec-server-delete-actions.test.ts && tsx src/services/ipsec-proof-telemetry.test.ts && tsx src/services/vrf-production-admission.test.ts && tsx src/modules/mcp/flow-host-keys-actions.test.ts && tsx src/services/vrf-production-read.test.ts && tsx src/modules/mcp/ipsec-user-create-actions.test.ts && tsx src/modules/mcp/flow-settings-actions.test.ts && tsx src/services/vrf-production-ticket-store.test.ts && npm run test:mcp:routing-baseline", "test:mcp": "tsx --test src/services/server-poll-error-provenance.test.ts && tsx --test src/modules/mcp/plan-expiry.test.ts src/modules/mcp/result-budget.test.ts src/modules/mcp/result-delivery.test.ts src/modules/mcp/wire-admission.test.ts src/modules/mcp/flow-envelope-allowance.test.ts src/modules/mcp/sdk-terminal-budget.test.ts src/modules/mcp/inflight-owner.test.ts && npm run test:database-source-binding && tsx --test src/modules/mcp/integration-fixture-expiry.test.ts && npm run test:mcp:operation-origin && npm run test:mcp:service-context && npm run test:mcp:operation-locks && tsx --test src/modules/mcp/routing-community-public.test.ts src/modules/mcp/management-transport.test.ts src/modules/mcp/routing-community-context-model.test.ts src/modules/mcp/routing-community-context-native.test.ts ../packages/contracts/src/mcp-routing.test.ts && tsx src/modules/mcp/human-artifact-receipts.test.ts && tsx src/modules/mcp/chr-overlay-readiness.test.ts && tsx src/modules/mcp/chr-recovery-source.test.ts && tsx src/modules/mcp/chr-native-failure.test.ts && tsx src/modules/mcp/ospf-owner-graph.test.ts && tsx src/modules/mcp/routing-measurement-address.test.ts && tsx src/modules/mcp/routing-observation-source.test.ts && tsx src/services/traffic-flow-talkers-model.test.ts && tsx src/services/bgp-prefix-model.test.ts && tsx src/services/vrf-members.test.ts && tsx src/services/routing-gateway-reference.test.ts && tsx src/services/bgp-export-model.test.ts && tsx src/services/traffic-history-model.test.ts && tsx src/services/internet-path-snapshot.test.ts && tsx src/services/internet-path-observation.test.ts && tsx src/modules/mcp/coverage.test.ts && tsx src/modules/mcp/policy.test.ts && tsx src/modules/mcp/events-batch-model.test.ts && tsx src/modules/mcp/operation-revisions.test.ts && tsx src/modules/mcp/native-create-identity.test.ts && tsx src/modules/mcp/ros-step-verification.test.ts && tsx src/modules/mcp/ros-prefix.test.ts && tsx src/modules/mcp/native-conservation-http.test.ts && tsx src/modules/mcp/security.test.ts && tsx src/modules/mcp/state-file-publish.test.ts && tsx src/modules/mcp/routing-model.test.ts && tsx src/modules/mcp/routing-observations-model.test.ts && tsx src/modules/mcp/ospf-health.test.ts && tsx src/modules/mcp/ecmp-preflight.test.ts && tsx src/modules/mcp/recursive-recovery.test.ts && tsx src/modules/mcp/control-path-model.test.ts && tsx src/modules/mcp/routing-community-scoped-model.test.ts && tsx src/modules/mcp/routing-community-scoped-native.test.ts && tsx src/modules/mcp/path-verification.test.ts && tsx src/modules/mcp/database-maintenance.test.ts && tsx src/routes/mcp.test.ts && tsx ../lib/auth-mcp.test.ts && tsx --test src/modules/mcp/routing-controller-fence.test.ts && tsx src/test/chr-vrf-companion-proof.test.ts && tsx src/test/chr-vrf-companion-telemetry.test.ts && tsx src/test/chr-vrf-companion-address-space.test.ts && tsx src/modules/mcp/flow-facts-rebuild-actions.test.ts && tsx src/modules/mcp/ipsec-peer-create-dependencies.test.ts && tsx src/services/ipsec-certificate-update-model.test.ts && tsx src/modules/mcp/ipsec-native-dispatch.test.ts && tsx src/modules/mcp/ipsec-user-update-actions.test.ts && tsx src/modules/mcp/ipsec-server-delete-actions.test.ts && tsx src/services/ipsec-proof-telemetry.test.ts && tsx src/services/vrf-production-admission.test.ts && tsx src/modules/mcp/flow-host-keys-actions.test.ts && tsx src/services/vrf-production-read.test.ts && tsx src/modules/mcp/ipsec-user-create-actions.test.ts && tsx src/modules/mcp/flow-settings-actions.test.ts && tsx src/services/vrf-production-ticket-store.test.ts && npm run test:mcp:routing-baseline",
"test:wireguard": "tsx src/services/mikrotik-response-bounds.test.ts && tsx src/services/wireguard-interface-update.test.ts && tsx src/services/wireguard-peer-update.test.ts && npx tsx src/services/wireguard-config.test.ts && npx tsx src/services/wireguard-ipv6.test.ts && npx tsx --test src/services/wireguard-import.test.ts src/services/wireguard-import-apply.test.ts src/services/wireguard-import-revision.test.ts src/services/wireguard-delete.test.ts && npx tsx src/routes/wireguard-import.test.ts && tsx src/services/wireguard-interface-create.test.ts && tsx src/services/wireguard-peer-create.test.ts && tsx src/services/wireguard-import-plan.test.ts && tsx src/services/wireguard-import-execution.test.ts && tsx src/modules/mcp/wireguard-import-actions.test.ts && tsx --test src/services/wireguard-export.test.ts", "test:wireguard": "tsx src/services/mikrotik-response-bounds.test.ts && tsx --test src/services/wireguard-dependency-read.test.ts && tsx src/services/wireguard-interface-update.test.ts && tsx src/services/wireguard-peer-update.test.ts && npx tsx src/services/wireguard-config.test.ts && npx tsx src/services/wireguard-ipv6.test.ts && npx tsx --test src/services/wireguard-import.test.ts src/services/wireguard-import-apply.test.ts src/services/wireguard-import-revision.test.ts src/services/wireguard-delete.test.ts && npx tsx src/routes/wireguard-import.test.ts && tsx src/services/wireguard-interface-create.test.ts && tsx src/services/wireguard-peer-create.test.ts && tsx src/services/wireguard-import-plan.test.ts && tsx src/services/wireguard-import-execution.test.ts && tsx src/modules/mcp/wireguard-import-actions.test.ts && tsx --test src/services/wireguard-export.test.ts",
"test:traffic-rate": "tsx src/services/traffic-rate.test.ts && tsx src/services/traffic-observation.test.ts && tsx src/services/traffic-freshness.test.ts && tsx src/services/traffic-live.test.ts && tsx src/services/traffic-observation-sql.test.ts && tsx src/services/traffic-history-sql.test.ts && tsx src/services/traffic-live-sql.test.ts && tsx src/services/traffic-volume-sql.test.ts && tsx src/services/traffic-binding-status.test.ts && tsx src/services/traffic-status-sql.test.ts && tsx src/services/traffic-gap.test.ts && tsx src/services/traffic-identity.test.ts && tsx src/services/traffic-membership.test.ts && tsx src/services/traffic-aggregate-peak.test.ts && tsx src/services/traffic-users-cache.test.ts && tsx src/services/traffic-collection-lock.test.ts && tsx src/services/traffic-concurrency-sql.test.ts", "test:traffic-rate": "tsx src/services/traffic-rate.test.ts && tsx src/services/traffic-observation.test.ts && tsx src/services/traffic-freshness.test.ts && tsx src/services/traffic-live.test.ts && tsx src/services/traffic-observation-sql.test.ts && tsx src/services/traffic-history-sql.test.ts && tsx src/services/traffic-live-sql.test.ts && tsx src/services/traffic-volume-sql.test.ts && tsx src/services/traffic-binding-status.test.ts && tsx src/services/traffic-status-sql.test.ts && tsx src/services/traffic-gap.test.ts && tsx src/services/traffic-identity.test.ts && tsx src/services/traffic-membership.test.ts && tsx src/services/traffic-aggregate-peak.test.ts && tsx src/services/traffic-users-cache.test.ts && tsx src/services/traffic-collection-lock.test.ts && tsx src/services/traffic-concurrency-sql.test.ts",
"test:traffic-flow": "tsx --test src/services/traffic-flow-heartbeat.test.ts src/services/traffic-flow-heartbeat-wiring.test.ts src/services/traffic-flow-heartbeat-receiver.test.ts src/services/traffic-flow-ingest-quality.test.ts src/services/traffic-flow-quality-contract.test.ts && tsx src/services/traffic-flow-catalog-read.test.ts && tsx src/routes/traffic-flow-query.test.ts && tsx src/services/traffic-flow-month.test.ts && tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-ipv6-pipeline.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/geoip-download.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-fact-accumulator.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts && tsx src/services/geoip-manual-update.test.ts && tsx src/services/traffic-flow-overlay-model.test.ts && npm run test:traffic-flow:ownership && tsx src/services/traffic-flow-worker-platform.test.ts && tsx src/services/traffic-flow-template-cache.test.ts && tsx --test src/services/traffic-flow-template-lifetime.test.ts && tsx src/services/traffic-flow-protocol-scope.test.ts && npm run test:traffic-flow:read-budget && npm run test:traffic-flow:resource-profile", "test:traffic-flow": "tsx --test src/services/traffic-flow-heartbeat.test.ts src/services/traffic-flow-heartbeat-wiring.test.ts src/services/traffic-flow-heartbeat-receiver.test.ts src/services/traffic-flow-ingest-quality.test.ts src/services/traffic-flow-quality-contract.test.ts && tsx src/services/traffic-flow-catalog-read.test.ts && tsx src/routes/traffic-flow-query.test.ts && tsx src/services/traffic-flow-month.test.ts && tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-ipv6-pipeline.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/geoip-download.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-fact-accumulator.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts && tsx src/services/geoip-manual-update.test.ts && tsx src/services/traffic-flow-overlay-model.test.ts && npm run test:traffic-flow:ownership && tsx src/services/traffic-flow-worker-platform.test.ts && tsx src/services/traffic-flow-template-cache.test.ts && tsx --test src/services/traffic-flow-template-lifetime.test.ts && tsx src/services/traffic-flow-protocol-scope.test.ts && npm run test:traffic-flow:read-budget && npm run test:traffic-flow:resource-profile",
"test:users": "tsx ../packages/contracts/src/users.test.ts && tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts", "test:users": "tsx ../packages/contracts/src/users.test.ts && tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts",
@@ -54,7 +55,7 @@
"test:ospf-optimizer": "tsx src/services/ospf-cost-optimization.test.ts && tsx src/modules/mcp/ospf-legacy-source.test.ts && tsx src/services/ospf-legacy-optimization.test.ts && tsx src/services/ospf-template-flags.test.ts && tsx src/services/ospf-route-proof.test.ts && tsx src/services/ospf-link-catalog.test.ts && tsx src/services/ospf-identity-catalog.test.ts && tsx src/services/ospf-template-cost-write.test.ts && tsx src/services/ospf-template-cost-http.test.ts && tsx src/services/ospf-optimizer-identity.test.ts && tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/services/ospf-effective-disabled.test.ts && tsx src/routes/ospf-optimizer.test.ts", "test:ospf-optimizer": "tsx src/services/ospf-cost-optimization.test.ts && tsx src/modules/mcp/ospf-legacy-source.test.ts && tsx src/services/ospf-legacy-optimization.test.ts && tsx src/services/ospf-template-flags.test.ts && tsx src/services/ospf-route-proof.test.ts && tsx src/services/ospf-link-catalog.test.ts && tsx src/services/ospf-identity-catalog.test.ts && tsx src/services/ospf-template-cost-write.test.ts && tsx src/services/ospf-template-cost-http.test.ts && tsx src/services/ospf-optimizer-identity.test.ts && tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/services/ospf-effective-disabled.test.ts && tsx src/routes/ospf-optimizer.test.ts",
"test:containers": "tsx src/services/containers-live.test.ts && tsx src/services/containers-state.test.ts && tsx src/services/container-removal.test.ts && tsx src/routes/containers-state.test.ts", "test:containers": "tsx src/services/containers-live.test.ts && tsx src/services/containers-state.test.ts && tsx src/services/container-removal.test.ts && tsx src/routes/containers-state.test.ts",
"test:certificates": "tsx src/services/certificate-installation.test.ts && tsx src/services/certificate-renew-collector.test.ts && tsx src/services/certificate-issue-execution.test.ts && tsx src/services/certificate-issue-request.test.ts && tsx src/routes/certificates-issue.test.ts && tsx src/services/certificate-issue-dto.test.ts && tsx src/services/certificate-parse.test.ts && tsx src/services/certificates-state.test.ts && tsx src/routes/certificates-state.test.ts && tsx src/services/acme-settings-test.test.ts && tsx src/services/acme-settings-update-model.test.ts && tsx src/modules/mcp/acme-settings-actions.test.ts", "test:certificates": "tsx src/services/certificate-installation.test.ts && tsx src/services/certificate-renew-collector.test.ts && tsx src/services/certificate-issue-execution.test.ts && tsx src/services/certificate-issue-request.test.ts && tsx src/routes/certificates-issue.test.ts && tsx src/services/certificate-issue-dto.test.ts && tsx src/services/certificate-parse.test.ts && tsx src/services/certificates-state.test.ts && tsx src/routes/certificates-state.test.ts && tsx src/services/acme-settings-test.test.ts && tsx src/services/acme-settings-update-model.test.ts && tsx src/modules/mcp/acme-settings-actions.test.ts",
"test:system-database": "tsx src/services/bounded-database-dump.test.ts && tsx src/services/database-dump-connection.test.ts && tsx src/services/system-database-partition-restore.test.ts && tsx src/services/system-database-cache.test.ts && tsx src/services/database-background-work.test.ts && tsx src/services/database-request-gate.test.ts && tsx src/services/traffic-flow-worker-shutdown.test.ts && tsx src/services/system-database-restore-execution.test.ts && tsx src/services/system-database-operation.test.ts && tsx src/services/system-database-schema-readiness.test.ts && tsx src/routes/system-database.test.ts && tsx src/plugins/auth.system-database.test.ts && tsx src/services/database-control-archive-boundary.test.ts && tsx src/db/database-schema-control-model.test.ts && tsx src/db/database-schema-control.test.ts && tsx src/db/controlled-migrations.test.ts && tsx src/db/controlled-schema-verification.test.ts && tsx --test src/db/migration-data-inventory.test.ts", "test:system-database": "tsx src/services/bounded-database-dump.test.ts && tsx src/services/database-dump-connection.test.ts && tsx src/services/system-database-partition-restore.test.ts && tsx src/services/system-database-cache.test.ts && tsx src/services/database-background-work.test.ts && tsx src/services/database-request-gate.test.ts && tsx --test src/services/detached-database-admission.test.ts && tsx src/services/traffic-flow-worker-shutdown.test.ts && tsx src/services/system-database-restore-execution.test.ts && tsx src/services/system-database-operation.test.ts && tsx src/services/system-database-schema-readiness.test.ts && tsx --test src/routes/mcp-oauth-database-admission.test.ts && tsx src/routes/system-database.test.ts && tsx src/plugins/auth.system-database.test.ts && tsx src/services/database-control-archive-boundary.test.ts && tsx src/db/database-schema-control-model.test.ts && tsx src/db/database-schema-control.test.ts && tsx src/db/controlled-migrations.test.ts && tsx src/db/controlled-schema-verification.test.ts && tsx --test src/db/migration-data-inventory.test.ts && tsx --test src/services/database-maintenance-startup.test.ts && tsx --test src/services/database-restore-audit.test.ts && tsx --test src/services/database-maintenance-resolution.test.ts && tsx --test scripts/database-maintenance-resolution.test.mts",
"test:all": "npm run test:logs && npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:alerts-telegram && npm run test:auth && npm run test:wireguard && npm run test:ipsec && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:sidebar-counts && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge && npm run test:evolution && npm run test:system-database && npm run test:server-privacy && npm run test:mcp:module-completion && npm run test:mcp:loopback-agent && npm run test:server-probe-management && npm run test:server-poll-management && npm run test:mcp:routing-authority && npm run test:mcp:native-readers && npm run test:mcp:chr-acquisition && npm run test:mcp:shared-context && npm run test:mcp:telemetry-configuration", "test:all": "npm run test:logs && npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:alerts-telegram && npm run test:auth && npm run test:wireguard && npm run test:ipsec && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:sidebar-counts && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge && npm run test:evolution && npm run test:system-database && npm run test:server-privacy && npm run test:mcp:module-completion && npm run test:mcp:loopback-agent && npm run test:server-probe-management && npm run test:server-poll-management && npm run test:mcp:routing-authority && npm run test:mcp:native-readers && npm run test:mcp:chr-acquisition && npm run test:mcp:shared-context && npm run test:mcp:telemetry-configuration",
"test:server-privacy": "tsx ../packages/contracts/src/servers.test.ts && tsx src/routes/server-secrets-sql.test.ts && tsx --test src/services/server-connection-test.test.ts", "test:server-privacy": "tsx ../packages/contracts/src/servers.test.ts && tsx src/routes/server-secrets-sql.test.ts && tsx --test src/services/server-connection-test.test.ts",
"test:mcp:flow-rebuild:pg": "node scripts/test-mcp-postgres.mjs --flow-rebuild-mcp", "test:mcp:flow-rebuild:pg": "node scripts/test-mcp-postgres.mjs --flow-rebuild-mcp",
@@ -65,7 +66,7 @@
"test:mcp:ipsec-export-journal:pg": "node scripts/test-mcp-postgres.mjs --ipsec-export-journal --restore", "test:mcp:ipsec-export-journal:pg": "node scripts/test-mcp-postgres.mjs --ipsec-export-journal --restore",
"test:mcp:ipsec-export:http:pg": "node scripts/test-mcp-postgres.mjs --ipsec-export-http --restore", "test:mcp:ipsec-export:http:pg": "node scripts/test-mcp-postgres.mjs --ipsec-export-http --restore",
"test:mcp:diagnostic-cancel-host:pg": "node scripts/test-mcp-postgres.mjs --diagnostic-cancel-host --restore", "test:mcp:diagnostic-cancel-host:pg": "node scripts/test-mcp-postgres.mjs --diagnostic-cancel-host --restore",
"test:server-probe-management": "tsx src/services/server-probe-authority.test.ts && tsx src/services/server-probe-public-result.test.ts && tsx src/services/server-probe-execution.test.ts && tsx src/services/server-probe-rest-contract.test.ts && tsx src/services/server-probe-rest-execution.test.ts && tsx src/services/server-probe-journal.test.ts && tsx src/services/server-probe-journal-history.test.ts && tsx src/modules/mcp/server-probe-actions.test.ts && tsx src/modules/mcp/diagnostic-cancellation.test.ts && tsx src/modules/mcp/service-diagnostic-progress.test.ts && tsx src/services/mikrotik-get-admission.test.ts && tsx src/services/server-probe-storage-journal-adapter.test.ts && tsx --test src/services/server-probe-storage-history.test.ts && npm run test:server-probe-storage-models", "test:server-probe-management": "tsx src/services/server-probe-authority.test.ts && tsx src/services/server-probe-public-result.test.ts && tsx src/services/server-probe-execution.test.ts && tsx src/services/server-probe-rest-contract.test.ts && tsx src/services/server-probe-rest-execution.test.ts && tsx src/services/server-probe-journal.test.ts && tsx src/services/server-probe-journal-history.test.ts && tsx src/modules/mcp/server-probe-actions.test.ts && tsx src/modules/mcp/diagnostic-cancellation.test.ts && tsx src/modules/mcp/service-diagnostic-progress.test.ts && tsx src/services/mikrotik-get-admission.test.ts && tsx src/services/server-probe-storage-journal-adapter.test.ts && npm run test:server-probe-history && npm run test:server-probe-storage-models",
"test:server-poll-http:pg": "node scripts/test-server-poll-http-owned.mjs", "test:server-poll-http:pg": "node scripts/test-server-poll-http-owned.mjs",
"test:server-poll-approval:pg": "node scripts/test-server-poll-approval-owned.mjs", "test:server-poll-approval:pg": "node scripts/test-server-poll-approval-owned.mjs",
"test:server-poll-atomic:pg": "node scripts/test-server-poll-atomic-owned.mjs", "test:server-poll-atomic:pg": "node scripts/test-server-poll-atomic-owned.mjs",
@@ -111,6 +112,7 @@
"test:mcp:routing-schema:pg": "node scripts/test-mcp-postgres.mjs --routing-reservation-schema", "test:mcp:routing-schema:pg": "node scripts/test-mcp-postgres.mjs --routing-reservation-schema",
"test:mcp:reserved-step": "tsx --test src/modules/mcp/routing-reserved-step.test.ts", "test:mcp:reserved-step": "tsx --test src/modules/mcp/routing-reserved-step.test.ts",
"test:mcp:routing-forward-rounds:pg": "node scripts/test-mcp-postgres.mjs --routing-network-forward-rounds", "test:mcp:routing-forward-rounds:pg": "node scripts/test-mcp-postgres.mjs --routing-network-forward-rounds",
"test:mcp:routing-original-effects:pg": "node scripts/test-mcp-postgres.mjs --routing-original-effects",
"test:flow-active-services:pg": "node scripts/test-mcp-postgres.mjs --flow-active-services", "test:flow-active-services:pg": "node scripts/test-mcp-postgres.mjs --flow-active-services",
"test:flow-active-rebuild:pg": "node scripts/test-mcp-postgres.mjs --flow-active-rebuild", "test:flow-active-rebuild:pg": "node scripts/test-mcp-postgres.mjs --flow-active-rebuild",
"test:flow-worker-ipc:pg": "node scripts/test-mcp-postgres.mjs --flow-worker-ipc", "test:flow-worker-ipc:pg": "node scripts/test-mcp-postgres.mjs --flow-worker-ipc",
@@ -140,7 +142,7 @@
"test:server-poll-storage-fresh:pg": "node scripts/test-mcp-postgres.mjs --server-poll-storage-fresh --restore", "test:server-poll-storage-fresh:pg": "node scripts/test-mcp-postgres.mjs --server-poll-storage-fresh --restore",
"test:partition-writers": "tsx src/services/partition-write-transaction.test.ts && tsx src/services/collector-partition-writes.test.ts && tsx src/db/partitions-writer.test.ts && tsx src/db/migrate-partition-admission.test.ts && tsx src/db/migration-manifest.test.ts && tsx src/db/sqlite-import-partition-admission.test.ts && tsx src/services/system-database-partition-restore.test.ts && tsx src/services/traffic-flow-write-transaction.test.ts && tsx src/services/traffic-flow-facts-rebuild-cli.test.ts", "test:partition-writers": "tsx src/services/partition-write-transaction.test.ts && tsx src/services/collector-partition-writes.test.ts && tsx src/db/partitions-writer.test.ts && tsx src/db/migrate-partition-admission.test.ts && tsx src/db/migration-manifest.test.ts && tsx src/db/sqlite-import-partition-admission.test.ts && tsx src/services/system-database-partition-restore.test.ts && tsx src/services/traffic-flow-write-transaction.test.ts && tsx src/services/traffic-flow-facts-rebuild-cli.test.ts",
"test:mcp:routing-authority": "tsx --test src/modules/mcp/routing-switch-ledger-contract.test.ts && tsx src/modules/mcp/routing-profile-authority.test.ts && tsx src/modules/mcp/routing-recovery-authority.test.ts && tsx src/modules/mcp/routing-reservation.test.ts && tsx src/modules/mcp/routing-community-reserved-source.test.ts && tsx src/modules/mcp/routing-network-verifier-model.test.ts && tsx --test src/test/committed-scoped-checkpoint-observer.test.ts && node --test src/modules/mcp/routing-forward-transport-charge.test.cjs", "test:mcp:routing-authority": "tsx --test src/modules/mcp/routing-switch-ledger-contract.test.ts && tsx src/modules/mcp/routing-profile-authority.test.ts && tsx src/modules/mcp/routing-recovery-authority.test.ts && tsx src/modules/mcp/routing-reservation.test.ts && tsx src/modules/mcp/routing-community-reserved-source.test.ts && tsx src/modules/mcp/routing-network-verifier-model.test.ts && tsx --test src/test/committed-scoped-checkpoint-observer.test.ts && node --test src/modules/mcp/routing-forward-transport-charge.test.cjs",
"test:mcp:native-readers": "tsx src/modules/mcp/routing-network-verifier-model.test.ts && tsx src/modules/mcp/routing-network-native-parser.test.ts && tsx src/modules/mcp/routing-network-native-session.test.ts && tsx src/db/migration-schema-inventory.test.ts && tsx src/modules/mcp/routing-network-observation-assembly.test.ts && tsx src/modules/mcp/routing-network-configuration-bridge.test.ts", "test:mcp:native-readers": "tsx src/modules/mcp/routing-network-verifier-model.test.ts && tsx src/modules/mcp/routing-network-native-parser.test.ts && tsx src/modules/mcp/routing-network-native-session.test.ts && tsx src/db/migration-schema-inventory.test.ts && tsx src/modules/mcp/routing-network-observation-assembly.test.ts && tsx src/modules/mcp/routing-network-configuration-bridge.test.ts && tsx --test src/services/routing-table-fib.test.ts src/services/routing-topology-observation.test.ts && node --test src/modules/mcp/routing-original-effects.test.mjs src/modules/mcp/routing-original-effects-caller.test.mjs src/modules/mcp/routing-original-effects-controller.test.mjs src/modules/mcp/routing-network-execution-origin.test.mjs",
"test:mcp:native-readers:pg": "node scripts/test-mcp-postgres.mjs --routing-network-native --restore", "test:mcp:native-readers:pg": "node scripts/test-mcp-postgres.mjs --routing-network-native --restore",
"test:mcp:schema-inventory:pg": "node scripts/test-mcp-postgres.mjs --migration-schema-inventory --restore", "test:mcp:schema-inventory:pg": "node scripts/test-mcp-postgres.mjs --migration-schema-inventory --restore",
"test:mcp:native-tls": "tsx src/modules/mcp/routing-network-native-tls.test.ts", "test:mcp:native-tls": "tsx src/modules/mcp/routing-network-native-tls.test.ts",
@@ -153,8 +155,9 @@
"test:server-poll-durable:pg": "node scripts/test-mcp-postgres.mjs --server-poll-durable --restore", "test:server-poll-durable:pg": "node scripts/test-mcp-postgres.mjs --server-poll-durable --restore",
"test:server-probe-legacy:pg": "node scripts/test-mcp-postgres.mjs --probe-legacy-orphans", "test:server-probe-legacy:pg": "node scripts/test-mcp-postgres.mjs --probe-legacy-orphans",
"test:server-probe-storage:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-journal", "test:server-probe-storage:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-journal",
"test:server-probe-history": "tsx --test src/services/server-probe-storage-history.test.ts", "test:server-probe-history": "tsx --test src/services/server-probe-storage-history.test.ts src/services/server-probe-retired-source.test.ts",
"test:server-probe-history:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-history", "test:server-probe-history:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-history",
"test:server-probe-retirement:pg": "node scripts/test-mcp-postgres.mjs --probe-operation-retirement",
"test:server-probe-storage-models": "tsx src/services/server-probe-journal-maintenance.test.ts && tsx src/modules/mcp/diagnostic-cancellation-source.test.ts && tsx src/services/server-probe-storage-source.test.ts && tsx src/services/server-probe-storage-connection.test.ts && tsx src/modules/mcp/server-probe-storage-host.test.ts && tsx src/services/server-probe-storage-rest.test.ts && tsx src/services/server-probe-storage-metadata.test.ts && tsx src/services/server-probe-legacy-verifier.test.ts && tsx src/services/server-probe-legacy-snapshot.test.ts && tsx src/services/server-probe-legacy-inspection.test.ts && tsx src/db/database-source-observation.test.ts && tsx src/services/server-probe-retention.test.ts && tsx src/modules/mcp/server-probe-registration.test.ts", "test:server-probe-storage-models": "tsx src/services/server-probe-journal-maintenance.test.ts && tsx src/modules/mcp/diagnostic-cancellation-source.test.ts && tsx src/services/server-probe-storage-source.test.ts && tsx src/services/server-probe-storage-connection.test.ts && tsx src/modules/mcp/server-probe-storage-host.test.ts && tsx src/services/server-probe-storage-rest.test.ts && tsx src/services/server-probe-storage-metadata.test.ts && tsx src/services/server-probe-legacy-verifier.test.ts && tsx src/services/server-probe-legacy-snapshot.test.ts && tsx src/services/server-probe-legacy-inspection.test.ts && tsx src/db/database-source-observation.test.ts && tsx src/services/server-probe-retention.test.ts && tsx src/modules/mcp/server-probe-registration.test.ts",
"test:mcp:migration-data:pg": "node scripts/test-mcp-postgres.mjs --migration-data-inventory --restore", "test:mcp:migration-data:pg": "node scripts/test-mcp-postgres.mjs --migration-data-inventory --restore",
"test:mcp:poll-memory:pg": "npm run build -w @mmapp/contracts && node scripts/acceptance/build-poll-memory.mjs && node scripts/test-mcp-postgres.mjs --poll-memory", "test:mcp:poll-memory:pg": "npm run build -w @mmapp/contracts && node scripts/acceptance/build-poll-memory.mjs && node scripts/test-mcp-postgres.mjs --poll-memory",
@@ -0,0 +1,123 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { randomBytes } from 'node:crypto'
import { lstat, mkdtemp, rm } from 'node:fs/promises'
import { createServer } from 'node:net'
import { tmpdir } from 'node:os'
import { isAbsolute, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import EmbeddedPostgres from 'embedded-postgres'
import pg from 'pg'
import { applySqlMigrations } from '../../src/db/migrate.ts'
import { MIGRATION_MANIFEST } from '../../src/db/migration-manifest.ts'
assert.deepEqual(process.argv.slice(2), ['--owned-embedded'])
const bin = process.env.MMAPP_PG_CLIENT_BIN
assert(bin && isAbsolute(bin) && resolve(bin) === bin, 'Set MMAPP_PG_CLIENT_BIN to the exact local PostgreSQL client directory')
const psql = join(bin, process.platform === 'win32' ? 'psql.exe' : 'psql')
const executable = await lstat(psql)
assert(executable.isFile() && !executable.isSymbolicLink(), 'PostgreSQL client must be a regular file')
const clientEnv = Object.fromEntries(Object.entries(process.env).filter(([name]) => !/^PG/i.test(name)))
const run = (command, args, options = {}) => execFileSync(command, args, {
timeout: 10000, maxBuffer: 16384, encoding: 'utf8', windowsHide: true, ...options,
})
assert.match(run(psql, ['--version'], { env: clientEnv }), /^psql \(PostgreSQL\) (?:1[6-9]|[2-9][0-9])\./)
const witness = fileURLToPath(new URL('../../../tmp/mcp-netflow-linux-topology-20261104/collector-drain-witness.py', import.meta.url))
const { IDENTITY_SQL, SQL } = JSON.parse(run('python', ['-I', '-B', '-c',
'import json,runpy,sys; m=runpy.run_path(sys.argv[1]); print(json.dumps({k:m[k] for k in ("IDENTITY_SQL","SQL")}))', witness]))
assert(typeof SQL === 'string' && typeof IDENTITY_SQL === 'string')
const directory = await mkdtemp(join(tmpdir(), 'mm-netflow-drain-pg-'))
const password = randomBytes(24).toString('hex')
const host = '127.0.0.1', database = 'postgres', user = 'mcp_test'
const listener = createServer()
let embedded, pool, started = false
try {
await new Promise((done, reject) => { listener.once('error', reject); listener.listen(0, host, done) })
const { port } = listener.address()
await new Promise((done, reject) => listener.close(error => error ? reject(error) : done()))
embedded = new EmbeddedPostgres({
databaseDir: join(directory, 'pg'), port, user, password, persistent: true, createPostgresUser: false,
initdbFlags: ['--encoding=UTF8'], postgresFlags: ['-h', host, '-c', 'unix_socket_directories='],
onLog: () => {}, onError: () => {},
})
await embedded.initialise()
await embedded.start()
started = true
pool = new pg.Pool({ host, port, database, user, password, max: 1, connectionTimeoutMillis: 5000 })
await applySqlMigrations(pool)
const { rows: [version] } = await pool.query("SELECT current_setting('server_version_num')::int AS number, (SELECT count(*)::int FROM schema_migrations) AS migrations")
assert(version.number >= 180000 && version.number <= 189999)
assert.equal(version.migrations, MIGRATION_MANIFEST.length)
const runId = 'a012bcde', owner = 'netflow-operator-' + runId, collectorPort = 51000
const read = (sql, probePort = collectorPort) => JSON.parse(run(psql, ['-X', '-w', '-qAt',
'-h', host, '-p', String(port), '-U', user, '-d', database, '-v', 'ON_ERROR_STOP=1',
'-v', 'database=' + database, '-v', 'socket_dir=/tmp/mcp-netflow-' + runId + '/pg-socket',
'-v', 'run_id=' + runId, '-v', 'port=' + probePort],
{ env: { ...clientEnv, PGCONNECT_TIMEOUT: '2', PGPASSWORD: password }, input: sql }).trim())
const identity = read(IDENTITY_SQL)
assert.deepEqual(Object.keys(identity).sort(), ['oid', 'source'])
assert.match(identity.oid, /^[1-9][0-9]{0,9}$/)
// The production source predicate deliberately rejects this TCP fixture.
assert.equal(identity.source, false)
const expect = (planCount, enabled, verified, ordered = true, sqlPort = collectorPort) =>
assert.deepEqual(read(SQL, sqlPort), { ...identity, connectionCount: 1, planCount, enabled, verified, ordered })
const uuid = index => 'a012bcde-0000-4000-8000-' + String(index).padStart(12, '0')
const connection = uuid(1)
await pool.query(`INSERT INTO mcp_connections(id,name,owner_id,owner_permissions,policy,generation)
VALUES($1,$2,$3,'[]','{}','inert-witness')`, [connection, 'Owned NetFlow ' + runId, owner])
await pool.query("UPDATE traffic_flow_settings SET collector_ip='198.18.242.3', flow_listen_port=$1, enabled=false WHERE id=1", [collectorPort])
expect(0, false, [])
async function seed(index, enabled) {
const plan = uuid(index), operation = uuid(index + 1), receipt = uuid(index + 2)
const base = Date.UTC(2026, 0, 1) + index * 1000
const at = offset => new Date(base + offset)
const result = { receiptId: receipt, configurationConfirmed: true, receiptConfirmed: true,
listener: 'request-acknowledged', automaticReplay: false }
await pool.query(`INSERT INTO mcp_plans(id,connection_id,operation_id,operation_version,policy_version,generation,payload,evidence,state,expires_at,approved_by,approved_at,created_at)
VALUES($1,$2,'netflow.settings.update',3,1,'inert-witness','{}','{}','completed',$3,$4,$5,$6)`,
[plan, connection, at(9000), owner, at(100), at(0)])
await pool.query(`INSERT INTO mcp_operations(id,plan_id,connection_id,idempotency_key,state,verification,created_at,finished_at)
VALUES($1,$2,$3,$4,'applied','passed',$5,$6)`, [operation, plan, connection, 'inert-' + index, at(200), at(400)])
await pool.query(`INSERT INTO mcp_operation_steps(operation_id,step_index,state,intended_state,result,created_at,finished_at)
VALUES($1,0,'verified',$2,$3,$4,$5)`,
[operation, { service: 'netflow.settings.update', input: { receiptId: receipt, enabled } }, result, at(300), at(400)])
await pool.query(`INSERT INTO mcp_audit(actor_id,action,connection_id,payload,created_at)
VALUES($1,'plan.approve',$2,$3,$4)`, [owner, connection, { planId: plan }, at(100)])
await pool.query(`INSERT INTO flow_settings_intents(receipt_id,phase,request_proof,state,result,created_at)
VALUES($1,'configuration','inert-proof','completed',$2,$3),($1,'listener','inert-proof','completed',$2,$3)`, [receipt, result, at(300)])
await pool.query('UPDATE traffic_flow_settings SET enabled=$1 WHERE id=1', [enabled])
return [plan, operation, receipt]
}
const enabled = await seed(10, true)
expect(1, true, [enabled])
const disabled = await seed(20, false)
expect(2, false, [enabled, disabled])
expect(2, null, [enabled, disabled], true, collectorPort + 1)
await pool.query("UPDATE flow_settings_intents SET request_proof='foreign' WHERE receipt_id=$1 AND phase='listener'", [disabled[2]])
expect(2, false, [enabled])
await pool.query("UPDATE flow_settings_intents SET request_proof='inert-proof' WHERE receipt_id=$1 AND phase='listener'", [disabled[2]])
const { rows: [duplicate] } = await pool.query(`INSERT INTO mcp_audit(actor_id,action,connection_id,payload)
VALUES($1,'plan.approve',$2,$3) RETURNING id`, [owner, connection, { planId: disabled[0] }])
expect(2, false, [enabled])
await pool.query('DELETE FROM mcp_audit WHERE id=$1', [duplicate.id])
const { rows: [original] } = await pool.query(`UPDATE mcp_operations SET finished_at=(SELECT created_at + interval '1 second' FROM mcp_plans WHERE id=$2)
WHERE id=$1 RETURNING created_at`, [enabled[1], disabled[0]])
expect(2, false, [enabled, disabled], false)
await pool.query("UPDATE mcp_operations SET finished_at=$2::timestamptz + interval '200 milliseconds' WHERE id=$1", [enabled[1], original.created_at])
expect(2, false, [enabled, disabled])
console.log(JSON.stringify({ migrations: version.migrations, exactPsqlQueries: true, inertEnableDisableAndRejections: true,
linuxSocketSourceAccepted: false, realMcpExecutionClaimed: false }))
} finally {
try {
await pool?.end()
} finally {
if (started) await embedded.stop()
if (listener.listening) await new Promise(done => listener.close(done))
await rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
}
}
+3 -8
View File
@@ -7,14 +7,9 @@ import path from 'node:path'
import {fileURLToPath,pathToFileURL} from 'node:url' import {fileURLToPath,pathToFileURL} from 'node:url'
import ts from 'typescript' import ts from 'typescript'
export const OPERATOR_SOURCES=Object.freeze([ import {OPERATOR_SOURCES} from './database-operator-sources.mjs'
'database-writer-bootstrap-operator.mjs','database-writer-profile-operator.mts', export {OPERATOR_SOURCES}
'writer-profile/operator-files.mjs','writer-profile/operator-contract.mjs','writer-profile/publisher.mjs','writer-profile/context.mjs',
'writer-profile/catalogue.mts','writer-profile/catalogue-value-bridge.ts','writer-profile/catalogue-projection.ts','writer-profile/catalogue-leaves.ts',
'writer-profile/application-writer-profile-catalogue.ts','writer-profile/application-writer-profile-catalogue-values.ts',
'writer-profile/application-writer-profile-catalogue-parent-policy.ts','writer-profile/application-writer-profile-canonical.ts',
'writer-profile/fixed44-build-sql.ts','writer-profile/fixed44-build-definition.ts','writer-profile/expanded44-data-policy.ts',
])
const emitted=source=>source.replace(/\.mts$/,'.mjs').replace(/\.ts$/,'.js') const emitted=source=>source.replace(/\.mts$/,'.mjs').replace(/\.ts$/,'.js')
const digest=value=>createHash('sha256').update(value).digest('hex') const digest=value=>createHash('sha256').update(value).digest('hex')
const packageName=specifier=>specifier.startsWith('@')?specifier.split('/').slice(0,2).join('/'):specifier.split('/')[0] const packageName=specifier=>specifier.startsWith('@')?specifier.split('/').slice(0,2).join('/'):specifier.split('/')[0]
@@ -5,6 +5,7 @@ import {spawnSync} from 'node:child_process'
import path from 'node:path' import path from 'node:path'
import {fileURLToPath,pathToFileURL} from 'node:url' import {fileURLToPath,pathToFileURL} from 'node:url'
import {buildDatabaseOperator,compileOperatorModule,moduleSpecifiers,OPERATOR_SOURCES} from './build-database-operator.mjs' import {buildDatabaseOperator,compileOperatorModule,moduleSpecifiers,OPERATOR_SOURCES} from './build-database-operator.mjs'
import {stageDatabaseOperatorScripts} from './stage-database-operator.mjs'
const backend=process.env.OPERATOR_BUILD_TEST_BACKEND??path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..') const backend=process.env.OPERATOR_BUILD_TEST_BACKEND??path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..')
test('AST rewrite changes static/export/dynamic module specifiers and preserves SQL/string values',()=>{ test('AST rewrite changes static/export/dynamic module specifiers and preserves SQL/string values',()=>{
@@ -19,7 +20,7 @@ test('fixed artifact is reproducible, resolves the compiled backend graph, and r
const packageJson=JSON.parse(await readFile(path.join(backend,'package.json'),'utf8')) const packageJson=JSON.parse(await readFile(path.join(backend,'package.json'),'utf8'))
await mkdir(projectBackend,{recursive:true});await writeFile(path.join(projectBackend,'package.json'),JSON.stringify({type:'module',dependencies:packageJson.dependencies})) await mkdir(projectBackend,{recursive:true});await writeFile(path.join(projectBackend,'package.json'),JSON.stringify({type:'module',dependencies:packageJson.dependencies}))
const first=await buildDatabaseOperator({backendDirectory:backend,outputDirectory:output}),second=await buildDatabaseOperator({backendDirectory:backend,outputDirectory:output}) const first=await buildDatabaseOperator({backendDirectory:backend,outputDirectory:output}),second=await buildDatabaseOperator({backendDirectory:backend,outputDirectory:output})
assert.deepEqual(first,second);assert.equal(first.modules.length,OPERATOR_SOURCES.length);assert.equal(first.modules.length,17);assert.deepEqual(first.runtimeDependencies,['dotenv','pg','zod']);assert.equal(first.backendDistModules.length,9) assert.deepEqual(first,second);assert.equal(first.modules.length,OPERATOR_SOURCES.length);assert.equal(first.modules.length,19);assert.deepEqual(first.runtimeDependencies,['dotenv','pg','zod']);assert.equal(first.backendDistModules.length,12)
const copied=new Set() const copied=new Set()
async function copyDist(relative){ async function copyDist(relative){
if(copied.has(relative))return;copied.add(relative) if(copied.has(relative))return;copied.add(relative)
@@ -34,10 +35,11 @@ test('fixed artifact is reproducible, resolves the compiled backend graph, and r
for(const relative of first.backendDistImports)await copyDist(relative) for(const relative of first.backendDistImports)await copyDist(relative)
assert.deepEqual([...copied].sort(),first.backendDistModules.map(item=>item.file).sort()) assert.deepEqual([...copied].sort(),first.backendDistModules.map(item=>item.file).sort())
// Exercise the real build entry point with its default backend/output paths. // Exercise the real build entry point with its default backend/output paths.
for(const file of OPERATOR_SOURCES){const target=path.join(projectBackend,'scripts',file);await mkdir(path.dirname(target),{recursive:true});await copyFile(path.join(backend,'scripts',file),target)} const scripts=path.join(projectBackend,'scripts')
const buildScript=path.join(projectBackend,'scripts','build-database-operator.mjs');await copyFile(fileURLToPath(new URL('./build-database-operator.mjs',import.meta.url)),buildScript) assert.equal(await stageDatabaseOperatorScripts(scripts,path.join(backend,'scripts')),21)
const buildScript=path.join(scripts,'build-database-operator.mjs')
const built=spawnSync(process.execPath,[buildScript],{cwd:temporary,env:{...process.env,NODE_OPTIONS:''},encoding:'utf8',timeout:30000}) const built=spawnSync(process.execPath,[buildScript],{cwd:temporary,env:{...process.env,NODE_OPTIONS:''},encoding:'utf8',timeout:30000})
assert.equal(built.status,0,built.stderr);assert.equal(JSON.parse(built.stdout.trim()).modules,17) assert.equal(built.status,0,built.stderr);assert.equal(JSON.parse(built.stdout.trim()).modules,19)
assert.deepEqual(JSON.parse(await readFile(path.join(output,'build-manifest.json'),'utf8')),first) assert.deepEqual(JSON.parse(await readFile(path.join(output,'build-manifest.json'),'utf8')),first)
const configFile=path.join(projectBackend,'dist','config.js'),originalConfig=await readFile(configFile,'utf8') const configFile=path.join(projectBackend,'dist','config.js'),originalConfig=await readFile(configFile,'utf8')
for(const injected of ["import 'tsx';","import '../src/config.js';","import '../outside.js';"]){ for(const injected of ["import 'tsx';","import '../src/config.js';","import '../outside.js';"]){
@@ -59,10 +61,10 @@ test('fixed artifact is reproducible, resolves the compiled backend graph, and r
const modulesToImport=first.modules.map(item=>path.join(output,item.file)) const modulesToImport=first.modules.map(item=>path.join(output,item.file))
const importer=path.join(temporary,'import-all.mjs');await writeFile(importer,`import {pathToFileURL} from 'node:url';for(const file of ${JSON.stringify(modulesToImport)})await import(pathToFileURL(file).href);console.log(JSON.stringify({imported:${modulesToImport.length},readyAuthorized:false}));`) const importer=path.join(temporary,'import-all.mjs');await writeFile(importer,`import {pathToFileURL} from 'node:url';for(const file of ${JSON.stringify(modulesToImport)})await import(pathToFileURL(file).href);console.log(JSON.stringify({imported:${modulesToImport.length},readyAuthorized:false}));`)
const imported=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,importer],{cwd:temporary,env,encoding:'utf8',timeout:30000}) const imported=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,importer],{cwd:temporary,env,encoding:'utf8',timeout:30000})
assert.equal(imported.status,0,imported.stderr);assert.equal(JSON.parse(imported.stdout.trim()).imported,17);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0) assert.equal(imported.status,0,imported.stderr);assert.equal(JSON.parse(imported.stdout.trim()).imported,19);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0)
const refusals=[] const refusals=[]
for(const [entry,event] of [['database-writer-bootstrap-operator.mjs','application-writer-bootstrap-refused'],['database-writer-profile-operator.mjs','application-writer-profile-publication-refused']]){ for(const [entry,event,args] of [['database-writer-bootstrap-operator.mjs','application-writer-bootstrap-refused',['invalid']],['database-writer-profile-operator.mjs','application-writer-profile-publication-refused',['invalid']],['database-writer-profile-operator.mjs','database-maintenance-command-refused',['maintenance-inspect']],['database-writer-profile-operator.mjs','database-maintenance-command-refused',['maintenance-resolve']]]){
const child=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,path.join(output,entry),'invalid'],{cwd:temporary,env,encoding:'utf8',timeout:30000}) const child=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,path.join(output,entry),...args],{cwd:temporary,env,encoding:'utf8',timeout:30000})
assert.equal(child.status,1,child.stderr);assert.equal(child.stdout,'');assert.equal(JSON.parse(child.stderr.trim()).event,event);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0);refusals.push(event) assert.equal(child.status,1,child.stderr);assert.equal(child.stdout,'');assert.equal(JSON.parse(child.stderr.trim()).event,event);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0);refusals.push(event)
} }
const receipt={node:process.version,projection:temporary,modules:first.modules.length,compiledBackendModules:copied.size,buildCliExitCode:built.status,importedModules:modulesToImport.length,invalidArgumentsRefused:refusals,developmentDependenciesBlocked:denied,networkConnections:0,productionActivated:false,readyAuthorized:false} const receipt={node:process.version,projection:temporary,modules:first.modules.length,compiledBackendModules:copied.size,buildCliExitCode:built.status,importedModules:modulesToImport.length,invalidArgumentsRefused:refusals,developmentDependenciesBlocked:denied,networkConnections:0,productionActivated:false,readyAuthorized:false}
@@ -0,0 +1,100 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {mkdtemp,mkdir,writeFile,readFile,readdir,rm,unlink,lstat} from 'node:fs/promises'
import {createHash,randomUUID} from 'node:crypto'
import {rootCertificates} from 'node:tls'
import {EventEmitter} from 'node:events'
import path from 'node:path'
import {fileURLToPath,pathToFileURL} from 'node:url'
import {runInNewContext} from 'node:vm'
import {createRequire} from 'node:module'
import * as strictReader from '../src/services/database-maintenance-startup.js'
import * as grammar from '../src/services/database-maintenance-resolution.js'
import * as manifest from '../src/db/migration-manifest.js'
import * as contract from './writer-profile/operator-contract.mjs'
const backend=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'),require=createRequire(import.meta.url),ts=require('typescript'),hash=(value:Buffer|string)=>createHash('sha256').update(value).digest('hex')
async function own(work:(root:string)=>Promise<void>){const temp=path.resolve(backend,'../tmp');await mkdir(temp,{recursive:true});const root=await mkdtemp(path.join(temp,'owned-resolution-command-'));try{await work(root)}finally{assert.equal(path.dirname(root),temp);assert(path.basename(root).startsWith('owned-resolution-command-'));await rm(root,{recursive:true,force:true})}}
async function model(root:string,localHost?:string){
let owners=0,sql=0,releaseFailure=false,poolEndFailure=false,published=0,wrongOwner=false,controlSchema=false,observationFailure:Error|undefined
const capturedScopes:unknown[]=[]
const state=path.join(root,'state');await mkdir(state);await mkdir(path.join(state,'restore-vault'));await writeFile(path.join(state,'access-generation'),Buffer.alloc(32,7));const uncertain=randomUUID(),at=new Date().toISOString(),record=(value:string)=>JSON.stringify({operationId:uncertain,state:value,at})+'\n';await writeFile(path.join(state,'database-maintenance.jsonl'),record('intent')+record('unconfirmed'))
const source={systemIdentifier:'1234567890',databaseOid:'12345',databaseName:'managed',serverVersionNum:180004,inRecovery:false}
const schema={sha256:'a'.repeat(64),observedRaw:{sha256:'b'.repeat(64)},observedPhysical:{sha256:'c'.repeat(64)},sections:{relations:manifest.CURRENT_REQUIRED_TABLES.map(name=>({namespace:'public',name,kind:'r',persistence:'p',row_security:false,force_row_security:false})),columns:[{namespace:'public',relation:'schema_migrations',name:'id',dropped:false,type:'text'}]}}
class Pool {on(){};constructor(options:unknown){const ssl=(options as {ssl:false|{rejectUnauthorized:boolean}}).ssl;if(ssl===false)assert(['127.0.0.1','::1'].includes((options as {host:string}).host));else assert(ssl.rejectUnauthorized)}async connect(){owners++;const client=new EventEmitter();return Object.assign(client,{release:()=>{owners--;if(releaseFailure)throw Error('Synthetic release uncertainty')},query:async(text:string)=>{sql++;if(text.includes('pg_try_advisory_lock'))return {rows:[{locked:true}],command:'SELECT'};if(text.includes('pg_advisory_unlock'))return {rows:[{unlocked:true}],command:'SELECT'};if(text.includes('pg_namespace'))return {rows:controlSchema?[{nspname:'mmapp_control'}]:[],command:'SELECT'};if(text.includes('pg_control_system')){if(observationFailure)throw observationFailure;return {rows:[source],command:'SELECT'}};if(text.startsWith('SELECT id FROM public.schema_migrations'))return {rows:manifest.MIGRATION_MANIFEST.map(pin=>({id:pin.file.slice(0,-4)})),command:'SELECT'};return {rows:[],command:text.split(' ')[0]}}})}async end(){assert.equal(owners,0);if(poolEndFailure)throw Error('Synthetic pool retirement uncertainty')}}
const protectedSource=await readFile(path.join(backend,'scripts/writer-profile/operator-files.mjs'),'utf8'),protectedExports:Record<string,unknown>={},actualUid=(await lstat(state)).uid
const protectedCode=ts.transpileModule(protectedSource,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText
runInNewContext(protectedCode,{exports:protectedExports,require,Buffer,process:{platform:'linux',getuid:()=>actualUid+1},Object,Error})
const protectedDirectory=async(directory:string)=>{assert(path.resolve(directory).startsWith(root+path.sep)||path.resolve(directory)===root);if(wrongOwner)await (protectedExports.protectedDirectory as (path:string)=>Promise<void>)(directory);await readdir(directory)}
// Explicit MODELED protection adapter: native POSIX mode/ownership and hardlink
// fsync publication remain separate Linux acceptance, never a Windows claim.
const files={sha:hash,protectedDirectory,readProtected:async(file:string,maximum:number,pin?:string)=>{assert(file.startsWith(root+path.sep));const raw=await readFile(file);assert(raw.length>0&&raw.length<=maximum);if(pin)assert.equal(hash(raw),pin);return raw},writeFile:async(file:string,raw:Buffer)=>{assert(file.startsWith(root+path.sep));await writeFile(file,raw,{flag:'wx'});published++},withJournalLease:async(directory:string,work:()=>Promise<unknown>)=>{const lock=path.join(directory,'publisher-lease.lock');await writeFile(lock,'owned',{flag:'wx'});try{return await work()}finally{await unlink(lock)}}}
const readRecords=async(directory:string)=>{const records=path.join(directory,'maintenance-acceptances');let names:string[];try{names=await readdir(records)}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT')return [];throw error};assert(names.every(name=>/^[a-f0-9-]{36}\.json$/.test(name)));return Promise.all(names.sort().map(name=>readFile(path.join(records,name))))}
const moduleFile=path.join(backend,'scripts/writer-profile/maintenance-resolution.mts'),sourceText=(await readFile(moduleFile,'utf8')).replaceAll('import.meta.url',JSON.stringify(pathToFileURL(moduleFile).href)),exports:Record<string,unknown>={}
const dependencies:Record<string,unknown>={'./operator-files.mjs':files,'./operator-contract.mjs':contract,'../../src/services/database-maintenance-startup.js':{...strictReader,readMaintenanceAcceptanceRecords:readRecords},'../../src/services/database-maintenance-resolution.js':grammar,'../../src/db/migration-schema-inventory.js':{captureMigrationSchema:async(_client:unknown,options:{scope:unknown})=>{capturedScopes.push(options.scope);return schema}},'../../src/db/migration-manifest.js':manifest,pg:{Pool}}
const code=ts.transpileModule(sourceText,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText
runInNewContext(code,{exports,require:(name:string)=>name in dependencies?dependencies[name]:require(name),Buffer,URL,process:{env:{}},Date,Object,Map,Set,Error,console})
const command=exports.maintenanceMain as (args:string[])=>Promise<Record<string,unknown>>
const tlsTransport={format:'database-maintenance-transport-v1',connection:{host:'db.example.invalid',port:5432,user:'existing-maintenance-user',password:'synthetic-only',database:'managed'},tls:{serverName:'db.example.invalid',spkiSha256:'f'.repeat(64)}},localTransport=(host:string)=>({format:'database-maintenance-transport-v1',mode:'local-loopback',connection:{...tlsTransport.connection,host}})
const configs={transport:localHost===undefined?tlsTransport:localTransport(localHost),'runtime-target':{format:'database-maintenance-runtime-target-v1',databaseUrl:'postgresql://db.example.invalid:5432/managed?sslmode=require'}}
const args=['--state-dir',state]
for(const [name,value]of Object.entries(configs)){const raw=Buffer.from(JSON.stringify(value)),file=path.join(root,name+'.json');await writeFile(file,raw);args.push('--'+name,file,'--'+name+'-sha256',hash(raw))}
const ca=Buffer.from(rootCertificates[0]),caFile=path.join(root,'ca.pem');await writeFile(caFile,ca);if(localHost===undefined)args.push('--ca',caFile,'--ca-sha256',hash(ca))
const output=path.join(root,'inspection.json')
async function inspect(destination=output){return command(['maintenance-inspect',...args,'--output',destination])}
async function approve(change?:(value:Record<string,unknown>)=>void){const raw=await readFile(output),inspection=JSON.parse(raw.toString()),approval={format:'database-maintenance-current-state-approval-v1',resolutionId:randomUUID(),decision:'accept-observed-current-state',actorId:'host-human-reviewer',inspectionSha256:hash(raw),issuedAt:new Date().toISOString(),expiresAt:new Date(Date.now()+60000).toISOString(),managedBackendStopped:true,historicOutcomesRemainUnknown:true,automaticRestore:false};assert(inspection.createdAt<=approval.issuedAt);change?.(approval);const bytes=Buffer.from(JSON.stringify(approval)),file=path.join(root,'approval.json');await writeFile(file,bytes);return ['maintenance-resolve',...args,'--inspection',output,'--inspection-sha256',hash(raw),'--approval',file,'--approval-sha256',hash(bytes)]}
const toLocal=async(host='127.0.0.1')=>{const raw=Buffer.from(JSON.stringify(localTransport(host)));await writeFile(path.join(root,'transport.json'),raw);args[args.indexOf('--transport-sha256')+1]=hash(raw);for(const flag of ['--ca','--ca-sha256']){const at=args.indexOf(flag);if(at>=0)args.splice(at,2)}}
const configuration=exports.maintenancePoolConfiguration as (value:unknown,ca?:Buffer,pin?:string)=>{options:Record<string,unknown>;transportBinding:{mode:string;endpointSha256:string}}
return {command,inspect,approve,toLocal,configuration,readRecords,state,output,sql:()=>sql,published:()=>published,failRelease:()=>{releaseFailure=true},failEnd:()=>{poolEndFailure=true},driftSchema:()=>{schema.sha256='0'.repeat(64)},driftTarget:()=>{source.databaseOid='54321'},wrongOwner:()=>{wrongOwner=true},withControlSchema:()=>{controlSchema=true},scopes:()=>capturedScopes,failObservation:(error:Error)=>{observationFailure=error}}
}
test('actual operator commands inspect then separately approved resolve; old uncertain bytes never rewritten',()=>own(async root=>{
const fixture=await model(root),journal=path.join(fixture.state,'database-maintenance.jsonl'),before=await readFile(journal),inspected=await fixture.inspect();assert.equal(inspected.humanApprovalRequired,true);assert.equal((await fixture.readRecords(fixture.state)).length,0)
const result=await fixture.command(await fixture.approve());assert.equal(result.outcome,'committed');assert.equal(result.historicOutcomesChanged,false);assert.equal(result.archiveDataEquivalenceProved,false);assert.equal(result.automaticRestore,false)
assert.deepEqual(await readFile(journal),before);const records=await fixture.readRecords(fixture.state),facts=await strictReader.readMaintenanceRecoveryFacts(fixture.state);assert.equal(records.length,1);const accepted=grammar.evaluateMaintenanceAcceptances(records,facts.journal,facts.vault.evidence,grammar.runtimeDatabaseTargetSha256('postgresql://db.example.invalid:5432/managed?sslmode=require'));assert.equal(accepted.count,1)
fixture.driftSchema();const afterStatus=await fixture.inspect(path.join(root,'after-inspection.json'));assert.equal(afterStatus.state,'historically-accepted');assert.equal(afterStatus.humanApprovalRequired,false);assert.equal(afterStatus.historicOutcomesChanged,false)
await assert.rejects(fixture.command(await fixture.approve()),/No new unresolved|already|assert|Assertion/i)
}))
for(const type of ['expired','not-stopped','wrong-inspection'])test('actual resolve refuses '+type+' approval before connecting SQL',()=>own(async root=>{
const fixture=await model(root);await fixture.inspect();const args=await fixture.approve(value=>{if(type==='expired')value.expiresAt=new Date(Date.now()-1).toISOString();if(type==='not-stopped')value.managedBackendStopped=false;if(type==='wrong-inspection')value.inspectionSha256='0'.repeat(64)}),before=fixture.sql();await assert.rejects(fixture.command(args));assert.equal(fixture.sql(),before);assert.equal((await fixture.readRecords(fixture.state)).length,0)
}))
for(const type of ['release','pool-end','schema-drift','target-drift','external-drift'])test('actual resolve refuses '+type+' before acceptance publication',()=>own(async root=>{
const fixture=await model(root);await fixture.inspect();const args=await fixture.approve();if(type==='release')fixture.failRelease();if(type==='pool-end')fixture.failEnd()
if(type==='schema-drift')fixture.driftSchema();if(type==='target-drift')fixture.driftTarget()
if(type==='external-drift')await appendFileSafe(path.join(fixture.state,'database-maintenance.jsonl'))
await assert.rejects(fixture.command(args));assert.equal((await fixture.readRecords(fixture.state)).length,0)
}))
async function appendFileSafe(file:string){await writeFile(file,await readFile(file,'utf8')+JSON.stringify({operationId:randomUUID(),state:'intent',at:new Date().toISOString()})+'\n')}
test('existing shipped main dispatches both commands before writer-profile role/source flags',async()=>{
const source=await readFile(path.join(backend,'scripts/database-writer-profile-operator.mts'),'utf8');assert(source.indexOf("['maintenance-inspect','maintenance-resolve']")<source.indexOf("const flags="));assert(source.includes("import('./writer-profile/maintenance-resolution.mjs')"))
})
test('existing protectedDirectory owner contract refuses a foreign CLI UID before SQL or writes',()=>own(async root=>{
const fixture=await model(root);fixture.wrongOwner();await assert.rejects(fixture.inspect());assert.equal(fixture.sql(),0);assert.equal(fixture.published(),0);assert.equal((await fixture.readRecords(fixture.state)).length,0)
}))
test('explicit local-loopback supports both IP families and binds selected mode in actual inspect/resolve',async()=>{
for(const host of ['127.0.0.1','::1'])await own(async root=>{const fixture=await model(root,host);await fixture.inspect();const result=await fixture.command(await fixture.approve());assert.equal(result.outcome,'committed');const records=await fixture.readRecords(fixture.state);assert.equal(JSON.parse(records[0].toString()).knownCurrent.transport.mode,'local-loopback')})
})
test('plaintext remote/DNS/unspecified/mapped hosts refuse before SQL and never fall back',async()=>{
for(const host of ['db.example.invalid','localhost','0.0.0.0','192.0.2.1','[::1]','::ffff:127.0.0.1'])await own(async root=>{const fixture=await model(root,host);await assert.rejects(fixture.inspect());assert.equal(fixture.sql(),0);assert.equal(fixture.published(),0)})
})
test('an approval for TLS observation cannot resolve through a changed local-loopback transport',()=>own(async root=>{
const fixture=await model(root);await fixture.inspect();await fixture.toLocal();await assert.rejects(fixture.command(await fixture.approve()));assert.equal((await fixture.readRecords(fixture.state)).length,0)
}))
test('local mode explicitly disables SSL/environment substitution; TLS remains verified default without fallback',()=>own(async root=>{
const fixture=await model(root),ConnectionParameters=require('pg/lib/connection-parameters'),raw={format:'database-maintenance-transport-v1',mode:'local-loopback',connection:{host:'127.0.0.1',port:5432,user:'existing-user',password:'synthetic-only',database:'managed'}}
const previous={PGHOST:process.env.PGHOST,PGSSLMODE:process.env.PGSSLMODE,PGOPTIONS:process.env.PGOPTIONS};process.env.PGHOST='remote.invalid';process.env.PGSSLMODE='require';process.env.PGOPTIONS='-c role=foreign'
try{const config=fixture.configuration(raw),actual=new ConnectionParameters(config.options);assert.equal(actual.host,'127.0.0.1');assert.equal(actual.ssl,false);assert.equal(actual.options,contract.STARTUP_OPTIONS);assert.equal(config.transportBinding.mode,'local-loopback')}
finally{for(const [key,value]of Object.entries(previous)){if(value===undefined)delete process.env[key];else process.env[key]=value}}
const ca=Buffer.from(rootCertificates[0]),tls={format:'database-maintenance-transport-v1',connection:{...raw.connection,host:'db.example.invalid'},tls:{serverName:'db.example.invalid',spkiSha256:'f'.repeat(64)}}
assert.throws(()=>fixture.configuration(tls));const secure=fixture.configuration(tls,ca,hash(ca));assert.equal(secure.transportBinding.mode,'tls');assert.equal((secure.options.ssl as {rejectUnauthorized:boolean}).rejectUnauthorized,true)
assert.throws(()=>fixture.configuration({...raw,mode:'plaintext'}));assert.throws(()=>fixture.configuration({...tls,mode:null},ca,hash(ca)));assert.throws(()=>fixture.configuration(raw,ca,hash(ca)))
}))
// A normal installation has no optional writer-profile schema. Observation must
// select its exact namespace set without requiring installation of that system.
test('maintenance observes with and without the optional control namespace',async()=>{
for(const present of [false,true])await own(async root=>{const fixture=await model(root);if(present)fixture.withControlSchema();await fixture.inspect();assert.equal(JSON.stringify(fixture.scopes()),JSON.stringify([{applicationNamespaces:['public'],excludedControlNamespaces:present?['mmapp_control']:[]}]))})
})
test('original observation failure survives confirmed rollback/unlock/release',()=>own(async root=>{
const fixture=await model(root),failure=Error('Synthetic original observation failure');fixture.failObservation(failure);await assert.rejects(fixture.inspect(),error=>error===failure);assert.equal(fixture.published(),0);assert.equal((await fixture.readRecords(fixture.state)).length,0)
}))
@@ -0,0 +1,10 @@
/** The fixed build inputs shared by the operator and Docker staging. */
export const OPERATOR_SOURCES=Object.freeze([
'database-writer-bootstrap-operator.mjs','database-writer-profile-operator.mts',
'writer-profile/operator-files.mjs','writer-profile/operator-contract.mjs','writer-profile/publisher.mjs','writer-profile/context.mjs',
'writer-profile/protected-source-registry.mts','writer-profile/maintenance-resolution.mts',
'writer-profile/catalogue.mts','writer-profile/catalogue-value-bridge.ts','writer-profile/catalogue-projection.ts','writer-profile/catalogue-leaves.ts',
'writer-profile/application-writer-profile-catalogue.ts','writer-profile/application-writer-profile-catalogue-values.ts',
'writer-profile/application-writer-profile-catalogue-parent-policy.ts','writer-profile/application-writer-profile-canonical.ts',
'writer-profile/fixed44-build-sql.ts','writer-profile/fixed44-build-definition.ts','writer-profile/expanded44-data-policy.ts',
])
@@ -9,6 +9,7 @@ import {deploymentRequest} from './database-writer-bootstrap-operator.mjs'
const hex=(v:unknown)=>typeof v==='string'&&/^[a-f0-9]{64}$/.test(v) const hex=(v:unknown)=>typeof v==='string'&&/^[a-f0-9]{64}$/.test(v)
const parse=(b:Buffer)=>JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(b)) const parse=(b:Buffer)=>JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(b))
export async function main(args:string[]){ export async function main(args:string[]){
if(['maintenance-inspect','maintenance-resolve'].includes(args[0]))return (await import('./writer-profile/maintenance-resolution.mjs')).maintenanceMain(args)
const flags=['bootstrap','bootstrap-sha256','approval','approval-sha256','definition','definition-sha256','transport','transport-sha256','ca','source-registry','source-spki-sha256','journal'] const flags=['bootstrap','bootstrap-sha256','approval','approval-sha256','definition','definition-sha256','transport','transport-sha256','ca','source-registry','source-spki-sha256','journal']
assert(['publish','reconcile','preflight'].includes(args[0])&&args.length===1+2*flags.length) assert(['publish','reconcile','preflight'].includes(args[0])&&args.length===1+2*flags.length)
assertJsDriverEnvironment(process.env) assertJsDriverEnvironment(process.env)
@@ -38,7 +39,12 @@ export async function main(args:string[]){
assertJsDriverEnvironment(process.env) assertJsDriverEnvironment(process.env)
assert.equal(approvedDefinition(definition),pinned.definitionSha256) assert.equal(approvedDefinition(definition),pinned.definitionSha256)
const installation=Object.freeze({...subject,roles:Object.fromEntries(roleKeys.map(k=>[k,request.roles[k].signedOid])),roleBindings:request.roles,expectedSource:request.source}) const installation=Object.freeze({...subject,roles:Object.fromEntries(roleKeys.map(k=>[k,request.roles[k].signedOid])),roleBindings:request.roles,expectedSource:request.source})
const verifier=createDatabaseSourceBindingVerifier({registryDirectory:get('source-registry'),installationId:subject.installationId,publicKeySpkiSha256:get('source-spki-sha256'),currentHeadSha256:request.sourceHeadSha256}) const sourcePin={registryDirectory:get('source-registry'),installationId:subject.installationId,publicKeySpkiSha256:get('source-spki-sha256'),currentHeadSha256:request.sourceHeadSha256}
// Preflight is the read-only lifecycle entry for protected registry evidence.
// A refused protection check never falls back to the signature-only verifier.
const verifier=args[0]==='preflight'
?(await import('./writer-profile/protected-source-registry.mjs')).createProtectedDatabaseSourceBindingVerifier(sourcePin)
:createDatabaseSourceBindingVerifier(sourcePin)
const publisher=createConnectedPublisher(installation,(s:Parameters<typeof collectSource44Catalogue>[0])=>collectSource44Catalogue(s,installation),verifier,pinned) const publisher=createConnectedPublisher(installation,(s:Parameters<typeof collectSource44Catalogue>[0])=>collectSource44Catalogue(s,installation),verifier,pinned)
const pool=new pg.Pool(options) const pool=new pg.Pool(options)
// A pool-level async error never becomes raw stdout or a retry signal. // A pool-level async error never becomes raw stdout or a retry signal.
@@ -47,7 +53,7 @@ export async function main(args:string[]){
try{result=args[0]==='preflight'?await publisher.preflightCurrentProfile(pool,{deadlineAt:Date.now()+120000}):args[0]==='reconcile'?await publisher.reconcilePendingPublication(pool,{journalDirectory:get('journal'),deadlineAt:Date.now()+120000}):await publisher.publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256:pinned.expectedHeadSha256,journalDirectory:get('journal'),validityMs:pinned.validityMs,deadlineAt:Date.now()+120000})} try{result=args[0]==='preflight'?await publisher.preflightCurrentProfile(pool,{deadlineAt:Date.now()+120000}):args[0]==='reconcile'?await publisher.reconcilePendingPublication(pool,{journalDirectory:get('journal'),deadlineAt:Date.now()+120000}):await publisher.publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256:pinned.expectedHeadSha256,journalDirectory:get('journal'),validityMs:pinned.validityMs,deadlineAt:Date.now()+120000})}
catch(error){primary=error;failed=true} catch(error){primary=error;failed=true}
try{await pool.end()}catch(error){primary??=error;failed=true} try{await pool.end()}catch(error){primary??=error;failed=true}
if(args[0]==='preflight'&&result?.state==='matched'&&!failed&&!asyncFailure)return {event:'application-writer-profile-preflight',outcome:'matched',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,sourceMatched:true,rolesMatched:true,catalogueMatched:true,writerAuthorized:false,cleanupUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false} if(args[0]==='preflight'&&result?.state==='matched'&&result.platformProtectionVerified===true&&!failed&&!asyncFailure)return {event:'application-writer-profile-preflight',outcome:'matched',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,sourceMatched:true,rolesMatched:true,catalogueMatched:true,platformProtectionVerified:true,writerAuthorized:false,cleanupUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
if(result?.outcome==='committed')return {event:'application-writer-profile-publication',outcome:'committed',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter, if(result?.outcome==='committed')return {event:'application-writer-profile-publication',outcome:'committed',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,
cleanupUnconfirmed:result.cleanupUnconfirmed||asyncFailure||failed,journalUnconfirmed:!!result.journalUnconfirmed,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false} cleanupUnconfirmed:result.cleanupUnconfirmed||asyncFailure||failed,journalUnconfirmed:!!result.journalUnconfirmed,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
if(args[0]==='reconcile'&&result&&!failed&&!asyncFailure&&['idle','confirmed','not-committed'].includes(result.state))return {event:'application-writer-profile-reconciliation',outcome:result.state,headSha256:result.headSha256??null,cleanupUnconfirmed:false,journalUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false} if(args[0]==='reconcile'&&result&&!failed&&!asyncFailure&&['idle','confirmed','not-committed'].includes(result.state))return {event:'application-writer-profile-reconciliation',outcome:result.state,headSha256:result.headSha256??null,cleanupUnconfirmed:false,journalUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
@@ -55,5 +61,5 @@ export async function main(args:string[]){
} }
if(process.argv[1]&&import.meta.url===pathToFileURL(path.resolve(process.argv[1])).href){ if(process.argv[1]&&import.meta.url===pathToFileURL(path.resolve(process.argv[1])).href){
try{const result=await main(process.argv.slice(2));console.log(JSON.stringify(result));if(result.cleanupUnconfirmed||result.journalUnconfirmed)process.exitCode=2} try{const result=await main(process.argv.slice(2));console.log(JSON.stringify(result));if(result.cleanupUnconfirmed||result.journalUnconfirmed)process.exitCode=2}
catch{console.error(JSON.stringify({event:'application-writer-profile-publication-refused',automaticReplay:false,inspectPendingBeforeRetry:true,readyAuthorized:false}));process.exitCode=1} catch{console.error(JSON.stringify({event:['maintenance-inspect','maintenance-resolve'].includes(process.argv[2])?'database-maintenance-command-refused':'application-writer-profile-publication-refused',automaticReplay:false,inspectPendingBeforeRetry:true,readyAuthorized:false}));process.exitCode=1}
} }
@@ -11,9 +11,9 @@ import {readCurrentApplicationWriterProfile} from '../src/db/application-writer-
import {syntheticFixture,stagedModelClient} from '../src/db/application-writer-profile-registry-read-model.test.ts' import {syntheticFixture,stagedModelClient} from '../src/db/application-writer-profile-registry-read-model.test.ts'
const require=createRequire(new URL('../package.json',import.meta.url)),ts=require('typescript'),pg=require('pg') const require=createRequire(new URL('../package.json',import.meta.url)),ts=require('typescript'),pg=require('pg')
const candidate=await fs.readFile(new URL('./writer-profile/publisher.mjs',import.meta.url),'utf8') const candidate=await fs.readFile(new URL('./writer-profile/publisher.mjs',import.meta.url),'utf8')
interface PreflightDiagnostics {state:string;headSha256:string;sourceMatched:boolean;rolesMatched:boolean;catalogueMatched:boolean;writerAuthorized:boolean;generationAuthenticated:boolean;readyAuthorized:boolean} interface PreflightDiagnostics {state:string;headSha256:string;sourceMatched:boolean;rolesMatched:boolean;catalogueMatched:boolean;platformProtectionVerified:boolean;writerAuthorized:boolean;generationAuthenticated:boolean;readyAuthorized:boolean}
interface PublisherModule {createConnectedPublisher(installation:unknown,collect:()=>Promise<unknown>,sourceVerifier:{verify:()=>Promise<void>},approval:unknown):{preflightCurrentProfile(pool:unknown,options:{deadlineAt:number}):Promise<PreflightDiagnostics>}} interface PublisherModule {createConnectedPublisher(installation:unknown,collect:()=>Promise<unknown>,sourceVerifier:{verify:()=>Promise<{platformProtectionVerified:boolean}>},approval:unknown):{preflightCurrentProfile(pool:unknown,options:{deadlineAt:number}):Promise<PreflightDiagnostics>}}
interface OperatorModule {main(args:string[]):Promise<{event:string;outcome:string;writerAuthorized:boolean;readyAuthorized:boolean}>} interface OperatorModule {main(args:string[]):Promise<{event:string;outcome:string;platformProtectionVerified:boolean;writerAuthorized:boolean;readyAuthorized:boolean}>}
function compile<T>(source:string,modules:Record<string,unknown>):T{ function compile<T>(source:string,modules:Record<string,unknown>):T{
const exports:Record<string,unknown>={};const compiled=ts.transpileModule(source.split('\nif(process.argv[1]')[0].replaceAll('import.meta.url',JSON.stringify('file:///candidate-not-cli.mts')),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText const exports:Record<string,unknown>={};const compiled=ts.transpileModule(source.split('\nif(process.argv[1]')[0].replaceAll('import.meta.url',JSON.stringify('file:///candidate-not-cli.mts')),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText
runInNewContext(compiled,{exports,require:(name:string)=>{assert(name in modules,'Unapproved import '+name);return modules[name]},Buffer,URL,TextDecoder,Date,setTimeout,clearTimeout,Object,Error,AggregateError,Promise,Map,Set,process:{env:{},argv:[]}}, {filename:'candidate.mjs'}) runInNewContext(compiled,{exports,require:(name:string)=>{assert(name in modules,'Unapproved import '+name);return modules[name]},Buffer,URL,TextDecoder,Date,setTimeout,clearTimeout,Object,Error,AggregateError,Promise,Map,Set,process:{env:{},argv:[]}}, {filename:'candidate.mjs'})
@@ -56,17 +56,23 @@ function fixture(fault=''){
'../../src/db/application-writer-profile-format.js':format, '../../src/db/application-writer-profile-format.js':format,
'./context.mjs':{ownedContext:async()=>{contexts++;if(fault==='late-actor'&&contexts===2)throw Error('Actor changed');return {pid:1,source}},sameOwned:(a:unknown,b:unknown)=>assert.deepEqual(a,b),sourceCoordinates:(v:{source:typeof source})=>v.source,protectedStructure:async()=>{events.push('roles');if(fault==='role-structure')throw Error('Unsafe role');return security},readCurrent:readCurrentApplicationWriterProfile,HISTORY:'history',CURRENT:'current'}, './context.mjs':{ownedContext:async()=>{contexts++;if(fault==='late-actor'&&contexts===2)throw Error('Actor changed');return {pid:1,source}},sameOwned:(a:unknown,b:unknown)=>assert.deepEqual(a,b),sourceCoordinates:(v:{source:typeof source})=>v.source,protectedStructure:async()=>{events.push('roles');if(fault==='role-structure')throw Error('Unsafe role');return security},readCurrent:readCurrentApplicationWriterProfile,HISTORY:'history',CURRENT:'current'},
} }
const api=compile<PublisherModule>(candidate,modules).createConnectedPublisher(installation,async()=>{events.push('catalogue');if(fault==='catalogue-eof')throw Error('Incomplete EOF');return catalogue},{verify:async()=>{verifications++;events.push('verify-source');if(fault==='source-verifier')throw Error('Unsigned source');if(fault==='late-expiry'&&verifications===2)approval.expiresAt=1}},approval) const api=compile<PublisherModule>(candidate,modules).createConnectedPublisher(installation,async()=>{events.push('catalogue');if(fault==='catalogue-eof')throw Error('Incomplete EOF');return catalogue},{verify:async()=>{verifications++;events.push('verify-source');if(fault==='source-verifier')throw Error('Unsigned source');if(fault==='late-expiry'&&verifications===2)approval.expiresAt=1;return {platformProtectionVerified:!(fault==='initial-unprotected'&&verifications===1||fault==='terminal-unprotected'&&verifications===2)}}},approval)
return {run:()=>api.preflightCurrentProfile(pool,{deadlineAt:Date.now()+30000}),queries,events,f} return {run:()=>api.preflightCurrentProfile(pool,{deadlineAt:Date.now()+30000}),queries,events,f}
} }
test('current44 preflight authenticates signed current and actual component inputs on one read-only lease',async()=>{ test('current44 preflight authenticates signed current and actual component inputs on one read-only lease',async()=>{
const f=fixture(),result=await f.run();assert.equal(result.state,'matched');assert.equal(result.headSha256,f.f.state.current[0].head_sha256) const f=fixture(),result=await f.run();assert.equal(result.state,'matched');assert.equal(result.headSha256,f.f.state.current[0].head_sha256)
assert.equal(result.sourceMatched,true);assert.equal(result.rolesMatched,true);assert.equal(result.catalogueMatched,true) assert.equal(result.sourceMatched,true);assert.equal(result.rolesMatched,true);assert.equal(result.catalogueMatched,true)
assert.equal(result.platformProtectionVerified,true)
assert.equal(result.writerAuthorized,false);assert.equal(result.generationAuthenticated,false);assert.equal(result.readyAuthorized,false) assert.equal(result.writerAuthorized,false);assert.equal(result.generationAuthenticated,false);assert.equal(result.readyAuthorized,false)
assert.equal(f.queries.filter(q=>q.startsWith('BEGIN')).length,1);assert(f.queries.includes('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'));assert.equal(f.queries.filter(q=>q==='ROLLBACK').length,1) assert.equal(f.queries.filter(q=>q.startsWith('BEGIN')).length,1);assert(f.queries.includes('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'));assert.equal(f.queries.filter(q=>q==='ROLLBACK').length,1)
assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT|CREATE|ALTER|DROP)\b/.test(q)));assert.equal(f.events.at(-1),'release:false') assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT|CREATE|ALTER|DROP)\b/.test(q)));assert.equal(f.events.at(-1),'release:false')
assert.deepEqual(f.events.slice(0,6),['capture-source','verify-source','roles','catalogue','verify-source','fresh-source']) assert.deepEqual(f.events.slice(0,6),['capture-source','verify-source','roles','catalogue','verify-source','fresh-source'])
}) })
for(const fault of ['initial-unprotected','terminal-unprotected'])test('preflight cannot promote '+fault+' into protected evidence',async()=>{
const f=fixture(fault),result=await f.run();assert.equal(result.platformProtectionVerified,false)
assert.equal(result.writerAuthorized,false);assert.equal(result.readyAuthorized,false)
assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT|CREATE|ALTER|DROP)\b/.test(q)))
})
for(const fault of ['tamper','empty','expired','signed-source','signed-role','signed-catalogue','signed-definition','signed-manifest','role-structure','catalogue-eof','source-verifier','late-source','late-actor','late-expiry','late-clock','rollback-ack'])test('preflight refuses '+fault+' without any write or replay',async()=>{ for(const fault of ['tamper','empty','expired','signed-source','signed-role','signed-catalogue','signed-definition','signed-manifest','role-structure','catalogue-eof','source-verifier','late-source','late-actor','late-expiry','late-clock','rollback-ack'])test('preflight refuses '+fault+' without any write or replay',async()=>{
const f=fixture(fault);await assert.rejects(f.run());assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT|CREATE|ALTER|DROP)\b/.test(q)));assert.equal(f.queries.filter(q=>q.startsWith('BEGIN')).length,1) const f=fixture(fault);await assert.rejects(f.run());assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT|CREATE|ALTER|DROP)\b/.test(q)));assert.equal(f.queries.filter(q=>q.startsWith('BEGIN')).length,1)
}) })
@@ -74,23 +80,28 @@ test('existing operator main dispatches preflight only after protected input val
const cli=await fs.readFile(new URL('./database-writer-profile-operator.mts',import.meta.url),'utf8'),f=syntheticFixture('1'),reads:string[]=[],events:string[]=[] const cli=await fs.readFile(new URL('./database-writer-profile-operator.mts',import.meta.url),'utf8'),f=syntheticFixture('1'),reads:string[]=[],events:string[]=[]
const request={installationId:f.subject.installationId,roles:Object.fromEntries(['runtime','operator','capture','publisher','control'].map((k,i)=>[k,{name:k,signedOid:String(i+11)}])),source:f.policy.source,sourceHeadSha256:'a'.repeat(64),tls:{caSha256:'a'.repeat(64)}} const request={installationId:f.subject.installationId,roles:Object.fromEntries(['runtime','operator','capture','publisher','control'].map((k,i)=>[k,{name:k,signedOid:String(i+11)}])),source:f.policy.source,sourceHeadSha256:'a'.repeat(64),tls:{caSha256:'a'.repeat(64)}}
const files:Record<string,unknown>={bootstrap:{format:'application-writer-bootstrap-v1',publicationAuthorized:false,readyAuthorized:false,deploymentRequest:request,verificationSubject:f.subject},approval:{expiresAt:Date.now()+60000,definitionSha256:'b'.repeat(64)},definition:{},transport:{},ca:'fixture'} const files:Record<string,unknown>={bootstrap:{format:'application-writer-bootstrap-v1',publicationAuthorized:false,readyAuthorized:false,deploymentRequest:request,verificationSubject:f.subject},approval:{expiresAt:Date.now()+60000,definitionSha256:'b'.repeat(64)},definition:{},transport:{},ca:'fixture'}
let failEnd=false,asyncError=false let failEnd=false,asyncError=false,protectedRefused=false
const retainedPools:Pool[]=[] const retainedPools:Pool[]=[]
class Pool extends EventEmitter{constructor(){super();retainedPools.push(this)}async end(){events.push('pool-end');if(failEnd)throw Error('Pool cleanup unconfirmed')}} class Pool extends EventEmitter{constructor(){super();retainedPools.push(this)}async end(){events.push('pool-end');if(failEnd)throw Error('Pool cleanup unconfirmed')}}
const result={state:'matched',epoch:'1',headSha256:'c'.repeat(64),notAfter:Date.now()+60000} const result={state:'matched',epoch:'1',headSha256:'c'.repeat(64),notAfter:Date.now()+60000,platformProtectionVerified:true}
const protectedVerifier={verify:async()=>({platformProtectionVerified:true})}
const modules={ const modules={
'node:assert/strict':assert,'node:path':require('node:path'),'node:url':require('node:url'),'node:crypto':require('node:crypto'), 'node:assert/strict':assert,'node:path':require('node:path'),'node:url':require('node:url'),'node:crypto':require('node:crypto'),
'./writer-profile/operator-files.mjs':{readProtected:async(file:string)=>{reads.push(file);assert(file in files);return Buffer.from(JSON.stringify(files[file]))},protectedDirectory:async()=>{},sha:format.sha}, './writer-profile/operator-files.mjs':{readProtected:async(file:string)=>{reads.push(file);assert(file in files);return Buffer.from(JSON.stringify(files[file]))},protectedDirectory:async()=>{},sha:format.sha},
'./writer-profile/operator-contract.mjs':{approval:(value:unknown)=>{events.push('approval');return value},reconciliationApproval:()=>{throw Error('No expired reconciliation bypass')},poolOptions:()=>{events.push('transport');return {}},roleKeys:['runtime','operator','capture','publisher','control'],assertJsDriverEnvironment:()=>{}}, './writer-profile/operator-contract.mjs':{approval:(value:unknown)=>{events.push('approval');return value},reconciliationApproval:()=>{throw Error('No expired reconciliation bypass')},poolOptions:()=>{events.push('transport');return {}},roleKeys:['runtime','operator','capture','publisher','control'],assertJsDriverEnvironment:()=>{}},
'./database-writer-bootstrap-operator.mjs':{deploymentRequest:(value:unknown)=>value},'../src/db/database-source-binding.js':{createDatabaseSourceBindingVerifier:()=>({})}, './database-writer-bootstrap-operator.mjs':{deploymentRequest:(value:unknown)=>value},'../src/db/database-source-binding.js':{createDatabaseSourceBindingVerifier:()=>{throw Error('Preflight must never fall back to unprotected source verification')}},
pg:{Pool},'./writer-profile/publisher.mjs':{createConnectedPublisher:()=>({preflightCurrentProfile:async()=>{events.push('preflight');if(asyncError){const activePool=retainedPools.at(-1);assert(activePool);activePool.emit('error',Error('Async pool fault'))}return result},publishCurrentProfile:()=>{throw Error('No publish')},reconcilePendingPublication:()=>{throw Error('No reconcile')}})}, './writer-profile/protected-source-registry.mjs':{createProtectedDatabaseSourceBindingVerifier:(pin:{registryDirectory:string;currentHeadSha256:string})=>{events.push('protected-source');assert.equal(pin.registryDirectory,'source-registry');assert.equal(pin.currentHeadSha256,request.sourceHeadSha256);if(protectedRefused)throw Error('Protected source refused');return protectedVerifier}},
pg:{Pool},'./writer-profile/publisher.mjs':{createConnectedPublisher:(_installation:unknown,_collect:unknown,verifier:unknown)=>{assert.equal(verifier,protectedVerifier);return {preflightCurrentProfile:async()=>{events.push('preflight');if(asyncError){const activePool=retainedPools.at(-1);assert(activePool);activePool.emit('error',Error('Async pool fault'))}return result},publishCurrentProfile:()=>{throw Error('No publish')},reconcilePendingPublication:()=>{throw Error('No reconcile')}}}},
'./writer-profile/catalogue.mjs':{collectSource44Catalogue:()=>{},approvedDefinition:()=> 'b'.repeat(64)}, './writer-profile/catalogue.mjs':{collectSource44Catalogue:()=>{},approvedDefinition:()=> 'b'.repeat(64)},
} }
const flags=['bootstrap','bootstrap-sha256','approval','approval-sha256','definition','definition-sha256','transport','transport-sha256','ca','source-registry','source-spki-sha256','journal'],args=['preflight',...flags.flatMap(flag=>['--'+flag,flag.endsWith('sha256')?'a'.repeat(64):flag])] const flags=['bootstrap','bootstrap-sha256','approval','approval-sha256','definition','definition-sha256','transport','transport-sha256','ca','source-registry','source-spki-sha256','journal'],args=['preflight',...flags.flatMap(flag=>['--'+flag,flag.endsWith('sha256')?'a'.repeat(64):flag])]
const output=await compile<OperatorModule>(cli,modules).main(args) const output=await compile<OperatorModule>(cli,modules).main(args)
assert.equal(output.event,'application-writer-profile-preflight');assert.equal(output.outcome,'matched');assert.equal(output.writerAuthorized,false);assert.equal(output.readyAuthorized,false) assert.equal(output.event,'application-writer-profile-preflight');assert.equal(output.outcome,'matched');assert.equal(output.platformProtectionVerified,true);assert.equal(output.writerAuthorized,false);assert.equal(output.readyAuthorized,false)
assert.deepEqual(reads,['bootstrap','approval','definition','transport','ca']);assert.deepEqual(events,['approval','transport','preflight','pool-end']) assert.deepEqual(reads,['bootstrap','approval','definition','transport','ca']);assert.deepEqual(events,['approval','transport','protected-source','preflight','pool-end'])
failEnd=true;await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Pool cleanup unconfirmed/) failEnd=true;await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Pool cleanup unconfirmed/)
failEnd=false;asyncError=true;await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Publisher terminal unconfirmed/) failEnd=false;asyncError=true;await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Publisher terminal unconfirmed/)
asyncError=false;result.platformProtectionVerified=false;await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Publisher terminal unconfirmed/)
result.platformProtectionVerified=true;protectedRefused=true;const before=retainedPools.length
await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Protected source refused/);assert.equal(retainedPools.length,before,'Protection failure must not open a Pool or fall back')
}) })
@@ -0,0 +1,73 @@
/** External SDK consumer. No application/store/runtime/database imports. */
import assert from 'node:assert/strict'
import { randomUUID } from 'node:crypto'
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'
const endpoint = new URL(process.env.OWN_MCP_ENDPOINT)
assert.equal(process.env.OWN_EXTERNAL_CLIENT, 'true')
assert.equal(endpoint.hostname, '127.0.0.1')
assert.notEqual(process.pid, Number(process.env.OWN_SERVER_PID))
const client = new Client({ name: 'owned-linked-bridge-vlan-sdk', version: '1.0.0' })
const boundedFetch = async (input, init) => {
const url = new URL(typeof input === 'string' || input instanceof URL ? input : input.url)
assert.equal(url.origin, endpoint.origin)
return fetch(input, { ...init, redirect: 'manual', signal: AbortSignal.timeout(10000) })
}
const payload = result => {
const text = result.content.find(item => item.type === 'text')?.text
assert(text && Buffer.byteLength(text) < 131072)
return JSON.parse(text)
}
const call = async (name, args) => {
const result = await client.callTool({ name, arguments: args }, undefined, { timeout: 10000 })
assert(!result.isError, JSON.stringify(payload(result)))
return payload(result)
}
try {
await client.connect(new StreamableHTTPClientTransport(endpoint, {
requestInit: { headers: { authorization: 'Bearer ' + process.env.OWN_MCP_TOKEN } }, fetch: boundedFetch,
}), { timeout: 10000 })
const tools = (await client.listTools({}, { timeout: 10000 })).tools
const read = tools.find(tool => tool.name === 'mm_bridge_vlan_configuration_get')
const configure = tools.find(tool => tool.name === 'mm_bridge_vlan_configuration_plan')
assert(read && configure)
assert.match(read.description, /Полная конфигурация bridge VLAN/)
assert.match(read.description, /Возвращает данные только из разрешённой области/)
assert.match(configure.description, /Связанный план VLAN membership, PVID и bridge filtering/)
assert.match(configure.description, /Подготавливает неизменяемый план/)
assert.equal(read.annotations.readOnlyHint, true)
assert.equal(configure.annotations.readOnlyHint, false)
assert.deepEqual(read.inputSchema.required, ['serverId'])
assert.deepEqual(configure.inputSchema.required, ['serverId', 'bridgeId', 'desired'])
const desiredSchema = configure.inputSchema.properties.desired
for (const name of ['ports', 'vlans', 'removePortIds', 'removeVlanIds']) assert(desiredSchema.properties[name])
const serverId = Number(process.env.OWN_ALLOWED_SERVER)
const state = (await call(read.name, { serverId })).state
assert.equal(state.bridges[0]['.id'], '*1')
assert(state.interfaces.some(item => item.resource === 'ethernet' && item.id === '*3' && item.name === 'ether3'))
assert.equal(state.vlans[0]['vlan-ids'], '10')
const cpu = { resource: 'bridge', id: '*1' }, access = { resource: 'ethernet', id: '*2' }, trunk = { resource: 'ethernet', id: '*3' }
const plan = await call(configure.name, { serverId, bridgeId: '*1', desired: {
filtering: true, pvid: 1,
ports: [{ id: '*10', interface: access, pvid: 10, ingressFiltering: true, frameTypes: 'admit-all', disabled: false },
{ interface: trunk, pvid: 1, ingressFiltering: true, frameTypes: 'admit-only-vlan-tagged', disabled: false }],
vlans: [{ id: '*11', vlanIds: '10', tagged: [cpu], untagged: [access], disabled: false },
{ vlanIds: '20', tagged: [cpu, trunk], untagged: [], disabled: false }], removePortIds: [], removeVlanIds: [],
} })
assert.equal(plan.state, 'awaiting-approval')
assert.equal(typeof plan.planId, 'string')
assert.equal(plan.changes.requiresHuman, true)
assert.deepEqual(plan.changes.steps.map(item => [item.step.resource, item.step.action]),
[['bridge_vlan', 'create'], ['bridge_port', 'create'], ['bridge', 'update']])
const denied = await client.callTool({ name: 'mm_change_apply', arguments: { planId: plan.planId, idempotencyKey: randomUUID() } }, undefined, { timeout: 10000 })
assert.equal(denied.isError, true)
const deniedPayload = payload(denied)
assert(JSON.stringify(deniedPayload).includes('APPROVAL_REQUIRED'))
const readback = (await call(read.name, { serverId })).state
assert.deepEqual(readback, state)
process.send({ kind: 'result', result: { externalSdkProcess: true, transport: 'streamable-http', clientPid: process.pid,
listedTools: [read, configure], state, plan, applyDenial: deniedPayload, unchangedReadback: true } })
} finally {
await client.close()
process.disconnect?.()
}
@@ -0,0 +1,84 @@
import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import { createServer } from 'node:http'
import { writeFile } from 'node:fs/promises'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { isAbsolute, resolve } from 'node:path'
assert.equal(process.env.OWN_EXTERNAL_PROCESS_PG, 'true')
assert.equal(process.env.NODE_ENV, 'test')
assert(isAbsolute(process.env.OWN_EXTERNAL_SOURCE_ROOT!))
assert(isAbsolute(process.env.OWN_EXTERNAL_RESULT_FILE!))
const database = new URL(process.env.DATABASE_URL!)
assert.equal(database.hostname, '127.0.0.1'); assert.equal(database.username, 'mcp_test')
assert.equal(database.pathname, '/postgres'); assert.notEqual(database.port, '5432')
const source = (path: string) => pathToFileURL(resolve(process.env.OWN_EXTERNAL_SOURCE_ROOT!, 'backend/src', path)).href
const rows: Record<string, Record<string, string>[]> = {
'/interface/ethernet': [{ '.id': '*2', name: 'ether2', disabled: 'false' }, { '.id': '*3', name: 'ether3', disabled: 'false' }, { '.id': '*4', name: 'ether-mgmt', disabled: 'false' }],
'/interface/bridge': [{ '.id': '*1', name: 'br-users', pvid: '1', 'vlan-filtering': 'false', 'frame-types': 'admit-all', 'ingress-filtering': 'true', mvrp: 'false', disabled: 'false' }],
'/interface/bridge/port': [{ '.id': '*10', bridge: 'br-users', interface: 'ether2', pvid: '10', 'ingress-filtering': 'true', 'frame-types': 'admit-all', disabled: 'false' }],
'/interface/bridge/vlan': [{ '.id': '*11', bridge: 'br-users', 'vlan-ids': '10', tagged: 'br-users', untagged: 'ether2', disabled: 'false', dynamic: 'false' }],
'/interface/vlan': [], '/interface/gre': [], '/interface/wireguard': [], '/interface/vxlan': [],
'/ip/address': [{ '.id': '*20', interface: 'ether-mgmt', address: '127.0.0.1/8', disabled: 'false', dynamic: 'false' }], '/ipv6/address': [],
}
const requests: string[] = []
let writeAttempts = 0
const router = createServer((req, res) => {
res.setHeader('content-type', 'application/json')
const path = decodeURIComponent(new URL(req.url!, 'http://127.0.0.1').pathname.replace(/^\/rest/, ''))
requests.push(req.method + ' ' + path)
if (req.method !== 'GET') { writeAttempts++; res.writeHead(405); res.end('{}'); return }
if (req.headers.authorization !== 'Basic ' + Buffer.from('fixture:fixture-only').toString('base64')) { res.writeHead(401); res.end('{}'); return }
if (!Object.hasOwn(rows, path)) { res.writeHead(404); res.end('{}'); return }
res.end(JSON.stringify(rows[path]))
})
await new Promise<void>(done => router.listen(0, '127.0.0.1', done))
const { pool, closePool } = await import(source('db/index.js'))
const { applySqlMigrations } = await import(source('db/migrate.js'))
const { mcpConnectionCreateSchema } = await import('@mmapp/contracts/mcp')
const { createMcpConnection, updateMcpSettings } = await import(source('modules/mcp/store.js'))
const { buildApp } = await import(source('index.js'))
const actor = { id: 'owned-bridge-vlan-sdk-owner', email: 'bridge-sdk@example.invalid', name: 'Owned bridge VLAN SDK owner', apps: ['mm'], permissions: ['mm:settings:admin', 'mm:mcp:admin', 'mm:network:admin', 'mm:servers:admin'], expiresAt: Date.now() + 3600000, issuedAt: Date.now() - 1000 }
let app: Awaited<ReturnType<typeof buildApp>> | undefined
let child: ReturnType<typeof spawn> | undefined
let childExit: Promise<number> | undefined
try {
await applySqlMigrations(pool)
const serverId = (await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password) VALUES('owned bridge VLAN SDK','127.0.0.1',$1,false,true,'fixture','fixture-only') RETURNING id", [(router.address() as { port: number }).port])).rows[0].id
await updateMcpSettings({ enabled: true }, actor)
app = await buildApp({ startScheduler: false, logger: false })
await app.listen({ host: '127.0.0.1', port: 0 })
const endpoint = `http://127.0.0.1:${(app.server.address() as { port: number }).port}/mcp`
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: 'Owned bridge VLAN external SDK', mode: 'autonomous',
expiresAt: new Date(Date.now() + 1200000).toISOString(), permissions: ['mm:network:write'],
scope: { serverIds: [serverId], operationIds: ['network.bridge-vlan.read', 'network.bridge-vlan.configure', 'changes.apply'], allowGlobal: false },
limits: { callsPerMinute: 120, mutationsPerHour: 100 } }), actor)
let result: unknown, stdout = '', stderr = ''
child = spawn(process.execPath, [fileURLToPath(new URL('./bridge-vlan-client.mjs', import.meta.url))], { cwd: process.cwd(), windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'], env: { ...Object.fromEntries(['SystemRoot', 'WINDIR', 'TEMP', 'TMP', 'PATH', 'Path'].filter(k => process.env[k]).map(k => [k, process.env[k]!])),
OWN_EXTERNAL_CLIENT: 'true', OWN_MCP_ENDPOINT: endpoint, OWN_MCP_TOKEN: issued.token, OWN_SERVER_PID: String(process.pid), OWN_ALLOWED_SERVER: String(serverId) } })
child.on('message', (message: { kind: string; result?: unknown }) => { if (message.kind === 'result') result = message.result })
child.stdout?.on('data', chunk => { stdout += String(chunk); if (stdout.length > 1048576) child?.kill() })
child.stderr?.on('data', chunk => { stderr += String(chunk); if (stderr.length > 1048576) child?.kill() })
childExit = new Promise<number>((done, reject) => { child!.once('exit', code => done(code ?? 1)); child!.once('error', reject) })
const timeout = setTimeout(() => child?.kill(), 60000)
const code = await childExit; clearTimeout(timeout)
assert.equal(code, 0, stderr.slice(0, 3000)); assert(result)
assert.equal(writeAttempts, 0)
assert.equal((await pool.query('SELECT count(*)::int AS count FROM mcp_operations')).rows[0].count, 0)
const plans = (await pool.query('SELECT operation_id,state,approved_by FROM mcp_plans')).rows
assert.equal(plans.length, 1); assert.equal(plans[0].operation_id, 'network.bridge-vlan.configure')
assert.equal(plans[0].state, 'awaiting-approval'); assert.equal(plans[0].approved_by, null)
const audit = (await pool.query('SELECT tool,status,error_code FROM mcp_requests ORDER BY created_at')).rows
assert(audit.some(row => row.error_code === 'APPROVAL_REQUIRED'))
const serialized = JSON.stringify({ result, stdout, stderr, audit })
assert(!serialized.includes(issued.token)); assert(!serialized.includes('fixture-only'))
const final = { ...(result as object), actualOwnPg: true, actualProductionHttpServer: true, serverFixturePid: process.pid,
connectionMode: 'autonomous', plans, audit, nativeReadRequests: requests, nativeWriteAttempts: writeAttempts,
appliedOperations: 0, syntheticRouterOnly: true, productionDatabaseTouched: false, portalContacted: false }
await writeFile(process.env.OWN_EXTERNAL_RESULT_FILE!, JSON.stringify(final, null, 2))
console.log('Owned bridge VLAN SDK passed:', JSON.stringify({ listedTools: 2, planState: plans[0].state, nativeReadRequests: requests.length, nativeWriteAttempts: writeAttempts }))
} finally {
if (child && child.exitCode === null && child.signalCode === null) { child.kill(); await childExit }
try { await app?.close() } finally { try { await closePool() } finally { router.closeIdleConnections(); await new Promise<void>((done, reject) => router.close(error => error ? reject(error) : done())) } }
}
@@ -6,10 +6,14 @@ import { randomBytes, createHash } from 'node:crypto'
import { createServer } from 'node:net' import { createServer } from 'node:net'
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
const sourceRoot = resolve(process.argv[2] ?? fileURLToPath(new URL('../../../', import.meta.url))) const sourceRoot = resolve(process.argv[2] ?? fileURLToPath(new URL('../../../', import.meta.url)))
const output = resolve(sourceRoot, 'tmp/mcp-external-process-acceptance'); await mkdir(output, { recursive: true }) const scenario = process.argv[3] ?? 'acceptance'
if (!['acceptance', 'bridge-vlan'].includes(scenario)) throw Error('Unsupported owned external scenario')
const output = resolve(sourceRoot, scenario === 'bridge-vlan' ? 'tmp/mcp-bridge-vlan-sdk-20261105' : 'tmp/mcp-external-process-acceptance'); await mkdir(output, { recursive: true })
const paths = ['backend/src/scripts/mcp-stdio.ts', 'backend/src/routes/mcp-transport.ts', 'backend/src/routes/mcp-oauth.ts', 'backend/src/modules/mcp/store.ts', const paths = ['backend/src/scripts/mcp-stdio.ts', 'backend/src/routes/mcp-transport.ts', 'backend/src/routes/mcp-oauth.ts', 'backend/src/modules/mcp/store.ts',
'backend/src/modules/mcp/runtime.ts', 'backend/src/modules/mcp/actions.ts', 'backend/src/modules/mcp/security.ts', 'backend/src/modules/mcp/protocol-limits.ts', 'backend/src/modules/mcp/runtime.ts', 'backend/src/modules/mcp/actions.ts', 'backend/src/modules/mcp/security.ts', 'backend/src/modules/mcp/protocol-limits.ts',
'backend/src/modules/mcp/portal-revocation.ts', 'backend/src/plugins/auth.ts', 'backend/src/index.ts', 'node_modules/@modelcontextprotocol/client/package.json', 'node_modules/@modelcontextprotocol/server/package.json'] 'backend/src/modules/mcp/portal-revocation.ts', 'backend/src/plugins/auth.ts', 'backend/src/index.ts', 'node_modules/@modelcontextprotocol/client/package.json', 'node_modules/@modelcontextprotocol/server/package.json']
if (scenario === 'bridge-vlan') paths.push('backend/scripts/mcp-external-process/bridge-vlan-client.mjs', 'backend/scripts/mcp-external-process/bridge-vlan-fixture.ts',
'backend/src/modules/mcp/bridge-vlan-actions.ts', 'backend/src/services/bridge-vlan-configuration.ts', 'packages/contracts/src/bridge-vlan.ts')
async function pins() { const result = []; for (const path of paths) result.push({ path, sha256: createHash('sha256').update(await readFile(resolve(sourceRoot, path))).digest('hex') }); return result } async function pins() { const result = []; for (const path of paths) result.push({ path, sha256: createHash('sha256').update(await readFile(resolve(sourceRoot, path))).digest('hex') }); return result }
const before = await pins(), directory = await mkdtemp(join(resolve(sourceRoot, 'tmp'), 'mm-mcp-external-')), state = join(directory, 'state'); await mkdir(state) const before = await pins(), directory = await mkdtemp(join(resolve(sourceRoot, 'tmp'), 'mm-mcp-external-')), state = join(directory, 'state'); await mkdir(state)
const listener = createServer(); await new Promise(resolve => listener.listen(0, '127.0.0.1', resolve)); const port = listener.address().port; await new Promise(resolve => listener.close(resolve)) const listener = createServer(); await new Promise(resolve => listener.listen(0, '127.0.0.1', resolve)); const port = listener.address().port; await new Promise(resolve => listener.close(resolve))
@@ -19,7 +23,7 @@ const postgres = new EmbeddedPostgres({ databaseDir: join(directory, 'pg'), port
try { try {
await postgres.initialise(); await postgres.start() await postgres.initialise(); await postgres.start()
process.exitCode = await new Promise((done, reject) => { process.exitCode = await new Promise((done, reject) => {
const child = spawn(process.execPath, ['--import', import.meta.resolve('tsx'), fileURLToPath(new URL('./server-fixture.ts', import.meta.url))], { cwd: directory, const child = spawn(process.execPath, ['--import', import.meta.resolve('tsx'), fileURLToPath(new URL(scenario === 'bridge-vlan' ? './bridge-vlan-fixture.ts' : './server-fixture.ts', import.meta.url))], { cwd: directory,
windowsHide: true, stdio: 'inherit', env: { ...Object.fromEntries(['SystemRoot', 'WINDIR', 'TEMP', 'TMP', 'PATH', 'Path'].filter(k => process.env[k]).map(k => [k, process.env[k]])), windowsHide: true, stdio: 'inherit', env: { ...Object.fromEntries(['SystemRoot', 'WINDIR', 'TEMP', 'TMP', 'PATH', 'Path'].filter(k => process.env[k]).map(k => [k, process.env[k]])),
OWN_EXTERNAL_CLEANUP_FAULT: process.env.OWN_EXTERNAL_CLEANUP_FAULT, OWN_EXTERNAL_SOURCE_ROOT: sourceRoot, OWN_EXTERNAL_RESULT_FILE: join(output, 'result.json'), NODE_ENV: 'test', OWN_EXTERNAL_PROCESS_PG: 'true', DATABASE_URL: `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres`, DATABASE_PATH: join(directory, 'no-legacy.sqlite'), OWN_EXTERNAL_CLEANUP_FAULT: process.env.OWN_EXTERNAL_CLEANUP_FAULT, OWN_EXTERNAL_SOURCE_ROOT: sourceRoot, OWN_EXTERNAL_RESULT_FILE: join(output, 'result.json'), NODE_ENV: 'test', OWN_EXTERNAL_PROCESS_PG: 'true', DATABASE_URL: `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres`, DATABASE_PATH: join(directory, 'no-legacy.sqlite'),
MCP_STATE_DIR: state, AUTH_REQUIRED: 'true', AUTH_JWT_SECRET: 'owned-external-synthetic-issuer-test-only', AUTH_ISSUER: 'https://synthetic-portal.invalid', AUTH_PORTAL_URL: 'https://synthetic-portal.invalid' } }) MCP_STATE_DIR: state, AUTH_REQUIRED: 'true', AUTH_JWT_SECRET: 'owned-external-synthetic-issuer-test-only', AUTH_ISSUER: 'https://synthetic-portal.invalid', AUTH_PORTAL_URL: 'https://synthetic-portal.invalid' } })
@@ -1,4 +1,5 @@
import assert from "node:assert/strict" import assert from "node:assert/strict"
import { createHash } from "node:crypto"
import { spawn } from "node:child_process" import { spawn } from "node:child_process"
import { createServer } from "node:net" import { createServer } from "node:net"
import { fileURLToPath, pathToFileURL } from "node:url" import { fileURLToPath, pathToFileURL } from "node:url"
@@ -49,6 +50,61 @@ try {
await updateMcpSettings({ enabled: true }, actor) await updateMcpSettings({ enabled: true }, actor)
app = await buildApp({ startScheduler: false, logger: false }); await app.listen({ port, host: "127.0.0.1" }) app = await buildApp({ startScheduler: false, logger: false }); await app.listen({ port, host: "127.0.0.1" })
const jwt = await new SignJWT({ sub: actor.id, apps: actor.apps, permissions: actor.permissions }).setProtectedHeader({ alg: "HS256" }).setIssuer(env.authIssuer).setIssuedAt().setExpirationTime("1h").sign(new TextEncoder().encode(env.jwtSecret)) const jwt = await new SignJWT({ sub: actor.id, apps: actor.apps, permissions: actor.permissions }).setProtectedHeader({ alg: "HS256" }).setIssuer(env.authIssuer).setIssuedAt().setExpirationTime("1h").sign(new TextEncoder().encode(env.jwtSecret))
const portalHeaders = { authorization: `Bearer ${jwt}`, "content-type": "application/json" }
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_connections WHERE owner_id=$1", [actor.id])).rows[0].count, 0)
const callback = "http://localhost:45678/callback?registered=1"
const registration = await fetch(origin + "/api/mcp/oauth/clients", { method: "POST", headers: portalHeaders,
body: JSON.stringify({ name: "Owned denial fixture", redirectUris: [callback] }) })
assert.equal(registration.status, 200)
const { clientId } = await registration.json() as { clientId: string }
const consent = (payload: Record<string, unknown>, headers = portalHeaders) => fetch(origin + "/api/mcp/oauth/consent", { method: "POST", headers, body: JSON.stringify(payload) })
const authorize = async (state: string) => {
const parameters = new URLSearchParams({ client_id: clientId, redirect_uri: callback, response_type: "code", resource: endpoint,
code_challenge_method: "S256", code_challenge: createHash("sha256").update("v".repeat(64)).digest("base64url"), state })
const response = await fetch(`${origin}/oauth/authorize?${parameters}`, { redirect: "manual" })
assert.equal(response.status, 302)
const requestId = new URL(response.headers.get("location")!).searchParams.get("oauthRequest")
assert.ok(requestId)
return requestId
}
const requestId = await authorize("deny-without-connection")
assert.equal((await consent({ requestId, approve: false }, { ...portalHeaders, authorization: "" })).status, 401)
assert.equal((await consent({ requestId, approve: true })).status, 400)
const refusals = await Promise.all([consent({ requestId, approve: false }), consent({ requestId, approve: false })])
assert.deepEqual(refusals.map(response => response.status).sort(), [200, 404])
const { redirectUrl } = await refusals.find(response => response.status === 200)!.json() as { redirectUrl: string }
const redirect = new URL(redirectUrl)
const expected = new URL(callback)
expected.searchParams.set("state", "deny-without-connection")
expected.searchParams.set("iss", origin)
expected.searchParams.set("error", "access_denied")
assert.equal(redirect.href, expected.href)
const otherOwner = { ...actor, id: "owned-external-process-other-owner" }
const unrelated = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Unrelated paused connection", mode: "observer",
expiresAt: new Date(Date.now() + 600000).toISOString(), permissions: ["mm:servers:read"],
scope: { serverIds: [Number(allowed)], operationIds: ["servers.list"], allowGlobal: false } }), otherOwner)
await setConnectionStatus(unrelated.connection.id, "paused", otherOwner)
const legacyRequestId = await authorize("legacy-denial")
const legacy = await consent({ requestId: legacyRequestId, connectionId: unrelated.connection.id, approve: false })
assert.equal(legacy.status, 200)
assert.equal((await consent({ requestId: legacyRequestId, connectionId: unrelated.connection.id, approve: false })).status, 404)
const expired = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Expired connection", mode: "observer",
expiresAt: new Date(Date.now() + 600000).toISOString(), permissions: ["mm:servers:read"],
scope: { serverIds: [Number(allowed)], operationIds: ["servers.list"], allowGlobal: false } }), actor)
await pool.query("UPDATE mcp_connections SET policy=jsonb_set(policy,'{expiresAt}',to_jsonb($2::text)) WHERE id=$1", [expired.connection.id, "2000-01-01T00:00:00Z"])
const expiredRequestId = await authorize("expired-approval")
assert.equal((await consent({ requestId: expiredRequestId, connectionId: expired.connection.id, approve: true })).status, 403)
const expiredRequest = (await pool.query("SELECT consumed_at,code_digest,connection_id FROM mcp_oauth_requests WHERE id=$1", [expiredRequestId])).rows[0]
assert.deepEqual(expiredRequest, { consumed_at: null, code_digest: null, connection_id: null })
assert.equal((await consent({ requestId: expiredRequestId, approve: false })).status, 200)
const deniedRows = (await pool.query("SELECT consumed_at,code_digest,connection_id FROM mcp_oauth_requests WHERE client_id=$1", [clientId])).rows
assert.equal(deniedRows.length, 3)
assert(deniedRows.every(row => row.consumed_at instanceof Date && row.code_digest === null && row.connection_id === null))
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_credentials WHERE client_id=$1", [clientId])).rows[0].count, 0)
const denialAudit = (await pool.query("SELECT actor_id,connection_id,payload FROM mcp_audit WHERE action='oauth.consent' AND payload->>'clientId'=$1", [clientId])).rows
assert.equal(denialAudit.length, 3)
assert(denialAudit.every(row => row.actor_id === actor.id && row.connection_id === null && row.payload.approved === false))
console.log("OAuth consent: expired approval refused before code issuance, denial uses exact redirect and one-use request without credentials")
const transcript: unknown[] = []; let failure: unknown, result: unknown const transcript: unknown[] = []; let failure: unknown, result: unknown
child = spawn(process.execPath, [fileURLToPath(new URL("./client.mjs", import.meta.url))], { cwd: process.cwd(), windowsHide: true, stdio: ["ignore", "pipe", "pipe", "ipc"], child = spawn(process.execPath, [fileURLToPath(new URL("./client.mjs", import.meta.url))], { cwd: process.cwd(), windowsHide: true, stdio: ["ignore", "pipe", "pipe", "ipc"],
env: { ...Object.fromEntries(["SystemRoot", "WINDIR", "TEMP", "TMP", "PATH", "Path"].filter(k => process.env[k]).map(k => [k, process.env[k]!])), env: { ...Object.fromEntries(["SystemRoot", "WINDIR", "TEMP", "TMP", "PATH", "Path"].filter(k => process.env[k]).map(k => [k, process.env[k]!])),
@@ -90,7 +146,8 @@ try {
const logs = JSON.stringify((await pool.query("SELECT tool,input,result,error_code FROM mcp_requests ORDER BY created_at")).rows) const logs = JSON.stringify((await pool.query("SELECT tool,input,result,error_code FROM mcp_requests ORDER BY created_at")).rows)
for (const token of [...credentials, jwt, secret]) { assert(!logs.includes(token)); assert(!stdout.includes(token)); assert(!stderr.includes(token)) } for (const token of [...credentials, jwt, secret]) { assert(!logs.includes(token)); assert(!stdout.includes(token)); assert(!stderr.includes(token)) }
const final = { ...(result as object), actualOwnPg: true, actualProductionHttpServer: true, serverFixturePid: process.pid, const final = { ...(result as object), actualOwnPg: true, actualProductionHttpServer: true, serverFixturePid: process.pid,
controlTransitions: transcript, auditCredentialLeakAbsent: true, nativeRouterCalls: 0, productionDatabaseTouched: false } controlTransitions: transcript, auditCredentialLeakAbsent: true, denialWithoutConnection: true,
legacyDenialIgnoresPausedForeignConnection: true, nativeRouterCalls: 0, productionDatabaseTouched: false }
await writeFile(process.env.OWN_EXTERNAL_RESULT_FILE!, JSON.stringify(final, null, 2)) await writeFile(process.env.OWN_EXTERNAL_RESULT_FILE!, JSON.stringify(final, null, 2))
console.log("Owned external process SDK result:", JSON.stringify(final)) console.log("Owned external process SDK result:", JSON.stringify(final))
} finally { } finally {
@@ -0,0 +1,29 @@
/** Build-time only: copy the exact operator inputs into a scoped Docker context. */
import assert from 'node:assert/strict'
import {constants} from 'node:fs'
import {copyFile,lstat,mkdir} from 'node:fs/promises'
import path from 'node:path'
import {fileURLToPath,pathToFileURL} from 'node:url'
import {OPERATOR_SOURCES} from './database-operator-sources.mjs'
const scriptsDirectory=path.dirname(fileURLToPath(import.meta.url))
const buildInputs=Object.freeze(['build-database-operator.mjs','database-operator-sources.mjs',...OPERATOR_SOURCES])
export async function stageDatabaseOperatorScripts(destination,source=scriptsDirectory){
const from=path.resolve(source),to=path.resolve(destination)
assert(from!==to,'A separate operator staging directory is required')
// Refuse an incomplete source tree before writing any staged files.
for(const file of buildInputs)assert((await lstat(path.join(from,file))).isFile(),'Regular operator build input required: '+file)
for(const file of buildInputs){
const target=path.join(to,file)
await mkdir(path.dirname(target),{recursive:true})
await copyFile(path.join(from,file),target,constants.COPYFILE_EXCL)
}
return buildInputs.length
}
if(process.argv[1]&&import.meta.url===pathToFileURL(path.resolve(process.argv[1])).href){
assert(process.argv.length===3&&process.argv[2],'A single Docker scripts destination is required')
const files=await stageDatabaseOperatorScripts(process.argv[2])
console.log(JSON.stringify({event:'database-operator-inputs-staged',files}))
}
+15 -6
View File
@@ -65,8 +65,9 @@ const telemetrySettingsConfiguration = process.argv.includes('--telemetry-settin
if (telemetrySettingsConfiguration && process.argv.slice(2).some(arg => arg !== '--telemetry-settings-configuration')) throw new Error('Telemetry settings configuration requires its own disposable cluster') if (telemetrySettingsConfiguration && process.argv.slice(2).some(arg => arg !== '--telemetry-settings-configuration')) throw new Error('Telemetry settings configuration requires its own disposable cluster')
const routingReservationSchema = process.argv.includes('--routing-reservation-schema') const routingReservationSchema = process.argv.includes('--routing-reservation-schema')
if (routingReservationSchema && process.argv.slice(2).some(arg => arg !== '--routing-reservation-schema')) throw new Error('Routing reservation schema requires its own disposable cluster') if (routingReservationSchema && process.argv.slice(2).some(arg => arg !== '--routing-reservation-schema')) throw new Error('Routing reservation schema requires its own disposable cluster')
const routingForwardRounds = process.argv.includes('--routing-network-forward-rounds') const routingOriginalEffects = process.argv.includes('--routing-original-effects')
if (routingForwardRounds && process.argv.slice(2).some(arg => arg !== '--routing-network-forward-rounds')) throw new Error('Routing forward rounds requires its own disposable cluster') const routingForwardRounds = routingOriginalEffects || process.argv.includes('--routing-network-forward-rounds')
if (routingForwardRounds && process.argv.slice(2).some(arg => arg !== (routingOriginalEffects ? '--routing-original-effects' : '--routing-network-forward-rounds'))) throw new Error('Routing forward rounds requires its own disposable cluster')
const flowActiveServices = process.argv.includes('--flow-active-services') const flowActiveServices = process.argv.includes('--flow-active-services')
if (flowActiveServices && process.argv.slice(2).some(arg => arg !== '--flow-active-services')) throw new Error('Flow active services requires its own disposable cluster') if (flowActiveServices && process.argv.slice(2).some(arg => arg !== '--flow-active-services')) throw new Error('Flow active services requires its own disposable cluster')
const flowActiveRebuild = process.argv.includes('--flow-active-rebuild') const flowActiveRebuild = process.argv.includes('--flow-active-rebuild')
@@ -93,8 +94,11 @@ const operatorPeerMetadata = process.argv.includes('--operator-peer-metadata')
if (operatorPeerMetadata && process.argv.slice(2).some(arg => arg !== '--operator-peer-metadata')) throw new Error('Operator peer metadata requires a separate disposable cluster without restore') if (operatorPeerMetadata && process.argv.slice(2).some(arg => arg !== '--operator-peer-metadata')) throw new Error('Operator peer metadata requires a separate disposable cluster without restore')
const externalPeerMetadata = process.argv.includes('--external-peer-metadata') const externalPeerMetadata = process.argv.includes('--external-peer-metadata')
if (externalPeerMetadata && process.argv.slice(2).some(arg => arg !== '--external-peer-metadata')) throw new Error('Guarded external peer metadata requires a separate disposable cluster without restore') if (externalPeerMetadata && process.argv.slice(2).some(arg => arg !== '--external-peer-metadata')) throw new Error('Guarded external peer metadata requires a separate disposable cluster without restore')
const mcpOperationOrigin = process.argv.includes('--mcp-operation-origin') const pollOperationRetirement = process.argv.includes('--poll-operation-retirement')
if (mcpOperationOrigin && process.argv.slice(2).some(arg => arg !== '--mcp-operation-origin')) throw new Error('MCP operation root requires a separate disposable cluster without restore') const ordinaryOperationRetirement = process.argv.includes('--ordinary-operation-retirement')
const probeOperationRetirement = process.argv.includes('--probe-operation-retirement')
const mcpOperationOrigin = process.argv.includes('--mcp-operation-origin') || pollOperationRetirement || ordinaryOperationRetirement || probeOperationRetirement
if (mcpOperationOrigin && process.argv.slice(2).some(arg => arg !== (probeOperationRetirement ? '--probe-operation-retirement' : ordinaryOperationRetirement ? '--ordinary-operation-retirement' : pollOperationRetirement ? '--poll-operation-retirement' : '--mcp-operation-origin'))) throw new Error('MCP operation root requires a separate disposable cluster without restore')
const mcpSpeedOriginal = process.argv.includes('--mcp-speed-original') const mcpSpeedOriginal = process.argv.includes('--mcp-speed-original')
if (mcpSpeedOriginal && process.argv.slice(2).some(arg => arg !== '--mcp-speed-original')) throw new Error('MCP speed component requires a separate disposable cluster without restore') if (mcpSpeedOriginal && process.argv.slice(2).some(arg => arg !== '--mcp-speed-original')) throw new Error('MCP speed component requires a separate disposable cluster without restore')
const routerConfigurationGuard = process.argv.includes('--router-configuration-guard') const routerConfigurationGuard = process.argv.includes('--router-configuration-guard')
@@ -242,12 +246,13 @@ try {
if (regressionOnly) console.log('Selected regression only: primary integration and terminal restore are not run') if (regressionOnly) console.log('Selected regression only: primary integration and terminal restore are not run')
if (profilesOnly) console.log('Profiles-only regression matrix: primary integration NOT RUN; terminal restore NOT RUN; full acceptance is not claimed') if (profilesOnly) console.log('Profiles-only regression matrix: primary integration NOT RUN; terminal restore NOT RUN; full acceptance is not claimed')
const result = regressionOnly || profilesOnly ? 0 : await new Promise((resolve, reject) => { const result = regressionOnly || profilesOnly ? 0 : await new Promise((resolve, reject) => {
const child = spawn(process.execPath, pollMemory ? ['--max-old-space-size=192', '--max-semi-space-size=16', fileURLToPath(new URL('../dist-mcp-memory/services/server-poll-memory.owned.test.js', import.meta.url))] : ['--import', 'tsx', principalSecuritySource ? 'src/modules/mcp/principal-security-source.owned.test.ts' : pollJournalSnapshot ? 'src/services/server-poll-journal-snapshot.owned.test.ts' : pollTerminalArchive ? 'src/services/server-poll-terminal-archive.owned.test.ts' : pollArchiveSchema ? 'src/db/server-poll-terminal-archives.sql.test.ts' : telemetrySettingsConfiguration ? 'src/modules/mcp/telemetry-settings-configuration.owned.test.ts' : routingReservationSchema ? 'src/db/routing-reservation-schema.owned.test.ts' : routingForwardRounds ? 'src/modules/mcp/routing-network-forward-rounds.owned.test.ts' : flowActiveServices ? 'src/services/traffic-flow-active-services.owned.test.ts' : flowActiveRebuild ? 'src/services/traffic-flow-active-rebuild.owned.test.ts' : flowQuality ? 'src/services/traffic-flow-quality.owned.test.ts' : flowWorkerIpc ? 'src/services/traffic-flow-worker-ipc.owned.test.ts' : manualUptime ? 'src/modules/mcp/manual-uptime-http.test.ts' : manualPingGroup ? 'src/modules/mcp/manual-ping-group-http.test.ts' : manualTraffic ? 'src/modules/mcp/manual-traffic-http.test.ts' : manualApiPing ? 'src/modules/mcp/manual-api-ping-http.test.ts' : simpleSpeedChr ? 'src/modules/mcp/simple-speed-chr.test.ts' : simpleSpeed ? 'src/modules/mcp/simple-speed-http.test.ts' : externalPeerMetadata ? 'src/services/bandwidth-external-peer-metadata.sql.test.ts' : operatorPeerMetadata ? 'src/services/bandwidth-operator-metadata.sql.test.ts' : publicRegistryMetadata ? 'src/services/bandwidth-public-registry.sql.test.ts' : operatorDockerSql ? 'src/services/bandwidth-operator-docker-stop.sql.test.ts' : mcpOperationOrigin ? 'src/modules/mcp/operation-execution-origin.sql.test.ts' : mcpSpeedOriginal ? 'src/modules/mcp/speed-bandwidth-original.sql.test.ts' : routerConfigurationGuard ? 'src/services/router-configuration-guard.sql.test.ts' : scheduledSpeedOriginal ? 'src/services/bandwidth-scheduled-speed-original.sql.test.ts' : bandwidthSpeedWorkflow ? 'src/services/bandwidth-speed-workflow.sql.test.ts' : restSpeedOriginal ? 'src/services/bandwidth-rest-speed-original.sql.test.ts' : operationLocks ? 'src/services/operation-config-lock-lease.sql.test.ts' : serviceExecutionOrigin ? 'src/modules/mcp/service-execution-origin.sql.test.ts' : bandwidthNodeSource ? 'src/services/bandwidth-node-source.sql.test.ts' : bandwidthExternalRegistry ? 'src/services/bandwidth-external-node-registry.sql.test.ts' : bandwidthNodeRegistry ? 'src/services/bandwidth-node-registry.sql.test.ts' : bandwidthControlSource ? 'src/services/bandwidth-control-source.sql.test.ts' : bandwidthAdmission ? 'src/services/bandwidth-admission.sql.test.ts' : serverPollStorageFresh ? 'src/services/server-poll-storage-fresh.sql.test.ts' : serverPollStorage ? 'src/services/server-poll-storage.sql.test.ts' : probeStorageFresh ? 'src/services/server-probe-storage-fresh.sql.test.ts' : probeLegacyOrphans ? 'src/services/server-probe-legacy-orphans.sql.test.ts' : diagnosticCancelHost ? 'src/modules/mcp/diagnostic-cancellation-storage.sql.test.ts' : ipsecExportHttp ? 'src/modules/mcp/ipsec-existing-certificate-export-http.sql.test.ts' : ipsecExportJournal ? 'src/services/ipsec-existing-certificate-export-rest-journal.sql.test.ts' : migrationDataInventory ? 'src/db/migration-data-inventory.sql.test.ts' : probeStorageHistory ? 'src/services/server-probe-storage-history.sql.test.ts' : probeStorageJournal ? 'src/services/server-probe-storage-journal.sql.test.ts' : controlledSchemaReference ? 'src/db/controlled-schema-reference.sql.test.ts' : controlledMigrations ? 'src/db/controlled-migrations.sql.test.ts' : databaseSchemaControl ? 'src/db/database-schema-control.sql.test.ts' : migrationSchemaInventory ? 'src/db/migration-schema-inventory.sql.test.ts' : routingNetworkNative ? 'src/modules/mcp/routing-network-native.sql.test.ts' : routingReservationRuntime ? 'src/modules/mcp/routing-reservation-runtime.sql.test.ts' : routingReservations ? 'src/modules/mcp/routing-reservation.sql.test.ts' : collectorPartitionWriters ? 'src/services/collector-partition-writes.sql.test.ts' : serverPollDurable ? 'src/services/server-poll-durable-session.sql.test.ts' : routingRecoveryAuthority ? 'src/modules/mcp/routing-recovery-authority.sql.test.ts' : legacyFlowRebuild ? 'src/test/legacy-flow-rebuild-sql.ts' : probeJournal ? 'src/services/server-probe-journal.sql.test.ts' : flowHostKeys ? 'src/modules/mcp/flow-host-keys-http.test.ts' : flowRebuildMcp ? 'src/modules/mcp/flow-facts-rebuild-http.test.ts' : flowRebuild ? 'src/test/flow-facts-rebuild-sql.ts' : flowWrites ? 'src/services/traffic-flow-write-transaction.pg.test.ts' : nativeConservation ? 'src/modules/mcp/native-conservation-sql.test.ts' : nativeCreateId ? 'src/modules/mcp/native-create-id-http.test.ts' : ipsecAdapters ? "src/mLine truncated const child = spawn(process.execPath, pollMemory ? ['--max-old-space-size=192', '--max-semi-space-size=16', fileURLToPath(new URL('../dist-mcp-memory/services/server-poll-memory.owned.test.js', import.meta.url))] : ['--import', 'tsx', principalSecuritySource ? 'src/modules/mcp/principal-security-source.owned.test.ts' : pollJournalSnapshot ? 'src/services/server-poll-journal-snapshot.owned.test.ts' : pollTerminalArchive ? 'src/services/server-poll-terminal-archive.owned.test.ts' : pollArchiveSchema ? 'src/db/server-poll-terminal-archives.sql.test.ts' : telemetrySettingsConfiguration ? 'src/modules/mcp/telemetry-settings-configuration.owned.test.ts' : routingReservationSchema ? 'src/db/routing-reservation-schema.owned.test.ts' : routingForwardRounds ? 'src/modules/mcp/routing-network-forward-rounds.owned.test.ts' : flowActiveServices ? 'src/services/traffic-flow-active-services.owned.test.ts' : flowActiveRebuild ? 'src/services/traffic-flow-active-rebuild.owned.test.ts' : flowQuality ? 'src/services/traffic-flow-quality.owned.test.ts' : flowWorkerIpc ? 'src/services/traffic-flow-worker-ipc.owned.test.ts' : manualUptime ? 'src/modules/mcp/manual-uptime-http.test.ts' : manualPingGroup ? 'src/modules/mcp/manual-ping-group-http.test.ts' : manualTraffic ? 'src/modules/mcp/manual-traffic-http.test.ts' : manualApiPing ? 'src/modules/mcp/manual-api-ping-http.test.ts' : simpleSpeedChr ? 'src/modules/mcp/simple-speed-chr.test.ts' : simpleSpeed ? 'src/modules/mcp/simple-speed-http.test.ts' : externalPeerMetadata ? 'src/services/bandwidth-external-peer-metadata.sql.test.ts' : operatorPeerMetadata ? 'src/services/bandwidth-operator-metadata.sql.test.ts' : publicRegistryMetadata ? 'src/services/bandwidth-public-registry.sql.test.ts' : operatorDockerSql ? 'src/services/bandwidth-operator-docker-stop.sql.test.ts' : probeOperationRetirement ? 'src/services/server-probe-operation-retirement.owned.test.ts' : ordinaryOperationRetirement ? 'src/modules/mcp/ordinary-operation-retirement.owned.test.ts' : pollOperationRetirement ? 'src/services/server-poll-operation-retirement.owned.test.ts' : mcpOperationOrigin ? 'src/modules/mcp/operation-execution-origin.sql.test.ts' : mcpSpeedOriginal ? 'src/modules/mcp/speed-bandwidth-original.sql.test.ts' : routerConfigurationGuard ? 'src/services/router-configuration-guard.sql.test.ts' : scheduledSpeedOriginal ? 'src/services/bandwidth-scheduled-speed-original.sql.test.ts' : bandwidthSpeedWorkflow ? 'src/services/bandwidth-speed-workflow.sql.test.ts' : restSpeedOriginal ? 'src/services/bandwidth-rest-speed-original.sql.test.ts' : operationLocks ? 'src/services/operation-config-lock-lease.sql.test.ts' : serviceExecutionOrigin ? 'src/modules/mcp/service-execution-origin.sql.test.ts' : bandwidthNodeSource ? 'src/services/bandwidth-node-source.sql.test.ts' : bandwidthExternalRegistry ? 'src/services/bandwidth-external-node-registry.sql.test.ts' : bandwidthNodeRegistry ? 'src/services/bandwidth-node-registry.sql.test.ts' : bandwidthControlSource ? 'src/services/bandwidth-control-source.sql.test.ts' : bandwidthAdmission ? 'src/services/bandwidth-admission.sql.test.ts' : serverPollStorageFresh ? 'src/services/server-poll-storage-fresh.sql.test.ts' : serverPollStorage ? 'src/services/server-poll-storage.sql.test.ts' : probeStorageFresh ? 'src/services/server-probe-storage-fresh.sql.test.ts' : probeLegacyOrphans ? 'src/services/server-probe-legacy-orphans.sql.test.ts' : diagnosticCancelHost ? 'src/modules/mcp/diagnostic-cancellation-storage.sql.test.ts' : ipsecExportHttp ? 'src/modules/mcp/ipsec-existing-certificate-export-http.sql.test.ts' : ipsecExportJournal ? 'src/services/ipsec-existing-certificate-export-rest-journal.sql.test.ts' : migrationDataInventory ? 'src/db/migration-data-inventory.sql.test.ts' : probeStorageHistory ? 'src/services/server-probe-storage-history.sql.test.ts' : probeStorageJournal ? 'src/services/server-probe-storage-journal.sql.test.ts' : controlledSchemaReference ? 'src/db/controlled-schema-reference.sql.test.ts' : controlledMigrations ? 'src/db/controlled-migrations.sql.test.ts' : databaseSchemaControl ? 'src/db/database-schema-control.sql.test.ts' : migrationSchemaInventory ? 'src/db/migration-schema-inventory.sql.test.ts' : routingNetworkNative ? 'src/modules/mcp/routing-network-native.sql.test.ts' : routingReservationRuntime ? 'src/modules/mcp/routing-reservation-runtime.sql.test.ts' : routingReservations ? 'src/modules/mcp/routing-reservation.sql.test.ts' : collectorPartitionWriters ? 'src/services/collector-partition-writes.sql.test.ts' : serverPollDurable ? 'src/services/server-poll-durable-session.sql.test.ts' : routingRecoveryAuthority ? 'src/modules/mcp/routing-recovery-authority.sql.test.ts' : legacyFlowRebuild ? 'src/test/legacy-flow-rebuild-sql.ts' : probeJournal ? 'src/services/server-probe-journal.sql.test.ts' : flowHostKeys ? 'src/modules/mcp/flow-host-keys-http.test.ts' : flowRebuildMcp ? 'src/modules/mcp/flow-facts-rebuild-http.test.ts' : flowRebuildLine truncated
cwd: pollMemory ? directory : new URL('../', import.meta.url), stdio: 'inherit', env: { ...(pollMemory ? pollMemoryEnvironment : process.env), cwd: pollMemory ? directory : new URL('../', import.meta.url), stdio: 'inherit', env: { ...(pollMemory ? pollMemoryEnvironment : process.env),
...(routingOriginalEffects ? { MMAPP_OWN_ROUTING_ORIGINAL_EFFECTS: 'true' } : {}),
DATABASE_URL: `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres`, DATABASE_URL: `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres`,
DATABASE_PATH: join(directory, 'no-legacy-database.sqlite'), SQLITE_IMPORT_FULL_HISTORY: 'false', DATABASE_PATH: join(directory, 'no-legacy-database.sqlite'), SQLITE_IMPORT_FULL_HISTORY: 'false',
MCP_STATE_DIR: join(directory, 'state'), AUTH_REQUIRED: 'true', AUTH_JWT_SECRET: 'mcp-integration-test-only', MMAPP_TEST_WG_EXPORT_ONLY: String(wireguardExport), MCP_STATE_DIR: join(directory, 'state'), AUTH_REQUIRED: 'true', AUTH_JWT_SECRET: 'mcp-integration-test-only', MMAPP_TEST_WG_EXPORT_ONLY: String(wireguardExport),
MCP_PUBLIC_URL: 'http://localhost:8000/mcp', NODE_ENV: 'test', PATH: testPath, NODE_OPTIONS: pollMemory ? '' : process.env.NODE_OPTIONS, MMAPP_OWN_PRINCIPAL_SECURITY_SOURCE: String(principalSecuritySource), MMAPP_OWN_POLL_JOURNAL_SNAPSHOT: String(pollJournalSnapshot), MMAPP_OWN_POLL_MEMORY: String(pollMemory), MMAPP_OWN_POLL_BOOTSTRAP: String(pollMemoryBootstrap), MMAPP_OWN_POLL_COST_DIAGNOSTICS: String(pollMemoryCost), MMAPP_OWN_POLL_ARCHIVE_CLAIM_LOSS: String(pollArchiveClaimLoss), MMAPP_OWN_POLL_ARCHIVE_HISTORY: String(pollArchiveHistory), MMAPP_OWN_POLL_ARCHIVE_COLD: String(pollArchiveCold), MMAPP_OWN_POLL_ARCHIVE_LOST_ACK: String(pollArchiveLostAck), MMAPP_OWN_POLL_TERMINAL_ARCHIVE: String(pollTerminalArchive), MMAPP_OWN_POLL_ARCHIVE_SCHEMA: String(pollArchiveSchema), MMAPP_OWN_TELEMETRY_SETTINGS_CONFIGURATION: String(telemetrySettingsConfiguration), MMAPP_OWN_ROUTING_RESERVATION_SCHEMA: String(routingReservationSchema), MMAPP_OWN_ROUTING_FORWARD_ROUNDS: String(routingForwardRounds), MMAPP_OWN_FLOW_ACTIVE_SERVICES: String(flowActiveServices), MMAPP_OWN_FLOW_ACTIVE_REBUILD: String(flowActiveRebuild), MMAPP_OWN_FLOW_QUALITY: String(flowQuality), MMAPP_OWN_FLOW_WORKER_IPC: String(flowWorkerIpc), MMAPP_OWN_MANUAL_UPTIME: String(manualUptime), MMAPP_OWN_MANUAL_PING_GROUP: String(manualPingGroup), MMAPP_OWN_MANUAL_TRAFFIC: String(manualTraffic), MMAPP_OWN_MANUAL_API_PING: String(manualApiPing), MMAPP_OWN_SIMPLE_SPEED: String(simpleSpeed), MMAPP_OWN_SIMPLE_SPEED_CHR: String(simpleSpeedChr), MMAPP_OWN_DIAGNOSTIC_CANCEL_HOST: String(diagnosticCancelHost), MMAPP_OWN_OPERATOR_DOCKER_SQL: String(operatorDockerSql), MMAPP_OWN_PUBLIC_REGISTRY_METADATA: String(publicRegistryMetadata), MMAPP_OWN_OPERATOR_PEER_METADATA: String(operatorPeerMetadata), MMAPP_OWN_EXTERNAL_PEER_METADATA: String(externalPeerMetadata), MMAPP_OWN_BANDWIDTH_EXTERNAL_REGISTRY: String(externalPeerMetadata || operatorPeerMetadata || publicRegistryMetadata || operatorDockerSql || mcpSpeedOriginal || bandwidthSpeedWorkflow || bandwidthExternalRegistry), MMAPP_OWN_BANDWIDTH_SPEED_WORKFLOW: String(bandwidthSpeedWorkflow), MMAPP_OWN_REST_SPEED_ORIGINAL: String(restSpeedOriginal), MMAPP_OWN_MCP_OPERATION_ORIGIN: String(mcpOperationOrigin), MMAPP_OWN_MCP_SPEED_ORIGINAL: String(mcpSpeedOriginal), MMAPP_OWN_SCHEDULED_SPEED_ORIGINAL: String(scheduledSpeedOriginal), MMAPP_TEST_ROUTER_GUARD_OWNED: String(routerConfigurationGuard), MMAPP_TEST_ROUTER_GUARD_CLUSTER_DISPOSABLE: String(routerConfigurationGuard), MMAPP_TEST_ROUTER_GUARD_ADMIN_URL: routerConfigurationGuard ? `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres` : '', MMAPP_OWN_BANDWIDTH_ADMISSION: String(externalPeerMetadata || operatorPeerMetadata || publicRegistryMetadata || operatorDockerSql || mcpOperationOrigin || mcpSpeedOriginal || scheduledSpeedOriginal || bandwidthSpeedWorkflow || restSpeedOriginal || bandwidthExternalRegistry || operationLocks || serviceExecutionOrigin || bandwidthAdmission || bandwidthControlSource || bandwidthNodeRegistry || bandwidthNodeSource), MMAPP_OWN_BANDWIDTH_RESTORE: String((bandwidthAdmission || bandwidthControlSource || bandwidthNodeRegistry || bandwidthNodeSource) && process.argv.includes('--restore')), MMAPP_OWN_IPSEC_EXPORT_HTTP: String(ipsecExportHttp), MMAPP_OWN_IPSEC_EXPORT_JOURNAL: String(ipsecExportJournal), MMAPP_OWN_SQL_DB: 'postgres', MMAPP_OWN_PROBE_LEGACY_ORPHANS: String(probeLegacyOrphans), MMAPP_OWN_PROBE_STORAGE_HISTORY: String(probeStorageHistory), MMAPP_OWN_PROBE_STORAGE_FRESH: String(probeStorageFresh), MMAPP_OWN_PROBE_STORAGE_JOURNAL: String(probeStorageJournal || probeStorageFresh), MMAPP_OWN_CONTROLLED_SCHEMA_REFERENCE: String(controlledSchemaReference), MMAPP_OWN_CONTROLLED_MIGRATIONS: String(controlledMigrations), MMAPP_OWN_DATABASE_SCHEMA_CONTROL: String(databaseSchemaControl), MMAPP_OWN_MIGRATION_DATA_INVENTORY: String(migrationDataInventory), MMAPP_OWN_MIGRATION_SCHEMA_INVENTORY: String(migrationSchemaInventory), MMAPP_OWN_ROUTING_NETWORK_NATIVE: String(routingNetworkNative), MMAPP_OWN_ROUTING_RESERVATIONS: String(routingReservations || routingReservationRuntime), MMAPP_OWN_COLLECTOR_PARTITION_WRITERS: String(collectorPartitionWriters), MMAPP_OWN_SERVER_POLL_STORAGE_FRESH: String(serverPollStorageFresh), MMAPP_OWN_SERVER_POLL_STORAGE: String(serverPollStorage), MMAPP_OWN_SERVER_POLL_DURABLE: String(serverPollDurable), MMAPP_OWN_ROUTING_RECOVERY_AUTHORITY: String(routingRecoveryAuthority), MMAPP_OWN_LEGACY_FLOW_REBUILD: String(legacyFlowRebuild), MMAPP_OWN_PROBE_JOURNAL: String(probeJournal), MMAPP_MODULE_COMPLETION_ONLY: String(moduleCompletion), MMAPP_MODULE_COMPLETION_TARGET: completionTarget, MMAPP_OWN_FLOW_HOST_KEYS: String(flowHostKeys), MMAPP_OWN_MCP_FLOW_REBUILD: String(flowRebuildMcp), MMAPP_OWN_FLOW_WRITES: String(flowWrites), MMAPP_OWN_FLOW_REBUILD: String(flowRebuild) }, MCP_PUBLIC_URL: 'http://localhost:8000/mcp', NODE_ENV: 'test', PATH: testPath, NODE_OPTIONS: pollMemory ? '' : process.env.NODE_OPTIONS, MMAPP_OWN_PRINCIPAL_SECURITY_SOURCE: String(principalSecuritySource), MMAPP_OWN_POLL_JOURNAL_SNAPSHOT: String(pollJournalSnapshot), MMAPP_OWN_POLL_MEMORY: String(pollMemory), MMAPP_OWN_POLL_BOOTSTRAP: String(pollMemoryBootstrap), MMAPP_OWN_POLL_COST_DIAGNOSTICS: String(pollMemoryCost), MMAPP_OWN_POLL_ARCHIVE_CLAIM_LOSS: String(pollArchiveClaimLoss), MMAPP_OWN_POLL_ARCHIVE_HISTORY: String(pollArchiveHistory), MMAPP_OWN_POLL_ARCHIVE_COLD: String(pollArchiveCold), MMAPP_OWN_POLL_ARCHIVE_LOST_ACK: String(pollArchiveLostAck), MMAPP_OWN_POLL_TERMINAL_ARCHIVE: String(pollTerminalArchive), MMAPP_OWN_POLL_ARCHIVE_SCHEMA: String(pollArchiveSchema), MMAPP_OWN_TELEMETRY_SETTINGS_CONFIGURATION: String(telemetrySettingsConfiguration), MMAPP_OWN_ROUTING_RESERVATION_SCHEMA: String(routingReservationSchema), MMAPP_OWN_ROUTING_FORWARD_ROUNDS: String(routingForwardRounds), MMAPP_OWN_FLOW_ACTIVE_SERVICES: String(flowActiveServices), MMAPP_OWN_FLOW_ACTIVE_REBUILD: String(flowActiveRebuild), MMAPP_OWN_FLOW_QUALITY: String(flowQuality), MMAPP_OWN_FLOW_WORKER_IPC: String(flowWorkerIpc), MMAPP_OWN_MANUAL_UPTIME: String(manualUptime), MMAPP_OWN_MANUAL_PING_GROUP: String(manualPingGroup), MMAPP_OWN_MANUAL_TRAFFIC: String(manualTraffic), MMAPP_OWN_MANUAL_API_PING: String(manualApiPing), MMAPP_OWN_SIMPLE_SPEED: String(simpleSpeed), MMAPP_OWN_SIMPLE_SPEED_CHR: String(simpleSpeedChr), MMAPP_OWN_DIAGNOSTIC_CANCEL_HOST: String(diagnosticCancelHost), MMAPP_OWN_OPERATOR_DOCKER_SQL: String(operatorDockerSql), MMAPP_OWN_PUBLIC_REGISTRY_METADATA: String(publicRegistryMetadata), MMAPP_OWN_OPERATOR_PEER_METADATA: String(operatorPeerMetadata), MMAPP_OWN_EXTERNAL_PEER_METADATA: String(externalPeerMetadata), MMAPP_OWN_BANDWIDTH_EXTERNAL_REGISTRY: String(externalPeerMetadata || operatorPeerMetadata || publicRegistryMetadata || operatorDockerSql || mcpSpeedOriginal || bandwidthSpeedWorkflow || bandwidthExternalRegistry), MMAPP_OWN_BANDWIDTH_SPEED_WORKFLOW: String(bandwidthSpeedWorkflow), MMAPP_OWN_REST_SPEED_ORIGINAL: String(restSpeedOriginal), MMAPP_OWN_MCP_OPERATION_ORIGIN: String(mcpOperationOrigin), MMAPP_OWN_PROBE_OPERATION_RETIREMENT: String(probeOperationRetirement), MMAPP_OWN_MCP_SPEED_ORIGINAL: String(mcpSpeedOriginal), MMAPP_OWN_SCHEDULED_SPEED_ORIGINAL: String(scheduledSpeedOriginal), MMAPP_TEST_ROUTER_GUARD_OWNED: String(routerConfigurationGuard), MMAPP_TEST_ROUTER_GUARD_CLUSTER_DISPOSABLE: String(routerConfigurationGuard), MMAPP_TEST_ROUTER_GUARD_ADMIN_URL: routerConfigurationGuard ? `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres` : '', MMAPP_OWN_BANDWIDTH_ADMISSION: String(externalPeerMetadata || operatorPeerMetadata || publicRegistryMetadata || operatorDockerSql || mcpOperationOrigin || mcpSpeedOriginal || scheduledSpeedOriginal || bandwidthSpeedWorkflow || restSpeedOriginal || bandwidthExternalRegistry || operationLocks || serviceExecutionOrigin || bandwidthAdmission || bandwidthControlSource || bandwidthNodeRegistry || bandwidthNodeSource), MMAPP_OWN_BANDWIDTH_RESTORE: String((bandwidthAdmission || bandwidthControlSource || bandwidthNodeRegistry || bandwidthNodeSource) && process.argv.includes('--restore')), MMAPP_OWN_IPSEC_EXPORT_HTTP: String(ipsecExportHttp), MMAPP_OWN_IPSEC_EXPORT_JOURNAL: String(ipsecExportJournal), MMAPP_OWN_SQL_DB: 'postgres', MMAPP_OWN_PROBE_LEGACY_ORPHANS: String(probeLegacyOrphans), MMAPP_OWN_PROBE_STORAGE_HISTORY: String(probeStorageHistory), MMAPP_OWN_PROBE_STORAGE_FRESH: String(probeStorageFresh), MMAPP_OWN_PROBE_STORAGE_JOURNAL: String(probeStorageJournal || probeStorageFresh), MMAPP_OWN_CONTROLLED_SCHEMA_REFERENCE: String(controlledSchemaReference), MMAPP_OWN_CONTROLLED_MIGRATIONS: String(controlledMigrations), MMAPP_OWN_DATABASE_SCHEMA_CONTROL: String(databaseSchemaControl), MMAPP_OWN_MIGRATION_DATA_INVENTORY: String(migrationDataInventory), MMAPP_OWN_MIGRATION_SCHEMA_INVENTORY: String(migrationSchemaInventory), MMAPP_OWN_ROUTING_NETWORK_NATIVE: String(routingNetworkNative), MMAPP_OWN_ROUTING_RESERVATIONS: String(routingReservations || routingReservationRuntime), MMAPP_OWN_COLLECTOR_PARTITION_WRITERS: String(collectorPartitionWriters), MMAPP_OWN_SERVER_POLL_STORAGE_FRESH: String(serverPollStorageFresh), MMAPP_OWN_SERVER_POLL_STORAGE: String(serverPollStorage), MMAPP_OWN_SERVER_POLL_DURABLE: String(serverPollDurable), MMAPP_OWN_ROUTING_RECOVERY_AUTHORITY: String(routingRecoveryAuthority), MMAPP_OWN_LEGACY_FLOW_REBUILD: String(legacyFlowRebuild), MMAPP_OWN_PROBE_JOURNAL: String(probeJournal), MMAPP_MODULE_COMPLETION_ONLY: String(moduleCompletion), MMAPP_MODULE_COMPLETION_TARGET: completionTarget, MMAPP_OWN_FLOW_HOST_KEYS: String(flowHostKeys), MMAPP_OWN_MCP_FLOW_REBUILD: String(flowRebuildMcp), MMAPP_OWN_FLOW_WRITES: String(flowWrites), MMAPP_OWN_FLOW_REBUILD: String(flowRebuild) },
}) })
child.once('error', reject); child.once('exit', code => resolve(code ?? 1)) child.once('error', reject); child.once('exit', code => resolve(code ?? 1))
}) })
@@ -268,6 +273,7 @@ try {
else if (result === 0 && pollArchiveSchema) console.log('Poll archive schema: owned nonempty40 preservation, numbered41 and structural refusal guards; no positive MCP archive claim') else if (result === 0 && pollArchiveSchema) console.log('Poll archive schema: owned nonempty40 preservation, numbered41 and structural refusal guards; no positive MCP archive claim')
else if (result === 0 && telemetrySettingsConfiguration) console.log('Telemetry settings: genuine MCP/SQL configuration binding, collector metadata tolerated; no native or external sources') else if (result === 0 && telemetrySettingsConfiguration) console.log('Telemetry settings: genuine MCP/SQL configuration binding, collector metadata tolerated; no native or external sources')
else if (result === 0 && routingReservationSchema) console.log('Routing reservation numbered0040: ownSQL fresh/legacy/manual preservation; no READY, backfill, capabilities or public native activation') else if (result === 0 && routingReservationSchema) console.log('Routing reservation numbered0040: ownSQL fresh/legacy/manual preservation; no READY, backfill, capabilities or public native activation')
else if (result === 0 && routingOriginalEffects) console.log('Routing original effects: approved manual revision 2 and original effects checked against disposable PostgreSQL and synthetic RouterOS HTTP')
else if (result === 0 && routingForwardRounds) console.log('Routing forward rounds: genuine approved running CONFIG_APPLIED, shared cumulative native budget; logs/security/autoRecovery remain unaccepted') else if (result === 0 && routingForwardRounds) console.log('Routing forward rounds: genuine approved running CONFIG_APPLIED, shared cumulative native budget; logs/security/autoRecovery remain unaccepted')
else if (result === 0 && flowActiveServices) console.log('Flow active settings/purge: production services and main Worker; disposable SQL/localhost only') else if (result === 0 && flowActiveServices) console.log('Flow active settings/purge: production services and main Worker; disposable SQL/localhost only')
else if (result === 0 && flowActiveRebuild) console.log('Flow active rebuild: production shared service and genuine Worker drain; disposable SQL/localhost only') else if (result === 0 && flowActiveRebuild) console.log('Flow active rebuild: production shared service and genuine Worker drain; disposable SQL/localhost only')
@@ -283,6 +289,9 @@ try {
else if (result === 0 && operatorPeerMetadata) console.log('Operator peer metadata: actual managed endpoint SQL and genuine localhost license origin; synthetic Q, external metadata and full operator window remain open') else if (result === 0 && operatorPeerMetadata) console.log('Operator peer metadata: actual managed endpoint SQL and genuine localhost license origin; synthetic Q, external metadata and full operator window remain open')
else if (result === 0 && publicRegistryMetadata) console.log('Public registry metadata: actual owned PostgreSQL and same physical lease; no secret/Q/native/write authority or backend-window acceptance') else if (result === 0 && publicRegistryMetadata) console.log('Public registry metadata: actual owned PostgreSQL and same physical lease; no secret/Q/native/write authority or backend-window acceptance')
else if (result === 0 && operatorDockerSql) console.log('Operator Docker SQL: actual owned PostgreSQL with mocked Docker HTTP only; not a real Docker/backend-window acceptance') else if (result === 0 && operatorDockerSql) console.log('Operator Docker SQL: actual owned PostgreSQL with mocked Docker HTTP only; not a real Docker/backend-window acceptance')
else if (result === 0 && ordinaryOperationRetirement) console.log('Ordinary operation retirement: owned actual logs-settings apply and worker')
else if (result === 0 && pollOperationRetirement) console.log('Poll operation retirement: owned authenticated historical fixtures and current reader/GC')
else if (result === 0 && probeOperationRetirement) console.log('Diagnostic retirement: actual owned worker tick, PostgreSQL and product read/replay; no live scheduling')
else if (result === 0 && mcpOperationOrigin) console.log('MCP operation root: owned PostgreSQL ordinary runner provenance; no restore/recovery, global writer activation, CHR or deployment') else if (result === 0 && mcpOperationOrigin) console.log('MCP operation root: owned PostgreSQL ordinary runner provenance; no restore/recovery, global writer activation, CHR or deployment')
else if (result === 0 && mcpSpeedOriginal) console.log('MCP speed: genuine trusted service context with real connection/grant/SQL, host receipt and localhost native; synthetic Q/mapping; not a public MCP runner/action acceptance') else if (result === 0 && mcpSpeedOriginal) console.log('MCP speed: genuine trusted service context with real connection/grant/SQL, host receipt and localhost native; synthetic Q/mapping; not a public MCP runner/action acceptance')
else if (result === 0 && routerConfigurationGuard) console.log('Router configuration guard: dedicated owned databases/roles and localhost native fixture; no application migration, production activation or real CHR') else if (result === 0 && routerConfigurationGuard) console.log('Router configuration guard: dedicated owned databases/roles and localhost native fixture; no application migration, production activation or real CHR')
@@ -1,7 +1,7 @@
import EmbeddedPostgres from 'embedded-postgres' import EmbeddedPostgres from 'embedded-postgres'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { mkdir, mkdtemp, readdir, readFile, writeFile, cp, rm } from 'node:fs/promises' import { mkdir, mkdtemp, readdir, readFile, writeFile, cp, rm } from 'node:fs/promises'
import { resolve, join, relative, dirname } from 'node:path' import { resolve, join, relative, dirname, basename } from 'node:path'
import { randomBytes, createHash } from 'node:crypto' import { randomBytes, createHash } from 'node:crypto'
import { createServer } from 'node:net' import { createServer } from 'node:net'
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
@@ -16,7 +16,8 @@ await readFile(join(root, 'backend/src/modules/mcp/routing-main-transition.ts'))
await readFile(join(root, 'backend/src/modules/mcp/routing-controller-baseline-acceptance.ts')) await readFile(join(root, 'backend/src/modules/mcp/routing-controller-baseline-acceptance.ts'))
if (process.argv.includes('--check-only')) { console.log(JSON.stringify({ mode, sourcePresent: true, acceptanceExecuted: false, productionApplied: false })); process.exit(0) } if (process.argv.includes('--check-only')) { console.log(JSON.stringify({ mode, sourcePresent: true, acceptanceExecuted: false, productionApplied: false })); process.exit(0) }
await mkdir(join(root, 'tmp'), { recursive: true }) await mkdir(join(root, 'tmp'), { recursive: true })
const directory = await mkdtemp(join(root, 'tmp/routing-controller-baseline-owned-')), output = join(directory, 'dist'), sourcePins = [] const ownedPrefix = 'mm-mcp-test-routing-controller-baseline-'
const directory = await mkdtemp(join(root, 'tmp', ownedPrefix)), output = join(directory, 'dist'), sourcePins = []
const hash = buffer => createHash('sha256').update(buffer).digest('hex') const hash = buffer => createHash('sha256').update(buffer).digest('hex')
async function compile(sourceDirectory) { async function compile(sourceDirectory) {
for (const entry of await readdir(sourceDirectory, { withFileTypes: true })) { for (const entry of await readdir(sourceDirectory, { withFileTypes: true })) {
@@ -59,7 +60,7 @@ try {
// embedded-postgres waits for a new exit event; avoid waiting again if its // embedded-postgres waits for a new exit event; avoid waiting again if its
// owned child has already exited. This never targets another database. // owned child has already exited. This never targets another database.
if (postgres.process?.exitCode === null) await postgres.stop() if (postgres.process?.exitCode === null) await postgres.stop()
if (dirname(resolve(directory)) !== resolve(root, 'tmp') || !directory.includes('routing-controller-baseline-owned-')) throw new Error('Owned routing cleanup boundary changed') if (dirname(resolve(directory)) !== resolve(root, 'tmp') || !basename(directory).startsWith(ownedPrefix)) throw new Error('Owned routing cleanup boundary changed')
await rm(join(directory, 'pg'), { recursive: true, force: true, maxRetries: 20, retryDelay: 100 }) await rm(join(directory, 'pg'), { recursive: true, force: true, maxRetries: 20, retryDelay: 100 })
console.log('Owned routing PostgreSQL stopped and its data removed; snapshot/log evidence retained') console.log('Owned routing PostgreSQL stopped and its data removed; snapshot/log evidence retained')
} }
+13 -7
View File
@@ -38,6 +38,7 @@ export function projectVrfDiagnostics(log, privateValues = [], expectedSlice = n
if (Array.isArray(value.accepted)) { row.finalCaseCount = value.accepted.length; if (expectedSlice !== null && value.slice === expectedSlice) finalComponentPresent = true } if (Array.isArray(value.accepted)) { row.finalCaseCount = value.accepted.length; if (expectedSlice !== null && value.slice === expectedSlice) finalComponentPresent = true }
if (['planned','queued','running','applied','failed','cancelled','unknown','expired','rejected','unconfirmed'].includes(value.operationState)) row.operationState = value.operationState if (['planned','queued','running','applied','failed','cancelled','unknown','expired','rejected','unconfirmed'].includes(value.operationState)) row.operationState = value.operationState
for (const key of ['phase', 'elapsedMs', 'nativeReads', 'nativeWrites', 'roundTrips', 'lastSqlMs', 'totalSqlMs', 'maxSqlMs']) if (Number.isSafeInteger(value[key]) && value[key] >= 0) row[key] = value[key] for (const key of ['phase', 'elapsedMs', 'nativeReads', 'nativeWrites', 'roundTrips', 'lastSqlMs', 'totalSqlMs', 'maxSqlMs']) if (Number.isSafeInteger(value[key]) && value[key] >= 0) row[key] = value[key]
if (Number.isSafeInteger(value.wireguardPeerTelemetryTicks) && value.wireguardPeerTelemetryTicks > 0 && value.wireguardPeerTelemetryTicks <= 10000) row.wireguardPeerTelemetryTicks = value.wireguardPeerTelemetryTicks
if (value.diagnosticWaitSql === true) row.diagnosticWaitSql = true if (value.diagnosticWaitSql === true) row.diagnosticWaitSql = true
if (value.diagnosticError === true) { row.diagnosticError = true; row.safeName = diagnosticNames.has(value.safeName) ? value.safeName : 'other'; row.safeCode = diagnosticCodes.has(value.safeCode) ? value.safeCode : 'other' } if (value.diagnosticError === true) { row.diagnosticError = true; row.safeName = diagnosticNames.has(value.safeName) ? value.safeName : 'other'; row.safeCode = diagnosticCodes.has(value.safeCode) ? value.safeCode : 'other' }
if (!Object.keys(row).length) { omittedLines++; continue } if (!Object.keys(row).length) { omittedLines++; continue }
@@ -47,10 +48,10 @@ export function projectVrfDiagnostics(log, privateValues = [], expectedSlice = n
} }
export function vrfSliceDiagnostic(result, log, childBudgetMs, privateValues = []) { export function vrfSliceDiagnostic(result, log, childBudgetMs, privateValues = []) {
assert(result && typeof result === 'object') assert(result && typeof result === 'object')
assert(['baseline','physical','vlan','disabled','mixed','reconcile','sql-journal','guards','coexistence','alias'].includes(result.slice)) assert(['baseline','physical','vlan','disabled','mixed','reconcile','sql-journal','guards','coexistence','alias','gre'].includes(result.slice))
assert(Number.isSafeInteger(result.code) && typeof result.childExitConfirmed === 'boolean') assert(Number.isSafeInteger(result.code) && typeof result.childExitConfirmed === 'boolean')
assert(Number.isSafeInteger(result.caseCount) && result.caseCount >= 0) assert(Number.isSafeInteger(result.caseCount) && result.caseCount >= 0)
assert.equal(childBudgetMs, result.slice === 'baseline' ? 240000 : 120000) assert.equal(childBudgetMs, result.slice === 'baseline' ? 240000 : result.slice === 'disabled' ? 180000 : 120000)
assert(result.failure === null || ['child-deadline','child-log-limit','child-signal','child-error','child-error-after-pid','child-exit-unconfirmed','missing-component-receipt'].includes(result.failure)) assert(result.failure === null || ['child-deadline','child-log-limit','child-signal','child-error','child-error-after-pid','child-exit-unconfirmed','missing-component-receipt'].includes(result.failure))
assert(result.accepted === (result.code === 0 && result.failure === null && result.childExitConfirmed)) assert(result.accepted === (result.code === 0 && result.failure === null && result.childExitConfirmed))
return { ...result, capMs: childBudgetMs, diagnostics: projectVrfDiagnostics(log, privateValues, result.slice), scope: 'same-owned-PG-test-only', actualRouter: false, forwardingVerified: false } return { ...result, capMs: childBudgetMs, diagnostics: projectVrfDiagnostics(log, privateValues, result.slice), scope: 'same-owned-PG-test-only', actualRouter: false, forwardingVerified: false }
@@ -88,10 +89,14 @@ export async function runOwnedVrfProductionProfile({ directory, database, databa
const report = fileURLToPath(new URL('../../docs/mcp-chr-vrf-companion-native-v5.json', import.meta.url)) const report = fileURLToPath(new URL('../../docs/mcp-chr-vrf-companion-native-v5.json', import.meta.url))
const loader = pathToFileURL(createRequire(import.meta.url).resolve('tsx')).href const loader = pathToFileURL(createRequire(import.meta.url).resolve('tsx')).href
const safe = Object.fromEntries(['SystemRoot', 'WINDIR', 'TEMP', 'TMP', 'LANG', 'LC_ALL'].filter(key => typeof process.env[key] === 'string').map(key => [key, process.env[key]])) const safe = Object.fromEntries(['SystemRoot', 'WINDIR', 'TEMP', 'TMP', 'LANG', 'LC_ALL'].filter(key => typeof process.env[key] === 'string').map(key => [key, process.env[key]]))
for (const slice of ['baseline', 'physical', 'vlan', 'disabled', 'mixed', 'reconcile', 'sql-journal', 'guards', 'coexistence', 'alias']) { // Diagnose the disabled lifecycle first, then the synthetic GRE extension.
// Every original case remains required by the same full74 gate and caps.
for (const slice of ['disabled', 'gre', 'baseline', 'physical', 'vlan', 'mixed', 'reconcile', 'sql-journal', 'guards', 'coexistence', 'alias']) {
currentSlice = slice; currentChildExitConfirmed = false; lastSliceResult = null currentSlice = slice; currentChildExitConfirmed = false; lastSliceResult = null
await persist(slice + '-started.json', { version: 1, slice, status: 'running', accepted: false, childExitConfirmed: false }) await persist(slice + '-started.json', { version: 1, slice, status: 'running', accepted: false, childExitConfirmed: false })
const childBudgetMs = slice === 'baseline' ? 240000 : 120000 // Nine disabled lifecycle operations need their own aggregate child cap;
// measured seven operations alone consumed 105.5s under the original120s.
const childBudgetMs = slice === 'baseline' ? 240000 : slice === 'disabled' ? 180000 : 120000
const cwd = join(owned, 'vrf-production-' + slice), rel = relative(owned, cwd) const cwd = join(owned, 'vrf-production-' + slice), rel = relative(owned, cwd)
assert(rel && !rel.startsWith('..') && !isAbsolute(rel)); await mkdir(cwd) assert(rel && !rel.startsWith('..') && !isAbsolute(rel)); await mkdir(cwd)
assert.equal(await realpath(cwd), cwd); assert(!(await lstat(cwd)).isSymbolicLink()) assert.equal(await realpath(cwd), cwd); assert(!(await lstat(cwd)).isSymbolicLink())
@@ -121,9 +126,10 @@ export async function runOwnedVrfProductionProfile({ directory, database, databa
unknownTimer = setTimeout(() => { if (!exited) { stop('child-exit-unconfirmed'); resolveExit(1) } }, childBudgetMs + 15000) unknownTimer = setTimeout(() => { if (!exited) { stop('child-exit-unconfirmed'); resolveExit(1) } }, childBudgetMs + 15000)
}) })
clearTimeout(deadline); clearTimeout(stopTimer); clearTimeout(unknownTimer) clearTimeout(deadline); clearTimeout(stopTimer); clearTimeout(unknownTimer)
const expectedCases = { baseline: 11, physical: 7, vlan: 9, disabled: 9, mixed: 7, reconcile: 6, 'sql-journal': 1, guards: 11, coexistence: 3, alias: 4 } const expectedCases = { baseline: 11, physical: 7, vlan: 9, disabled: 9, mixed: 7, reconcile: 6, 'sql-journal': 1, guards: 11, coexistence: 3, alias: 4, gre: 6 }
const component = log.trim().split(/\r?\n/).flatMap(line => { try { return [JSON.parse(line)] } catch { return [] } }).findLast(value => value.slice === slice && Array.isArray(value.accepted)) const component = log.trim().split(/\r?\n/).flatMap(line => { try { return [JSON.parse(line)] } catch { return [] } }).findLast(value => value.slice === slice && Array.isArray(value.accepted))
if (!component || component.accepted.length !== expectedCases[slice] || component.actualSql !== true || component.actualCrypto !== true || component.actualAuthPolicyApprovalRuntime !== true || component.fakeVerifier !== false || component.actualRouter !== false || component.forwardingVerified !== false) failure ??= 'missing-component-receipt' if (!component || component.accepted.length !== expectedCases[slice] || component.actualSql !== true || component.actualCrypto !== true || component.actualAuthPolicyApprovalRuntime !== true || component.fakeVerifier !== false || component.actualRouter !== false || component.forwardingVerified !== false) failure ??= 'missing-component-receipt'
if (slice === 'gre' && (!Number.isSafeInteger(component?.wireguardPeerTelemetryTicks) || component.wireguardPeerTelemetryTicks <= 6 || component.wireguardPeerTelemetryTicks > 10000)) failure ??= 'missing-component-receipt'
const result = { slice, accepted: code === 0 && !failure && exited, caseCount: component?.accepted?.length ?? 0, code, failure, childPid: child.pid ?? null, childExitConfirmed: exited } const result = { slice, accepted: code === 0 && !failure && exited, caseCount: component?.accepted?.length ?? 0, code, failure, childPid: child.pid ?? null, childExitConfirmed: exited }
lastSliceResult = result; currentChildExitConfirmed = exited lastSliceResult = result; currentChildExitConfirmed = exited
const diagnostic = vrfSliceDiagnostic(result, log, childBudgetMs, [databaseUrl, target.password, decodeURIComponent(target.password), authSecret]) const diagnostic = vrfSliceDiagnostic(result, log, childBudgetMs, [databaseUrl, target.password, decodeURIComponent(target.password), authSecret])
@@ -135,8 +141,8 @@ export async function runOwnedVrfProductionProfile({ directory, database, databa
console.log(JSON.stringify({ profile: 'test:vrf:production', ...result })) console.log(JSON.stringify({ profile: 'test:vrf:production', ...result }))
if (!result.accepted) break if (!result.accepted) break
} }
const receipt = { accepted: results.length === 10 && results.every(result => result.accepted), childExitConfirmed: results.every(result => result.childExitConfirmed), sameOwnedPg: true, actualRouter: false, forwardingVerified: false, results } const receipt = { accepted: results.length === 11 && results.every(result => result.accepted), childExitConfirmed: results.every(result => result.childExitConfirmed), sameOwnedPg: true, actualRouter: false, forwardingVerified: false, results }
await persist('terminal.json', { version: 1, status: 'terminal', ...receipt, requiredCases: 68, completedCases: results.reduce((sum, value) => sum + value.caseCount, 0), diagnosticDirectory: diagnosticPath }) await persist('terminal.json', { version: 1, status: 'terminal', ...receipt, requiredCases: 74, completedCases: results.reduce((sum, value) => sum + value.caseCount, 0), diagnosticDirectory: diagnosticPath })
await writeFile(join(owned, 'vrf-production-acceptance.json'), JSON.stringify(receipt, null, 2)) await writeFile(join(owned, 'vrf-production-acceptance.json'), JSON.stringify(receipt, null, 2))
return receipt return receipt
} catch { } catch {
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { vrfSliceDiagnostic } from './vrf-production-profile.mjs'
test('only the nine-operation disabled slice receives its measured aggregate budget', () => {
for (const [slice, capMs] of [['disabled', 180000], ['gre', 120000], ['baseline', 240000]]) {
const result = { slice, accepted: false, caseCount: 0, code: 1, failure: 'child-deadline', childExitConfirmed: true }
assert.equal(vrfSliceDiagnostic(result, '', capMs).capMs, capMs)
for (const otherCap of [120000, 180000, 240000].filter(value => value !== capMs)) assert.throws(() => vrfSliceDiagnostic(result, '', otherCap))
}
})
test('GRE lifecycle diagnostic retains the six-case receipt with existing privacy bounds', () => {
const accepted = ['vrf-create', 'vrf-rename', 'vrf-disable', 'vrf-reenable', 'vrf-membership-transfer-a-to-b', 'vrf-delete-addresses-remain']
const result = { slice: 'gre', accepted: true, caseCount: 6, code: 0, failure: null, childExitConfirmed: true }
const log = accepted.map((acceptedCase, index) => JSON.stringify({ slice: 'gre', acceptedCase, nativeWrites: 1, wireguardPeerTelemetryTicks: (index + 1) * 4, password: 'fixture-private-value' }))
log.push(JSON.stringify({ slice: 'gre', accepted, wireguardPeerTelemetryTicks: 24, rawShape: 'synthetic GRE lifecycle', password: 'fixture-private-value' }))
const diagnostic = vrfSliceDiagnostic(result, log.join('\n'), 120000, ['fixture-private-value'])
assert.deepEqual(diagnostic.diagnostics.acceptedCaseSequence, accepted)
assert.equal(diagnostic.diagnostics.finalComponentPresent, true)
assert.equal(diagnostic.diagnostics.rawLogRetained, false)
assert.deepEqual(diagnostic.diagnostics.records.map(row => row.wireguardPeerTelemetryTicks), [4, 8, 12, 16, 20, 24, 24])
assert.equal(diagnostic.actualRouter, false)
assert.equal(diagnostic.forwardingVerified, false)
assert.equal(JSON.stringify(diagnostic).includes('fixture-private-value'), false)
assert.throws(() => vrfSliceDiagnostic(result, '', 240000))
})
test('peer telemetry diagnostic rejects nonnumeric and out-of-budget values', () => {
const result = { slice: 'gre', accepted: false, caseCount: 0, code: 1, failure: 'missing-component-receipt', childExitConfirmed: true }
for (const wireguardPeerTelemetryTicks of [0, -1, 10001, 1.5, 'private-raw-value', { secret: 'private-raw-value' }]) {
const diagnostic = vrfSliceDiagnostic(result, JSON.stringify({ slice: 'gre', wireguardPeerTelemetryTicks }), 120000)
assert.equal(diagnostic.diagnostics.records.some(row => Object.hasOwn(row, 'wireguardPeerTelemetryTicks')), false)
assert.equal(JSON.stringify(diagnostic).includes('private-raw-value'), false)
}
})
test('a partial GRE lifecycle is diagnostic evidence without a final component receipt', () => {
const result = { slice: 'gre', accepted: false, caseCount: 0, code: 1, failure: 'child-deadline', childExitConfirmed: true }
const diagnostic = vrfSliceDiagnostic(result, JSON.stringify({ slice: 'gre', acceptedCase: 'vrf-create' }), 120000)
assert.equal(diagnostic.accepted, false)
assert.equal(diagnostic.diagnostics.finalComponentPresent, false)
assert.equal(diagnostic.diagnostics.lastAcceptedCase, 'vrf-create')
assert.equal(diagnostic.capMs, 120000)
})
@@ -0,0 +1,129 @@
/** Explicit host-operator inspection/acceptance. No restore, service restart, role
* enrollment or source/READY issuer. Populated data is accepted, not certified. */
import assert from 'node:assert/strict'
import path from 'node:path'
import {fileURLToPath} from 'node:url'
import {mkdir,readdir} from 'node:fs/promises'
import {readProtected,protectedDirectory,writeFile,withJournalLease,sha} from './operator-files.mjs'
import {poolOptions,STARTUP_OPTIONS,assertJsDriverEnvironment} from './operator-contract.mjs'
import {readMaintenanceRecoveryFacts,readMaintenanceAcceptanceRecords} from '../../src/services/database-maintenance-startup.js'
import {parseMaintenanceJSON,runtimeDatabaseTargetSha256,evaluateMaintenanceAcceptances,validateMaintenanceKnownCurrent,validateMaintenanceAcceptance,MAINTENANCE_ACCEPTANCE_FORMAT,type MaintenanceKnownCurrent} from '../../src/services/database-maintenance-resolution.js'
import {captureMigrationSchema} from '../../src/db/migration-schema-inventory.js'
import {MIGRATION_MANIFEST,CURRENT_REQUIRED_TABLES,migrationManifestDigest,inventoryMigrationDirectory} from '../../src/db/migration-manifest.js'
import type {Pool,PoolClient} from 'pg'
const hex=(v:unknown):v is string=>typeof v==='string'&&/^[a-f0-9]{64}$/.test(v)
function object(v:unknown):asserts v is Record<string,unknown>{assert(v&&typeof v==='object'&&!Array.isArray(v))}
function exact(v:unknown,keys:string[]):asserts v is Record<string,unknown>{object(v);assert.equal(Object.keys(v).sort().join('\0'),keys.sort().join('\0'))}
const stamp=(v:unknown):v is string=>typeof v==='string'&&Number.isFinite(Date.parse(v))&&new Date(v).toISOString()===v
const stable=(v:unknown)=>JSON.stringify(v)
const flags=['state-dir','transport','transport-sha256','runtime-target','runtime-target-sha256']
const validityMs=900000
/** Explicit local-container transport never falls back from TLS or environment.
* Caller launch review supplies --network container:<exact reviewed PG id>;
* this module has no Docker socket/API and claims only loopback + physical SQL proof. */
export function maintenancePoolConfiguration(value:unknown,ca?:Buffer,caSha256?:string){
object(value);const mode=value.mode===undefined?'tls':value.mode;assert(mode==='tls'||mode==='local-loopback')
exact(value,mode==='local-loopback'?['format','mode','connection']:value.mode===undefined?['format','connection','tls']:['format','mode','connection','tls'])
assert.equal(value.format,'database-maintenance-transport-v1');exact(value.connection,['host','port','user','password','database']);const connection=value.connection
assert(typeof connection.host==='string'&&typeof connection.user==='string'&&/^[^\x00-\x1f\x7f]{1,63}$/.test(connection.user)&&typeof connection.password==='string'&&connection.password.length>0&&connection.password.length<=4096&&!connection.password.includes('\0')&&typeof connection.database==='string'&&/^[^\x00-\x1f\x7f]{1,63}$/.test(connection.database)&&typeof connection.port==='number'&&Number.isSafeInteger(connection.port)&&Number(connection.port)>0&&Number(connection.port)<=65535)
let options,identity:unknown
if(mode==='tls'){
exact(value.tls,['serverName','spkiSha256']);assert(typeof value.tls.serverName==='string'&&/^[a-zA-Z0-9][a-zA-Z0-9.-]{0,252}$/.test(value.tls.serverName)&&hex(value.tls.spkiSha256));assert(ca&&hex(caSha256))
options=poolOptions(connection,{roles:{publisher:{name:connection.user}},source:{databaseName:connection.database},tls:{...value.tls,caSha256}},ca)
identity={mode,host:connection.host,port:connection.port,database:connection.database,tls:{...value.tls,caSha256}}
}else{
assert(connection.host==='127.0.0.1'||connection.host==='::1','Plaintext maintenance requires exact loopback and reviewed PostgreSQL container namespace');assert(ca===undefined&&caSha256===undefined)
options={host:connection.host,port:connection.port,user:connection.user,password:connection.password,database:connection.database,max:1,connectionTimeoutMillis:5000,idleTimeoutMillis:5000,statement_timeout:10000,lock_timeout:5000,options:STARTUP_OPTIONS,application_name:'mmapp-maintenance-acceptance',client_encoding:'UTF8',ssl:false}
identity={mode,host:connection.host,port:connection.port,database:connection.database}
}
return {options,transportBinding:{mode,endpointSha256:sha(stable(identity))} as MaintenanceKnownCurrent['transport']}
}
export function maintenanceApproval(value:unknown,inspectionSha256:string,inspectionCreatedAt:string){
exact(value,['format','resolutionId','decision','actorId','inspectionSha256','issuedAt','expiresAt','managedBackendStopped','historicOutcomesRemainUnknown','automaticRestore'])
assert(value.format==='database-maintenance-current-state-approval-v1'&&value.decision==='accept-observed-current-state'&&typeof value.resolutionId==='string'&&/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value.resolutionId))
assert(typeof value.actorId==='string'&&/^[^\x00-\x1f\x7f]{1,160}$/.test(value.actorId)&&value.inspectionSha256===inspectionSha256&&stamp(value.issuedAt)&&stamp(value.expiresAt))
const now=Date.now();assert(Date.parse(value.issuedAt)>=Date.parse(inspectionCreatedAt)&&Date.parse(value.issuedAt)<=now&&now<Date.parse(value.expiresAt)&&Date.parse(value.expiresAt)-Date.parse(value.issuedAt)<=validityMs)
assert(value.managedBackendStopped===true&&value.historicOutcomesRemainUnknown===true&&value.automaticRestore===false)
return value as unknown as {resolutionId:string;actorId:string;issuedAt:string;expiresAt:string}
}
/** Genuine observation, not a supplied snapshot factory. Owner/transaction ends
* must be confirmed before a caller can publish an acceptance. Exclusive key
* protects cooperative observations; stopped-backend assumption spans publication. */
export async function observeMaintenanceCurrent(pool:Pick<Pool,'connect'>,transport:MaintenanceKnownCurrent['transport']){
let client:PoolClient|undefined,locked=false,begun=false,bad=false,faulted=false
const fault=()=>{faulted=true},live=()=>{assert(!faulted)}
try{
client=await pool.connect();client.on('error',fault);client.on('end',fault)
const query=async(text:string,values:unknown[]=[])=>{live();const result=await client!.query(text,values);live();return result}
await query("SET statement_timeout='10s'");await query("SET lock_timeout='5s'")
const lease=await query('SELECT pg_catalog.pg_try_advisory_lock(8723110::bigint) AS locked');assert(lease.rows.length===1&&lease.rows[0].locked===true);locked=true
await query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');begun=true
for(const sql of ["SET LOCAL search_path=pg_catalog","SET LOCAL TimeZone='UTC'","SET LOCAL DateStyle='ISO, YMD'","SET LOCAL standard_conforming_strings=on","SET LOCAL statement_timeout='10s'"])await query(sql)
const identity=await query(`SELECT c.system_identifier::text AS "systemIdentifier",d.oid::text AS "databaseOid",d.datname AS "databaseName",pg_catalog.current_setting('server_version_num')::int AS "serverVersionNum",pg_catalog.pg_is_in_recovery() AS "inRecovery" FROM pg_catalog.pg_database d CROSS JOIN pg_catalog.pg_control_system() c WHERE d.datname=pg_catalog.current_database()`)
assert.equal(identity.rows.length,1)
// The optional writer-profile control schema is not installed by ordinary
// application migrations. Observe its presence in this same transaction;
// detailed capture still refuses every other unexpected namespace.
const control=await query("SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname='mmapp_control'")
assert(control.rows.length<=1&&control.rows.every(row=>row.nspname==='mmapp_control'))
const schema=await captureMigrationSchema({query:async(text,values)=>query(text,values)},{scope:{applicationNamespaces:['public'],excludedControlNamespaces:control.rows.length?['mmapp_control']:[]},deadlineAt:Date.now()+60000})
const marker=schema.sections.relations.find(row=>row.namespace==='public'&&row.name==='schema_migrations'),column=schema.sections.columns.find(row=>row.namespace==='public'&&row.relation==='schema_migrations'&&row.name==='id'&&row.dropped===false)
assert(marker?.kind==='r'&&marker.persistence==='p'&&marker.row_security===false&&marker.force_row_security===false&&column?.type==='text')
for(const table of CURRENT_REQUIRED_TABLES)assert(schema.sections.relations.some(row=>row.namespace==='public'&&row.name===table&&['r','p'].includes(String(row.kind))&&row.row_security===false&&row.force_row_security===false))
const markers=(await query('SELECT id FROM public.schema_migrations ORDER BY id LIMIT $1',[MIGRATION_MANIFEST.length+1])).rows
assert.equal(stable(markers),stable(MIGRATION_MANIFEST.map(entry=>({id:entry.file.slice(0,-4)}))))
const knownCurrent={transport,source:identity.rows[0],schemaSha256:schema.sha256,rawSchemaSha256:schema.observedRaw.sha256,physicalSchemaSha256:schema.observedPhysical.sha256,manifestSha256:migrationManifestDigest(),markersSha256:sha(stable(markers))}
validateMaintenanceKnownCurrent(knownCurrent)
const end=await query('ROLLBACK');assert.equal(end.command,'ROLLBACK');begun=false
const unlock=await query('SELECT pg_catalog.pg_advisory_unlock(8723110::bigint) AS unlocked');assert(unlock.rows.length===1&&unlock.rows[0].unlocked===true);locked=false
return {knownCurrent,schema}
}catch(error){bad=true;throw error}finally{
if(client){let cleanupFailed=false
try{if(begun&&!faulted){const end=await client.query('ROLLBACK');assert.equal(end.command,'ROLLBACK')}if(locked&&!faulted){const end=await client.query('SELECT pg_catalog.pg_advisory_unlock(8723110::bigint) AS unlocked');assert(end.rows[0]?.unlocked===true)}}catch{cleanupFailed=true}
try{client.off('error',fault);client.off('end',fault)}catch{cleanupFailed=true}
try{client.release(bad||faulted||cleanupFailed)}catch{cleanupFailed=true}
assert(!cleanupFailed&&!faulted,'Read-only maintenance owner cleanup unconfirmed')
}
}
}
async function snapshot(stateDirectory:string,target:string,records:readonly Buffer[]){
const state=await readMaintenanceRecoveryFacts(stateDirectory);assert(state.kind==='journal'&&state.generation)
const accepted=evaluateMaintenanceAcceptances(records,state.journal,state.vault.evidence,target)
const unresolvedOperations=state.operations.filter(op=>op.state!=='applied'),vaultCoverage=state.vault.evidence.filter(marker=>state.vault.unresolvedFiles.includes(marker.file))
const newUnresolved=unresolvedOperations.some(op=>!accepted.acceptedCoreIds.has(op.operationId))||state.vault.unresolvedFiles.some(file=>!accepted.acceptedVaultFiles.has(file))
return {newUnresolved,external:{journalCoverage:{prefixBytes:state.journal.length,sha256:sha(state.journal),unresolvedOperations},vaultCoverage,generationObservedSha256:sha(state.generation),previousResolutionSha256:accepted.headSha256}}
}
export async function maintenanceMain(args:string[]){
assertJsDriverEnvironment(process.env);const resolve=args[0]==='maintenance-resolve';assert(resolve||args[0]==='maintenance-inspect')
const required=[...flags,...(resolve?['inspection','inspection-sha256','approval','approval-sha256']:['output'])],names=[...required,'ca','ca-sha256'],values=new Map<string,string>()
assert(args.length===1+required.length*2||args.length===1+(required.length+2)*2)
for(let i=1;i<args.length;i+=2){const name=args[i].slice(2);assert(args[i].startsWith('--')&&names.includes(name)&&!values.has(name)&&args[i+1]);values.set(name,args[i+1])}
for(const name of required)assert(values.has(name));assert(values.has('ca')===values.has('ca-sha256'));const get=(name:string)=>values.get(name)!
for(const name of [...values.keys()].filter(name=>name.endsWith('sha256')))assert(hex(get(name)))
const stateDirectory=get('state-dir');await protectedDirectory(stateDirectory)
const transport=parseMaintenanceJSON(await readProtected(get('transport'),16384,get('transport-sha256')));object(transport);const mode=transport.mode===undefined?'tls':transport.mode;assert(mode==='tls'||mode==='local-loopback');assert(values.has('ca')===(mode==='tls'))
const ca=mode==='tls'?await readProtected(get('ca'),65536,get('ca-sha256')):undefined
const runtime=parseMaintenanceJSON(await readProtected(get('runtime-target'),16384,get('runtime-target-sha256')));exact(runtime,['format','databaseUrl']);assert(runtime.format==='database-maintenance-runtime-target-v1'&&typeof runtime.databaseUrl==='string');const runtimeUrl=new URL(runtime.databaseUrl);assert(!runtimeUrl.username&&!runtimeUrl.password)
const target=runtimeDatabaseTargetSha256(runtime.databaseUrl),{options,transportBinding}=maintenancePoolConfiguration(transport,ca,mode==='tls'?get('ca-sha256'):undefined)
exact(transport.connection,['host','port','user','password','database']);assert.equal(decodeURIComponent(runtimeUrl.pathname.slice(1)),transport.connection.database)
const records=await readMaintenanceAcceptanceRecords(stateDirectory);if(resolve)assert(records.length<64,'Maintenance acceptance history bound reached');const currentState=await snapshot(stateDirectory,target,records),before=currentState.external;if(resolve)assert(currentState.newUnresolved,'No new unresolved history to accept')
await inventoryMigrationDirectory(path.join(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))),'drizzle'))
let inspection:Record<string,unknown>|undefined,approved:ReturnType<typeof maintenanceApproval>|undefined
if(resolve){inspection=parseMaintenanceJSON(await readProtected(get('inspection'),67108864,get('inspection-sha256'))) as Record<string,unknown>;exact(inspection,['format','createdAt','expiresAt','runtimeTargetSha256','acceptanceRequired','external','knownCurrent','schema']);assert(inspection.format==='database-maintenance-current-state-inspection-v1'&&stamp(inspection.createdAt)&&stamp(inspection.expiresAt)&&Date.parse(inspection.createdAt)<=Date.now()&&Date.now()<Date.parse(inspection.expiresAt)&&Date.parse(inspection.expiresAt)-Date.parse(inspection.createdAt)<=validityMs&&inspection.runtimeTargetSha256===target&&inspection.acceptanceRequired===true);validateMaintenanceKnownCurrent(inspection.knownCurrent);assert.equal(stable(inspection.external),stable(before));approved=maintenanceApproval(parseMaintenanceJSON(await readProtected(get('approval'),16384,get('approval-sha256'))),get('inspection-sha256'),inspection.createdAt)}
const {default:pg}=await import('pg');assertJsDriverEnvironment(process.env)
const runProof=async()=>{const pool=new pg.Pool(options);let failed=false;pool.on('error',()=>{failed=true});try{const result=await observeMaintenanceCurrent(pool,transportBinding);await pool.end();assert(!failed);return result}catch(error){try{await pool.end()}catch{/* preserve refusal */}throw error}}
if(!resolve){const result=await runProof();assert.equal(stable((await snapshot(stateDirectory,target,await readMaintenanceAcceptanceRecords(stateDirectory))).external),stable(before));const nowMs=Date.now(),now=new Date(nowMs).toISOString(),out={format:'database-maintenance-current-state-inspection-v1',createdAt:now,expiresAt:new Date(nowMs+validityMs).toISOString(),runtimeTargetSha256:target,acceptanceRequired:currentState.newUnresolved,external:before,...result};const raw=Buffer.from(stable(out));assert(raw.length<=67108864);await writeFile(get('output'),raw,{flag:'wx',mode:0o600});return {event:'database-maintenance-inspection',inspectionSha256:sha(raw),unresolvedCoreOperations:before.journalCoverage.unresolvedOperations.length,unresolvedVaultFiles:before.vaultCoverage.length,state:currentState.newUnresolved?'inspection-required':before.previousResolutionSha256?'historically-accepted':'no-unresolved-history',historicalAcceptanceHeadSha256:before.previousResolutionSha256,humanApprovalRequired:currentState.newUnresolved,knownCurrentObserved:true,archiveDataEquivalenceProved:false,historicOutcomesChanged:false,automaticRestore:false}}
const resolutionDirectory=path.join(stateDirectory,'maintenance-acceptances');await mkdir(resolutionDirectory,{recursive:true,mode:0o700});await protectedDirectory(resolutionDirectory)
return withJournalLease(resolutionDirectory,async()=>{
// Existing immutable records are reread while our proven O_EXCL lease is held.
// Runtime reader refuses this lease; never infer stale ownership from PID/time.
const expected=records.map(raw=>{const v=parseMaintenanceJSON(raw) as {resolutionId:string};return v.resolutionId+'.json'}).sort()
assert.equal((await readdir(resolutionDirectory)).sort().join('\0'),[...expected,'publisher-lease.lock'].sort().join('\0'))
for(let i=0;i<records.length;i++){const value=parseMaintenanceJSON(records[i]) as {resolutionId:string};assert.equal(sha(await readProtected(path.join(resolutionDirectory,value.resolutionId+'.json'),1048576)),sha(records[i]))}
const observed=await runProof();assert.equal(stable(observed.knownCurrent),stable(inspection!.knownCurrent));assert.equal(stable(observed.schema),stable(inspection!.schema))
assert.equal(stable((await snapshot(stateDirectory,target,records)).external),stable(before));assert(Date.now()<Date.parse(approved!.expiresAt)&&Date.now()<Date.parse(inspection!.expiresAt as string))
const record={format:MAINTENANCE_ACCEPTANCE_FORMAT,resolutionId:approved!.resolutionId,decision:'accept-observed-current-state',historicOutcomeChanged:false,automaticRestore:false,actor:{kind:'host-operator',id:approved!.actorId},approvalSha256:get('approval-sha256'),inspectionSha256:get('inspection-sha256'),approvedAt:approved!.issuedAt,approvalExpiresAt:approved!.expiresAt,acceptedAt:new Date().toISOString(),runtimeTargetSha256:target,...before,knownCurrent:observed.knownCurrent}
validateMaintenanceAcceptance(record);const raw=Buffer.from(stable(record));assert(raw.length<=1048576,'Maintenance acceptance byte bound exceeded');await writeFile(path.join(resolutionDirectory,record.resolutionId+'.json'),raw,{flag:'wx',mode:0o600})
return {event:'database-maintenance-current-state-accepted',outcome:'committed',resolutionId:record.resolutionId,acceptanceSha256:sha(raw),archiveDataEquivalenceProved:false,historicOutcomesChanged:false,automaticRestore:false}
})
}
@@ -0,0 +1,84 @@
/** Explicit operator/lifecycle read component. No startup, grants or READY. */
import assert from 'node:assert/strict'
import {lstat} from 'node:fs/promises'
import path from 'node:path'
import {protectedDirectory,readProtected} from './operator-files.mjs'
import {createDatabaseSourceBindingVerifier,readDatabaseSourceBindingRegistry,
type DatabaseSourceDeploymentPin} from '../../src/db/database-source-binding.js'
import type {DatabaseSourceObservation,DatabaseSourceObservationOptions} from '../../src/db/database-source-observation.js'
import type {ProbeStorageSql} from '../../src/services/server-probe-journal-maintenance.js'
const hex=(value:unknown):value is string=>typeof value==='string'&&/^[a-f0-9]{64}$/.test(value)
function fence(options:DatabaseSourceObservationOptions){
assert(Number.isSafeInteger(options.deadlineAt)&&Date.now()<options.deadlineAt&&!options.signal?.aborted,'Protected source lifetime refused')
}
async function identity(filename:string,content=false){
const s=await lstat(filename,{bigint:true})
// BigInt identity is never narrowed to Number. Content changes/permission
// changes are checked around the whole verified read, not just open().
return [s.dev,s.ino,s.mode,s.uid,s.gid,s.nlink,...(content?[s.size,s.mtimeNs,s.ctimeNs]:[])].map(String).join(':')
}
async function protectedState(pin:DatabaseSourceDeploymentPin){
// Proven POSIX protection refuses unsupported Windows ACL BEFORE file reads.
await protectedDirectory(pin.registryDirectory)
const directories:Record<string,string>={}
for(let p=pin.registryDirectory;;p=path.dirname(p)){
directories[p]=await identity(p,p===pin.registryDirectory)
if(p===path.dirname(p))break
}
const names=['operator-public.pem','head-'+pin.currentHeadSha256+'.json']
const content:Record<string,Buffer>={},identities:Record<string,string>={}
const collect=async(name:string,maximum:number,expected?:string)=>{
const filename=path.join(pin.registryDirectory,name)
const before=await identity(filename,true)
content[name]=await readProtected(filename,maximum,expected)
assert.equal(await identity(filename,true),before,'Protected source file changed during read')
identities[name]=before
}
await collect(names[0],4096)
await collect(names[1],8192,pin.currentHeadSha256)
const head=JSON.parse(new TextDecoder('utf8',{fatal:true}).decode(content[names[1]]))
assert(hex(head?.body?.bindingSha256),'Pinned source binding digest required')
const bindingName='binding-'+head.body.bindingSha256+'.json'
await collect(bindingName,8192,head.body.bindingSha256)
await protectedDirectory(pin.registryDirectory)
for(const [directory,wanted]of Object.entries(directories))assert.equal(await identity(directory,directory===pin.registryDirectory),wanted,'Protected source directory changed')
return {directories,identities,content}
}
function same(before:Awaited<ReturnType<typeof protectedState>>,after:Awaited<ReturnType<typeof protectedState>>){
assert.deepEqual(after.directories,before.directories,'Protected source directory drift')
assert.deepEqual(after.identities,before.identities,'Protected source identity/mode drift')
assert.deepEqual(after.content,before.content,'Protected source content drift')
}
function captured(raw:DatabaseSourceDeploymentPin){
const pin=Object.freeze({...raw})
// Existing strict deployment schema rejects unknown/malformed pin fields.
const verifier=createDatabaseSourceBindingVerifier(pin)
return {pin,verifier}
}
/** Signature/head/source DATA plus actual POSIX protection of the pinned files.
* The supplied pin still needs independent deployment approval; protection
* cannot distinguish a restored clone/generation or authenticate all writers. */
export async function readProtectedDatabaseSourceBindingRegistry(raw:DatabaseSourceDeploymentPin,options:DatabaseSourceObservationOptions){
const {pin}=captured(raw);const lifetime={...options};fence(lifetime)
const before=await protectedState(pin);fence(lifetime)
const result=await readDatabaseSourceBindingRegistry(pin);fence(lifetime)
const after=await protectedState(pin);fence(lifetime);same(before,after)
return Object.freeze({...result,platformProtectionVerified:true as const,
generationAuthenticated:false as const,allWriterDrainAuthenticated:false as const,readyAuthorized:false as const})
}
/** Same-facade source verification is retained; filesystem JSON is never a
* source observation, writer capability or independent generation authority. */
export function createProtectedDatabaseSourceBindingVerifier(raw:DatabaseSourceDeploymentPin){
const {pin,verifier}=captured(raw)
return Object.freeze({async verify(sql:ProbeStorageSql,observation:DatabaseSourceObservation,options:DatabaseSourceObservationOptions){
const lifetime={...options};fence(lifetime)
const before=await protectedState(pin);fence(lifetime)
const result=await verifier.verify(sql,observation,lifetime);fence(lifetime)
const after=await protectedState(pin);fence(lifetime);same(before,after)
return Object.freeze({...result,platformProtectionVerified:true as const,
generationAuthenticated:false as const,allWriterDrainAuthenticated:false as const,readyAuthorized:false as const})
}})
}
@@ -0,0 +1,85 @@
/** Genuine Ed25519/files; POSIX permission metadata is modeled on Windows.
* No SQL, service, deployment or real POSIX acceptance is claimed. */
import test from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import os from 'node:os'
import crypto from 'node:crypto'
import {createRequire} from 'node:module'
import type {Stats,BigIntStats} from 'node:fs'
import type {DatabaseSourceDeploymentPin} from '../../src/db/database-source-binding.ts'
import type * as ProtectedApi from './protected-source-registry.mts'
import {runInNewContext} from 'node:vm'
import {readProtectedDatabaseSourceBindingRegistry} from './protected-source-registry.mts'
import {readDatabaseSourceBindingRegistry,sourceBindingBytes,sourceBindingCanonical,sourceBindingSha256,DATABASE_SOURCE_SIGNATURE_DOMAINS} from '../../src/db/database-source-binding.ts'
const require=createRequire(import.meta.url),ts=require('typescript')
const helperSource=await fs.readFile(new URL('./operator-files.mjs',import.meta.url),'utf8')
const source=await fs.readFile(new URL('./protected-source-registry.mts',import.meta.url),'utf8')
const deadline=()=>({deadlineAt:Date.now()+10000})
function compile(text:string){return ts.transpileModule(text,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText}
function evaluate<T=Record<string,unknown>>(text:string,imports:Record<string,unknown>,processModel:unknown=process):T{
const exports:Record<string,unknown>={}
runInNewContext(compile(text),{exports,require:(name:string)=>imports[name]??require(name),process:processModel,Buffer,TextDecoder,console})
return exports as T
}
interface Fixture {pin:DatabaseSourceDeploymentPin;faults:Record<string,boolean>;api:typeof ProtectedApi;verificationReads:()=>number;root:string}
async function own(work:(f:Fixture)=>Promise<void>){
const root=await fs.mkdtemp(path.join(os.tmpdir(),'mm-protected-source-'))
try{
const key=crypto.generateKeyPairSync('ed25519'),installationId=crypto.randomUUID()
const signed=(body:unknown,domain:string)=>sourceBindingBytes({body,signature:crypto.sign(null,Buffer.from(domain+sourceBindingCanonical(body)),key.privateKey).toString('base64url')})
const body={format:'database-source-binding-v1',installationId,sourceIdentityId:crypto.randomUUID(),generation:crypto.randomUUID(),ordinal:1,
source:{systemIdentifier:'123',databaseOid:16384,databaseName:'synthetic',sessionUser:'capture',currentUser:'capture',inRecovery:false,serverVersionNum:180004}}
const binding=signed(body,DATABASE_SOURCE_SIGNATURE_DOMAINS.binding),bindingSha256=sourceBindingSha256(binding)
const head=signed({format:'database-source-registry-head-v1',installationId,sourceIdentityId:body.sourceIdentityId,generation:body.generation,ordinal:1,bindingSha256,previousHeadSha256:null},DATABASE_SOURCE_SIGNATURE_DOMAINS.head)
const pin={registryDirectory:root,installationId,publicKeySpkiSha256:sourceBindingSha256(key.publicKey.export({format:'der',type:'spki'})),currentHeadSha256:sourceBindingSha256(head)}
await fs.writeFile(path.join(root,'operator-public.pem'),key.publicKey.export({format:'pem',type:'spki'}),{mode:0o600})
await fs.writeFile(path.join(root,'head-'+pin.currentHeadSha256+'.json'),head,{mode:0o600})
await fs.writeFile(path.join(root,'binding-'+bindingSha256+'.json'),binding,{mode:0o600})
const faults={modeFile:false,modeDirectory:false,symlink:false,replacement:false,lateMode:false,changed:false};let verificationReads=0
const metadata=(s:Stats|BigIntStats,filename:string,big:boolean)=>{
const n=(value:number)=>big?BigInt(value):value,isRoot=filename===root,isFile=s.isFile()
const mode=(isFile?0o100600:0o40700)|((isFile&&faults.modeFile||isRoot&&faults.modeDirectory||isFile&&faults.lateMode&&faults.changed)?0o022:0)
return {...s,mode:n(mode),uid:n(1000),gid:n(1000),ino:faults.replacement&&faults.changed&&isFile?(typeof s.ino==='bigint'?s.ino+1n:s.ino+1):s.ino,
isFile:()=>isFile,isDirectory:()=>s.isDirectory(),isSymbolicLink:()=>faults.symlink&&isRoot}
}
const modelFs={...fs,lstat:async(filename:string,options?:{bigint?:boolean})=>metadata(await fs.lstat(filename,{bigint:options?.bigint===true}),filename,options?.bigint===true),open:async(filename:string,flags:number)=>{
const file=await fs.open(filename,flags)
return {stat:async()=>metadata(await file.stat(),filename,false),read:file.read.bind(file),close:file.close.bind(file)}
}}
const processModel={platform:'linux',getuid:()=>1000}
const proven=evaluate(helperSource,{'node:fs/promises':modelFs},processModel)
const bindingModule={readDatabaseSourceBindingRegistry:async(p:DatabaseSourceDeploymentPin)=>{
verificationReads++;const result=await readDatabaseSourceBindingRegistry(p);faults.changed=true;return result
},createDatabaseSourceBindingVerifier:()=>({verify:async()=>{verificationReads++;faults.changed=true;return {genuineSourceFactsMatched:true,generationAuthenticated:false,readyAuthorized:false}}})}
const api=evaluate<typeof ProtectedApi>(source,{'node:fs/promises':modelFs,'./operator-files.mjs':proven,'../../src/db/database-source-binding.js':bindingModule},processModel)
await work({pin,faults,api,verificationReads:()=>verificationReads,root})
}finally{const rel=path.relative(os.tmpdir(),root);assert(rel&&!rel.startsWith('..')&&!path.isAbsolute(rel));await fs.rm(root,{recursive:true,force:true})}
}
test('unsupported Windows refuses before registry signature reads',{skip:process.platform!=='win32'},async()=>{
await assert.rejects(readProtectedDatabaseSourceBindingRegistry({registryDirectory:path.resolve('not-created-protected-source'),installationId:crypto.randomUUID(),publicKeySpkiSha256:'a'.repeat(64),currentHeadSha256:'b'.repeat(64)},deadline()),/Windows ACL/)
})
test('proven POSIX helpers plus genuine signature/head bytes retain false generation/READY',()=>own(async f=>{
const value=await f.api.readProtectedDatabaseSourceBindingRegistry(f.pin,deadline())
assert.equal(value.platformProtectionVerified,true);assert.equal(value.generationAuthenticated,false);assert.equal(value.allWriterDrainAuthenticated,false);assert.equal(value.readyAuthorized,false)
assert.equal(value.headSha256,f.pin.currentHeadSha256);assert.equal(f.verificationReads(),1)
}))
for(const fault of ['modeFile','modeDirectory','symlink'])test(fault+' refuses before cryptographic registry read',()=>own(async f=>{
f.faults[fault]=true;await assert.rejects(f.api.readProtectedDatabaseSourceBindingRegistry(f.pin,deadline()));assert.equal(f.verificationReads(),0)
}))
for(const fault of ['replacement','lateMode'])test(fault+' during genuine signature verification refuses protected output',()=>own(async f=>{
f.faults[fault]=true;await assert.rejects(f.api.readProtectedDatabaseSourceBindingRegistry(f.pin,deadline()));assert.equal(f.verificationReads(),1)
}))
test('expired/aborted lifetime refuses before filesystem or source access',()=>own(async f=>{
const abort=new AbortController();abort.abort()
for(const options of [{deadlineAt:Date.now()-1},{deadlineAt:Date.now()+1000,signal:abort.signal}])await assert.rejects(f.api.readProtectedDatabaseSourceBindingRegistry(f.pin,options))
assert.equal(f.verificationReads(),0)
}))
test('source verifier drift cannot grant protected output',()=>own(async f=>{
f.faults.lateMode=true
const verify=f.api.createProtectedDatabaseSourceBindingVerifier(f.pin).verify
await assert.rejects(verify({} as Parameters<typeof verify>[0],{} as Parameters<typeof verify>[1],deadline()))
assert.equal(f.verificationReads(),1)
}))
+3 -3
View File
@@ -141,14 +141,14 @@ async function preflightCurrentProfile(pool,{deadlineAt=Date.now()+120000}={}){
const first=await ownedContext(s,installation,PUBLISHER,'publisher-read') const first=await ownedContext(s,installation,PUBLISHER,'publisher-read')
if(canonical(sourceCoordinates(first))!==canonical(installation.expectedSource))throw Error('Pinned preflight source mismatch') if(canonical(sourceCoordinates(first))!==canonical(installation.expectedSource))throw Error('Pinned preflight source mismatch')
const facade=Object.freeze({query:(sql,values)=>s.query(sql,values)}),options={deadlineAt} const facade=Object.freeze({query:(sql,values)=>s.query(sql,values)}),options={deadlineAt}
const observation=await captureDatabaseSourceObservation(facade,options);await sourceVerifier.verify(facade,observation,options) const observation=await captureDatabaseSourceObservation(facade,options),initialSource=await sourceVerifier.verify(facade,observation,options)
const security=await protectedStructure(s,installation),catalogue=await collectFixedCatalogue(s) const security=await protectedStructure(s,installation),catalogue=await collectFixedCatalogue(s)
const current=await readCurrent(s,installation) const current=await readCurrent(s,installation)
if(!current)throw Error('Current signed policy required for preflight') if(!current)throw Error('Current signed policy required for preflight')
const catalogSha256=sha(canonical({base:catalogue.catalogSha256,security:security.sha256})),manifestSha256=migrationManifestDigest() const catalogSha256=sha(canonical({base:catalogue.catalogSha256,security:security.sha256})),manifestSha256=migrationManifestDigest()
if(approval.expiresAt<=Date.now()||approval.manifestSha256!==manifestSha256||approval.definitionSha256!==catalogue.definitionSha256||approval.catalogSha256!==catalogSha256)throw Error('Independent preflight approval differs from actual source44 catalogue') if(approval.expiresAt<=Date.now()||approval.manifestSha256!==manifestSha256||approval.definitionSha256!==catalogue.definitionSha256||approval.catalogSha256!==catalogSha256)throw Error('Independent preflight approval differs from actual source44 catalogue')
if(canonical(current.policy.source)!==canonical(sourceCoordinates(first))||canonical(current.policy.roles)!==canonical(security.roles)||current.policy.manifestSha256!==manifestSha256||current.policy.definitionSha256!==catalogue.definitionSha256||current.policy.catalogSha256!==catalogSha256)throw Error('Current signed policy differs from actual source/roles/catalogue') if(canonical(current.policy.source)!==canonical(sourceCoordinates(first))||canonical(current.policy.roles)!==canonical(security.roles)||current.policy.manifestSha256!==manifestSha256||current.policy.definitionSha256!==catalogue.definitionSha256||current.policy.catalogSha256!==catalogSha256)throw Error('Current signed policy differs from actual source/roles/catalogue')
await sourceVerifier.verify(facade,observation,options);await assertDatabaseSourceObservationCurrent(facade,observation,options) const terminalSource=await sourceVerifier.verify(facade,observation,options);await assertDatabaseSourceObservationCurrent(facade,observation,options)
sameOwned(first,await ownedContext(s,installation,PUBLISHER,'publisher-read')) sameOwned(first,await ownedContext(s,installation,PUBLISHER,'publisher-read'))
// Recheck the live interval after the potentially lengthy catalogue walk. // Recheck the live interval after the potentially lengthy catalogue walk.
const clock=(await s.query('SELECT (EXTRACT(EPOCH FROM pg_catalog.clock_timestamp())*1000)::bigint::text AS now')).rows const clock=(await s.query('SELECT (EXTRACT(EPOCH FROM pg_catalog.clock_timestamp())*1000)::bigint::text AS now')).rows
@@ -157,7 +157,7 @@ async function preflightCurrentProfile(pool,{deadlineAt=Date.now()+120000}={}){
if(approval.expiresAt<=Date.now())throw Error('Preflight approval expired before terminal') if(approval.expiresAt<=Date.now())throw Error('Preflight approval expired before terminal')
const terminal=await s.query('ROLLBACK');if(terminal.command!=='ROLLBACK')throw Error('Preflight terminal unconfirmed');s.begun(false) const terminal=await s.query('ROLLBACK');if(terminal.command!=='ROLLBACK')throw Error('Preflight terminal unconfirmed');s.begun(false)
return Object.freeze({state:'matched',epoch:current.epoch,headSha256:current.headSha256,notAfter:current.policy.notAfter, return Object.freeze({state:'matched',epoch:current.epoch,headSha256:current.headSha256,notAfter:current.policy.notAfter,
sourceMatched:true,rolesMatched:true,catalogueMatched:true,writerAuthorized:false,generationAuthenticated:false,readyAuthorized:false}) sourceMatched:true,rolesMatched:true,catalogueMatched:true,platformProtectionVerified:initialSource?.platformProtectionVerified===true&&terminalSource?.platformProtectionVerified===true,writerAuthorized:false,generationAuthenticated:false,readyAuthorized:false})
}) })
} }
async function boundedPending(filename){ async function boundedPending(filename){
+5
View File
@@ -3,10 +3,14 @@ import { applySqlMigrations } from "./migrate.js"
import { dropExpiredPartitions, ensurePartitionsAround } from "./partitions.js" import { dropExpiredPartitions, ensurePartitionsAround } from "./partitions.js"
import { importSqliteToPostgres, shouldImportSqlite, sqliteFileLooksPresent } from "./sqlite-import.js" import { importSqliteToPostgres, shouldImportSqlite, sqliteFileLooksPresent } from "./sqlite-import.js"
import { env } from "../config.js" import { env } from "../config.js"
import { mcpStateDirectory } from "../modules/mcp/security.js"
import { assertDatabaseMaintenanceStartup } from "../services/database-maintenance-startup.js"
import { reconcileDatabaseRestoreAudits } from "../services/database-restore-audit.js"
const ETL_LOCK = 8723101 const ETL_LOCK = 8723101
export async function initDatabase(): Promise<void> { export async function initDatabase(): Promise<void> {
await assertDatabaseMaintenanceStartup(mcpStateDirectory(), env.DATABASE_URL)
await pool.query("SELECT 1") await pool.query("SELECT 1")
await applySqlMigrations(pool) await applySqlMigrations(pool)
await ensurePartitionsAround(pool) await ensurePartitionsAround(pool)
@@ -39,4 +43,5 @@ export async function initDatabase(): Promise<void> {
} }
await dropExpiredPartitions(pool) await dropExpiredPartitions(pool)
await ensurePartitionsAround(pool) await ensurePartitionsAround(pool)
await reconcileDatabaseRestoreAudits(pool, mcpStateDirectory())
} }
+23 -6
View File
@@ -12,10 +12,10 @@ const reviewed = (source: string, extra: Partial<MigrationReview> = {}) => {
const bytes = Buffer.from(source), file = "9000_fixture.sql" const bytes = Buffer.from(source), file = "9000_fixture.sql"
return () => analyzeMigration(file, bytes, { file, sha256: createHash("sha256").update(bytes).digest("hex"), transaction: "owner-transaction", selfMarker: false, opaqueBodies: 0, bodyReview: [], effects: ["Explicit inert fixture review"], ...extra }) return () => analyzeMigration(file, bytes, { file, sha256: createHash("sha256").update(bytes).digest("hex"), transaction: "owner-transaction", selfMarker: false, opaqueBodies: 0, bodyReview: [], effects: ["Explicit inert fixture review"], ...extra })
} }
await test("all44 exact file pins and every top-level schema operation are inventoried", () => { await test("all 56 exact file pins and every top-level schema operation are inventoried", () => {
assert.equal(inventory.length, 44); assert.equal(MIGRATION_MANIFEST.length, 44) assert.equal(inventory.length, 56); assert.equal(MIGRATION_MANIFEST.length, 56)
assert.equal(inventory.filter(file => file.transaction === "owner-transaction").length, 17) assert.equal(inventory.filter(file => file.transaction === "owner-transaction").length, 17)
assert.equal(inventory.filter(file => file.transaction === "self-managed-single-transaction").length, 27) assert.equal(inventory.filter(file => file.transaction === "self-managed-single-transaction").length, 39)
assert(inventory.every(file => file.operations.length === file.statementCount && file.effects.length > 0)) assert(inventory.every(file => file.operations.length === file.statementCount && file.effects.length > 0))
let tables = new Set<string>() let tables = new Set<string>()
for (const file of inventory) { for (const file of inventory) {
@@ -32,8 +32,8 @@ await test("DO BEGIN and stored function BEGIN are never transaction boundaries"
assert.equal(endpoint.topLevelTransactionControls.length, 2); assert.equal(endpoint.opaqueBodies.length, 2) assert.equal(endpoint.topLevelTransactionControls.length, 2); assert.equal(endpoint.opaqueBodies.length, 2)
assert.deepEqual(endpoint.opaqueBodies.map(body => body.kind), ["stored-function-body", "executed-do-body"]) assert.deepEqual(endpoint.opaqueBodies.map(body => body.kind), ["stored-function-body", "executed-do-body"])
}) })
await test("0035 through0044 have exact atomic file-owned terminal markers", () => { await test("0035 through 0056 have exact atomic file-owned terminal markers", () => {
assert.deepEqual(inventory.filter(file => file.selfMarker).map(file => file.id), ["0035_server_probe_journal", "0036_diagnostic_cancellations", "0037_server_probe_storage_v2", "0038_server_poll_journal_and_writer_guards", "0039_server_poll_storage", "0040_mcp_routing_reservations", "0041_server_poll_terminal_archives", "0042_server_poll_settings_epoch", "0043_mcp_routing_forward_charges", "0044_mcp_routing_manual_inverse_charges"]) assert.deepEqual(inventory.filter(file => file.selfMarker).map(file => file.id), ["0035_server_probe_journal", "0036_diagnostic_cancellations", "0037_server_probe_storage_v2", "0038_server_poll_journal_and_writer_guards", "0039_server_poll_storage", "0040_mcp_routing_reservations", "0041_server_poll_terminal_archives", "0042_server_poll_settings_epoch", "0043_mcp_routing_forward_charges", "0044_mcp_routing_manual_inverse_charges", "0045_mcp_operation_replay_tombstones", "0046_server_poll_operation_retirement", "0047_server_poll_retirement_claims", "0048_mcp_ordinary_retirement_claims", "0049_mcp_ordinary_telemetry_retirement", "0050_server_probe_retired_sources", "0051_server_probe_retirement_claims", "0052_server_probe_retired_gc", "0053_server_probe_terminal_proofs", "0054_mcp_geoip_retirement", "0055_mcp_backup_schedule_retirement", "0056_mcp_manual_inverse_recovery_v8"])
for (const file of inventory.filter(file => file.selfMarker)) { assert.equal(file.selfMarker!.atomicWithAllFileEffects, true); assert.equal(file.selfMarker!.statement, file.statementCount - 2); assert.equal(file.currentOuterMarkerGap, false) } for (const file of inventory.filter(file => file.selfMarker)) { assert.equal(file.selfMarker!.atomicWithAllFileEffects, true); assert.equal(file.selfMarker!.statement, file.statementCount - 2); assert.equal(file.currentOuterMarkerGap, false) }
assert.equal(inventory.filter(file => file.currentOuterMarkerGap).length, 34) assert.equal(inventory.filter(file => file.currentOuterMarkerGap).length, 34)
}) })
@@ -134,7 +134,8 @@ await test("poll storage enrollment refuses history before any change and preser
for(const name of ["server_poll_executions","server_poll_intents","server_poll_acks","server_poll_receipts","server_poll_quarantines"])assert(guard.includes(`EXISTS(SELECT 1 FROM ${name})`)) for(const name of ["server_poll_executions","server_poll_intents","server_poll_acks","server_poll_receipts","server_poll_quarantines"])assert(guard.includes(`EXISTS(SELECT 1 FROM ${name})`))
for(const name of ["server_poll_storage_admissions","server_poll_storage_usage"])assert(guard.includes("'"+name+"'")&&CURRENT_REQUIRED_TABLES.includes(name)) for(const name of ["server_poll_storage_admissions","server_poll_storage_usage"])assert(guard.includes("'"+name+"'")&&CURRENT_REQUIRED_TABLES.includes(name))
const bodies=(list:ReturnType<typeof parseMigrationStatements>)=>list.filter(statement=>statement.tokens[0].value==="CREATE").flatMap(statement=>statement.tokens.filter(token=>token.kind==="dollar").map(token=>token.value)) const bodies=(list:ReturnType<typeof parseMigrationStatements>)=>list.filter(statement=>statement.tokens[0].value==="CREATE").flatMap(statement=>statement.tokens.filter(token=>token.kind==="dollar").map(token=>token.value))
assert.deepEqual(bodies(statements),bodies(historical));assert.equal(bodies(statements).length,5) const comparableBodies=(list:ReturnType<typeof parseMigrationStatements>)=>bodies(list).map(body=>body.replace(/\r\n/g,"\n"))
assert.deepEqual(comparableBodies(statements),comparableBodies(historical));assert.equal(bodies(statements).length,5)
assert(bodies(statements)[0].includes("mcp_settings.id=1 FOR SHARE"),"Predicate itself fences current retention, including direct guarded deletes") assert(bodies(statements)[0].includes("mcp_settings.id=1 FOR SHARE"),"Predicate itself fences current retention, including direct guarded deletes")
assert.deepEqual(migration.operations.filter(operation=>operation.command==="INSERT").map(operation=>operation.target),["server_poll_storage_usage","schema_migrations"]) assert.deepEqual(migration.operations.filter(operation=>operation.command==="INSERT").map(operation=>operation.target),["server_poll_storage_usage","schema_migrations"])
assert.equal(migration.operations.some(operation=>["UPDATE","DELETE","DROP"].includes(operation.command)),false) assert.equal(migration.operations.some(operation=>["UPDATE","DELETE","DROP"].includes(operation.command)),false)
@@ -227,4 +228,20 @@ await test("0044 adds only a fixed manual inverse ledger and current-version int
assert(!pin.operations.some(operation => operation.command === "UPDATE" || operation.command === "DELETE")) assert(!pin.operations.some(operation => operation.command === "UPDATE" || operation.command === "DELETE"))
}) })
await test("0056 changes only live manual inverse recovery version guards", async () => {
const migration = inventory.find(file => file.id === "0056_mcp_manual_inverse_recovery_v8")!
assert.equal(migration.statementCount, 5)
assert.equal(migration.opaqueBodies.length, 2)
assert.equal(migration.selfMarker?.atomicWithAllFileEffects, true)
const oldSql = await readFile(new URL("../../drizzle/0044_mcp_routing_manual_inverse_charges.sql", import.meta.url), "utf8")
const newSql = await readFile(new URL("../../drizzle/0056_mcp_manual_inverse_recovery_v8.sql", import.meta.url), "utf8")
for (const name of ["mcp_routing_manual_inverse_charge_source", "mcp_scoped_recovery_manual_version"]) {
const definition = new RegExp(`CREATE(?: OR REPLACE)? FUNCTION public\\.${name}\\(\\)[\\s\\S]*?END \\$\\$;`)
const original = oldSql.match(definition)?.[0]?.replaceAll("\r\n", "\n")
assert.ok(original)
assert.equal(newSql.match(definition)?.[0]?.replaceAll("\r\n", "\n"), original.replace("CREATE FUNCTION", "CREATE OR REPLACE FUNCTION")
.replace("p.operation_id='operations.recover' AND p.operation_version=7", "p.operation_id='operations.recover' AND p.operation_version=8"))
}
})
console.log(`migration-manifest: ${cases} inert parser/inventory/baseline scenarios passed; ${inventory.length} exact files`) console.log(`migration-manifest: ${cases} inert parser/inventory/baseline scenarios passed; ${inventory.length} exact files`)
+167 -1
View File
@@ -702,6 +702,172 @@ const classifiedMigrationPins: readonly MigrationReview[] = [
"Adds one one-unit immutable manual inverse charge table, two charge indexes, one manual-history page index, and four triggers. Existing43 bytes, reserved2 and generic debit ownership remain unchanged.", "Adds one one-unit immutable manual inverse charge table, two charge indexes, one manual-history page index, and four triggers. Existing43 bytes, reserved2 and generic debit ownership remain unchanged.",
"No inferred/backfilled identity, legacy timestamp debit, permission change, source enrollment, public controller activation or READY. Full RT3/all-writer/family-deletion proof remains open." "No inferred/backfilled identity, legacy timestamp debit, permission change, source enrollment, public controller activation or READY. Full RT3/all-writer/family-deletion proof remains open."
] ]
},
{
"file": "0045_mcp_operation_replay_tombstones.sql",
"sha256": "8edf5b074c7eef49dad7835c44ac2539df4347f19eb9dd9b2aa7005bafaadbc0",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 0,
"bodyReview": [],
"effects": [
"Adds one empty replay identity table with plan/operation uniqueness and connection-scoped idempotency digest uniqueness.",
"Table creation refuses an unmarked collision; the exact marker commits with the schema. No backfill, operation deletion, or worker activation."
]
},
{
"file": "0046_server_poll_operation_retirement.sql",
"sha256": "75d7956ea8134b01bcc88da083b77eb52f6e1ad70ad854918ed3380cb0e8cf88",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 4,
"bodyReview": [
"Archive guard permits only fixed receipt-scoped V1 to V2 updates with matching approved terminal operation, tombstone and SQL 180-day cutoff.",
"Deferred guard independently requires the tombstone, physically absent operation and plan, and exact ledger accounting.",
"Admission guard retains existing paths and applies exact V2 archive-byte delta to both owners without changing row charges or lifetime fields.",
"Existing GC retains its authority/activity cutoff and admits retired identity only with exact poll tombstone and compatible V2 archive when present."
],
"effects": [
"Extends the same archive table to a bounded version 2, changes three guards and adds one deferred retirement guard.",
"No automatic conversion, worker activation, operation deletion or retention-window adjustment in migration."
]
},
{
"file": "0047_server_poll_retirement_claims.sql",
"sha256": "8c01ab14d241f715af021e8a7d7f5bb273b450b32f566140359260282524e01d",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 2,
"bodyReview": [
"Protects exact current-transaction V1 claim insertion and permits deletion only as the owning admission is cascaded by GC.",
"Requires exact receipt, V1 archive proof and claim token already committed in another transaction before V2 update."
],
"effects": [
"Adds one durable one-shot poll retirement claim per admission, cascading only with that admission's existing guarded GC.",
"No migration-time conversion, backfill, dispatch, or quota change."
]
},
{
"file": "0048_mcp_ordinary_retirement_claims.sql",
"sha256": "5bc3598c9f5d09611e98d05345accda6d39b199a583bba789b97a24140fafbfd",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 1,
"bodyReview": [
"Makes ordinary claims immutable and allows insert only for the exact linked settled and aged logs.settings.update v1 operation."
],
"effects": [
"Adds one durable ordinary-operation claim with an authenticated source digest. No migration-time data purge or backfill."
]
},
{
"file": "0049_mcp_ordinary_telemetry_retirement.sql",
"sha256": "68993e924d20db6b08973846a0da367fe89fe2948c414c810e9b171dbbae6761",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 1,
"bodyReview": [
"Extends the same immutable ordinary claim guard to four explicitly versioned settled telemetry settings families."
],
"effects": [
"Replaces only the ordinary claim guard function. No backfill, data purge, or changes to poll or diagnostic storage."
]
},
{
"file": "0050_server_probe_retired_sources.sql",
"sha256": "0468ac7e53638b532557273f55d065ff4cde092d8b08073f5860bab35263aead",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 1,
"bodyReview": [
"Refuses update, delete, and truncate of a signed retired diagnostic source pending a separately reviewed lifecycle."
],
"effects": [
"Creates empty bounded storage linking exact diagnostic admission and replay tombstone IDs. No backfill, retirement, or GC activation."
]
},
{
"file": "0051_server_probe_retirement_claims.sql",
"sha256": "b38876b5da44cf14e20a9df0baaa233ea4de81e471b38dddd370e149c8116243",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 2,
"bodyReview": [
"Restricts diagnostic claims to their original settled 180-day source and makes each claim permanent.",
"Restricts archival insertion to the exact committed claim and current original source and tombstone identity."
],
"effects": [
"Creates empty diagnostic retirement claims and guards the archive insert. No conversion, backfill, or GC activation."
]
},
{
"file": "0052_server_probe_retired_gc.sql",
"sha256": "ea56a1f1bdabfce606bdfec56b6a7b31d45b0187fbea4c900bc656ae320f2fdd",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 3,
"bodyReview": [
"Validates the exact retired diagnostic identity and the later of current and persisted retention plus the original safety window.",
"Only permits archive removal during an exact guarded admission cascade after journal removal.",
"Adds the exact retired GC branch while retaining the live admission eligibility predicate."
],
"effects": [
"Changes only the retired source admission FK to guarded cascade and adds an explicit exact GC path. No collection runs during migration."
]
},
{
"file": "0053_server_probe_terminal_proofs.sql",
"sha256": "759ec8c349fcb24c43af410ad6ade714432678b3a021c94bd982080d6a8986be",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 2,
"bodyReview": [
"Pins terminal proof to the current retained original on insert and an exact committed retirement claim on delete.",
"Extends claims only to the identical signed terminal proof when the original diagnostic admission is gone."
],
"effects": [
"Creates empty bounded private terminal proof storage. Existing payload retention, scheduler, and archive GC remain unchanged."
]
},
{
"file": "0054_mcp_geoip_retirement.sql",
"sha256": "680390f98076e3047dbb5a34eab417b253853703932d16c17e4fa3a11060accc",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 1,
"bodyReview": [
"Adds only exact settled GeoIP settings version 1 to the existing immutable ordinary retirement claim guard."
],
"effects": [
"Replaces the ordinary claim guard function without changing settings or retiring existing rows during migration."
]
},
{
"file": "0055_mcp_backup_schedule_retirement.sql",
"sha256": "07e63c1245ea547176d613933a81ad354e734fa64218dadced67ee8f965c01b6",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 1,
"bodyReview": [
"Adds only exact settled backup schedule settings version 1 to the existing immutable ordinary retirement claim guard."
],
"effects": [
"Replaces the ordinary claim guard function without changing backup settings, metadata or entries during migration."
]
},
{
"file": "0056_mcp_manual_inverse_recovery_v8.sql",
"sha256": "1dece1d9c5a9cf69e51336f35f868561dad9da6d21556ea31ef5b102634ed9ee",
"transaction": "self-managed-single-transaction",
"selfMarker": true,
"opaqueBodies": 2,
"bodyReview": [
"The manual inverse charge-source guard requires recovery version 8 and preserves its pending operation, source version, owner and profile checks.",
"The manual inverse intent guard requires recovery version 8 and preserves its advisory lock and exact running service intent checks."
],
"effects": [
"Replaces the two live version guard functions and preserves existing charges, intents, and plans."
]
} }
] ]
@@ -724,7 +890,7 @@ export const migrationManifestDigest = () => hash(JSON.stringify(MIGRATION_MANIF
// resetting the accumulated public table set at the reviewed destructive0002. // resetting the accumulated public table set at the reviewed destructive0002.
// This is a necessary presence check, not a substitute for exact live column, // This is a necessary presence check, not a substitute for exact live column,
// constraint/index/function/trigger comparison by the control protocol. // constraint/index/function/trigger comparison by the control protocol.
export const CURRENT_REQUIRED_TABLES: readonly string[] = Object.freeze(["acme_settings","alert_engine_confirm_pending","alert_engine_cursor","alert_engine_prev_live","alert_engine_state","alert_groups","alert_history","alert_outbox","alert_rule_conditions","alert_rule_targets","alert_rules","alert_telegram_settings","app_users","backup_entries","backup_schedule_settings","backup_storage_settings","certificate_issue_jobs","certificate_renew_settings","config_revisions","data_migration","events","evobgp_settings","filter_rules","flow_asn_meta","flow_buckets","flow_daily_dims","flow_daily_facts","flow_hour_facts","flow_ip_meta","flow_minute_dims","flow_minute_stats","flow_settings_intents","flow_write_receipts","geoip_settings","internet_path_settings","internet_path_snapshots","mcp_artifacts","mcp_audit","mcp_budget_buckets","mcp_connections","mcp_credentials","mcp_diagnostic_cancellations","mcp_drift","mcp_grants","mcp_management_channels","mcp_oauth_clients","mcp_oauth_requests","mcp_operation_checkpoints","mcp_operation_steps","mcp_operations","mcp_owner_revocations","mcp_path_checks","mcp_plans","mcp_portal_events","mcp_protections","mcp_read_leases","mcp_recovery_intents","mcp_requests","mcp_routing_forward_charges","mcp_routing_manual_inverse_charges","mcp_routing_observations","mcp_routing_profiles","mcp_routing_reservation_slots","mcp_routing_reservations","mcp_scoped_recovery_intents","mcp_secrets","mcp_settings","mcp_unauthorized_buckets","recursive_routes","router_log_collection_state","router_log_entries","router_log_settings","scheduler_runs","schema_migrations","server_poll_acks","server_poll_archives","server_poll_executions","server_poll_intents","server_poll_quarantines","server_poll_receipts","server_poll_storage_admissions","server_poll_storage_usage","server_probe_execution_payloads","server_probe_execution_quarantines","server_probe_execution_stages","server_probe_executions","server_probe_storage_admissions","server_probe_storage_usage","server_snapshots","servers","servers_api_ping_settings","servers_rest_ping_samples","traffic_collection_state","traffic_flow_settings","traffic_samples","traffic_settings","uptime_probe_samples","uptime_probes","uptime_resource_samples","uptime_settings","uptime_speed_probes","uptime_speed_test_runs","user_interface_bindings","vrf_production_tickets"]) export const CURRENT_REQUIRED_TABLES: readonly string[] = Object.freeze(["acme_settings","alert_engine_confirm_pending","alert_engine_cursor","alert_engine_prev_live","alert_engine_state","alert_groups","alert_history","alert_outbox","alert_rule_conditions","alert_rule_targets","alert_rules","alert_telegram_settings","app_users","backup_entries","backup_schedule_settings","backup_storage_settings","certificate_issue_jobs","certificate_renew_settings","config_revisions","data_migration","events","evobgp_settings","filter_rules","flow_asn_meta","flow_buckets","flow_daily_dims","flow_daily_facts","flow_hour_facts","flow_ip_meta","flow_minute_dims","flow_minute_stats","flow_settings_intents","flow_write_receipts","geoip_settings","internet_path_settings","internet_path_snapshots","mcp_artifacts","mcp_audit","mcp_budget_buckets","mcp_connections","mcp_credentials","mcp_diagnostic_cancellations","mcp_drift","mcp_grants","mcp_management_channels","mcp_oauth_clients","mcp_oauth_requests","mcp_operation_checkpoints","mcp_operation_replay_tombstones","mcp_operation_steps","mcp_operations","mcp_ordinary_retirement_claims","mcp_owner_revocations","mcp_path_checks","mcp_plans","mcp_portal_events","mcp_protections","mcp_read_leases","mcp_recovery_intents","mcp_requests","mcp_routing_forward_charges","mcp_routing_manual_inverse_charges","mcp_routing_observations","mcp_routing_profiles","mcp_routing_reservation_slots","mcp_routing_reservations","mcp_scoped_recovery_intents","mcp_secrets","mcp_settings","mcp_unauthorized_buckets","recursive_routes","router_log_collection_state","router_log_entries","router_log_settings","scheduler_runs","schema_migrations","server_poll_acks","server_poll_archives","server_poll_executions","server_poll_intents","server_poll_quarantines","server_poll_receipts","server_poll_retirement_claims","server_poll_storage_admissions","server_poll_storage_usage","server_probe_execution_payloads","server_probe_execution_quarantines","server_probe_execution_stages","server_probe_executions","server_probe_retired_sources","server_probe_retirement_claims","server_probe_storage_admissions","server_probe_storage_usage","server_probe_terminal_proofs","server_snapshots","servers","servers_api_ping_settings","servers_rest_ping_samples","traffic_collection_state","traffic_flow_settings","traffic_samples","traffic_settings","uptime_probe_samples","uptime_probes","uptime_resource_samples","uptime_settings","uptime_speed_probes","uptime_speed_test_runs","user_interface_bindings","vrf_production_tickets"])
export interface LegacyBaselineEvidence { export interface LegacyBaselineEvidence {
inventoryComplete: boolean; databaseGeneration: string; unresolvedAttemptIds: readonly string[] inventoryComplete: boolean; databaseGeneration: string; unresolvedAttemptIds: readonly string[]
+4
View File
@@ -4,6 +4,8 @@ import { serializerCompiler, validatorCompiler } from "@fastify/type-provider-zo
import { monitorEventLoopDelay } from "node:perf_hooks" import { monitorEventLoopDelay } from "node:perf_hooks"
import { env } from "./config.js" import { env } from "./config.js"
import { initDatabase } from "./db/bootstrap.js" import { initDatabase } from "./db/bootstrap.js"
import { mcpStateDirectory } from "./modules/mcp/security.js"
import { assertDatabaseMaintenanceStartup } from "./services/database-maintenance-startup.js"
import { closePool } from "./db/index.js" import { closePool } from "./db/index.js"
import authPlugin, { requireApiAuth } from "./plugins/auth.js" import authPlugin, { requireApiAuth } from "./plugins/auth.js"
import { installDatabaseRequestGate } from "./plugins/database-request-gate.js" import { installDatabaseRequestGate } from "./plugins/database-request-gate.js"
@@ -186,6 +188,8 @@ const isMain =
if (isMain) { if (isMain) {
try { try {
// Resolve external maintenance before init/migrations or consumer startup.
await assertDatabaseMaintenanceStartup(mcpStateDirectory(), env.DATABASE_URL)
await initDatabase() await initDatabase()
const app = await buildApp() const app = await buildApp()
let shuttingDown = false let shuttingDown = false
+1 -1
View File
@@ -5,7 +5,7 @@ import type { ConnectionRow, McpPrincipal } from "./store.js"
import type { RosSnapshot, RosStep } from "./resources.js" import type { RosSnapshot, RosStep } from "./resources.js"
export interface ActionContext { export interface ActionContext {
connection: ConnectionRow; principal: McpPrincipal; signal?: AbortSignal connection: ConnectionRow; principal: McpPrincipal; signal?: AbortSignal; logsFollowMaintenance?: AbortSignal
/** Process-internal persistent controller identity; never parsed from tool JSON. */ /** Process-internal persistent controller identity; never parsed from tool JSON. */
persistentRoutingProfile?: { id: string; version: number } persistentRoutingProfile?: { id: string; version: number }
/** Available only during target resolution, never retained by the read/prepare handler. */ /** Available only during target resolution, never retained by the read/prepare handler. */
+62 -14
View File
@@ -9,14 +9,17 @@ import { registerIpsecExistingCertificateExportActions } from "./ipsec-existing-
import { registerIpsecUserUpdateActions } from "./ipsec-user-update-actions.js" import { registerIpsecUserUpdateActions } from "./ipsec-user-update-actions.js"
import { prepareIpsecNativeWrite } from "./ipsec-native-write-actions.js" import { prepareIpsecNativeWrite } from "./ipsec-native-write-actions.js"
import { isIP } from "node:net" import { isIP } from "node:net"
import { setTimeout as sleep } from "node:timers/promises"
import { logsFollowCancellation } from "./logs-follow-cancellation.js"
import { z } from "zod" import { z } from "zod"
import { logFiltersSchema, logSettingsPatchSchema } from "@mmapp/contracts" import { logFiltersSchema, logSettingsPatchSchema } from "@mmapp/contracts"
import { appUserCreateSchema, appUserUpdateSchema, appUserRoleSchema, sectionPermSchema, serverPermSchema, userBindingCreateSchema } from "@mmapp/contracts/users" import { appUserCreateSchema, appUserUpdateSchema, appUserRoleSchema, sectionPermSchema, serverPermSchema, userBindingCreateSchema } from "@mmapp/contracts/users"
import type { McpOperationDescriptor } from "@mmapp/contracts/mcp" import type { McpOperationDescriptor } from "@mmapp/contracts/mcp"
import { hasPermission } from "../../lib/permissions.js" import { hasPermission } from "../../lib/permissions.js"
import { assertOperationReplayScope, findOperationReplayTombstone, operationHistoryExpired } from "./operation-replay-tombstones.js"
import { pool } from "../../db/index.js" import { pool } from "../../db/index.js"
import { getServerReadById } from "../servers/service/servers-service.js" import { getServerReadById } from "../servers/service/servers-service.js"
import { listUsers, getUserById, listInterfaceCatalog, getInterfaceCatalogState } from "../users/service/users-service.js" import { listUsers, getUserById, listInterfaceCatalog, getInterfaceCatalogState, validateUserBindingCreate, UsersServiceError } from "../users/service/users-service.js"
import { getLogSettings } from "../../services/router-logs-collector.js" import { getLogSettings } from "../../services/router-logs-collector.js"
import { readLogPage, readLogSummary, readLogContext, readLogEntry } from "../../services/router-logs-query.js" import { readLogPage, readLogSummary, readLogContext, readLogEntry } from "../../services/router-logs-query.js"
import { logWhere } from "../../services/router-logs-model.js" import { logWhere } from "../../services/router-logs-model.js"
@@ -103,6 +106,7 @@ import { registerRecursiveRecovery } from "./recursive-recovery.js"
import { registerNetworkHostActions } from "./network-host-actions.js" import { registerNetworkHostActions } from "./network-host-actions.js"
import { registerBgpFilterBindingActions } from "./bgp-filter-binding-actions.js" import { registerBgpFilterBindingActions } from "./bgp-filter-binding-actions.js"
import { registerBgpCommunityPolicyActions } from "./bgp-community-policy-actions.js" import { registerBgpCommunityPolicyActions } from "./bgp-community-policy-actions.js"
import { registerBridgeVlanActions, prepareGenericBridgeVlanChange } from "./bridge-vlan-actions.js"
const serverId = z.number().int().positive().max(Number.MAX_SAFE_INTEGER) const serverId = z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
const rowId = z.string().trim().min(1).max(200) const rowId = z.string().trim().min(1).max(200)
@@ -126,6 +130,7 @@ registerRecursiveRecovery(add)
registerNetworkHostActions(add) registerNetworkHostActions(add)
registerBgpFilterBindingActions(add) registerBgpFilterBindingActions(add)
registerBgpCommunityPolicyActions(add) registerBgpCommunityPolicyActions(add)
registerBridgeVlanActions(add)
registerTrafficHistoryActions(add) registerTrafficHistoryActions(add)
registerSidebarCountsActions(add) registerSidebarCountsActions(add)
registerLogArtifactActions(add) registerLogArtifactActions(add)
@@ -153,7 +158,7 @@ for (const resource of rosResources) {
const delegated = resource.id === "wireguard" && action !== "create" || resource.id === "wireguard_peer" || resource.id === "gre" && action !== "create" || resource.id === "ipsec_peer" && action !== "create" || resource.id === "certificate" && action === "delete" || resource.id === "container" && action === "delete" const delegated = resource.id === "wireguard" && action !== "create" || resource.id === "wireguard_peer" || resource.id === "gre" && action !== "create" || resource.id === "ipsec_peer" && action !== "create" || resource.id === "certificate" && action === "delete" || resource.id === "container" && action === "delete"
const previousVersion = action === "create" ? (["ospf_template", "wireguard_peer", "wireguard"].includes(resource.id) ? 3 : 2) : resource.id === "ipsec_peer" || resource.id === "certificate" && action === "delete" ? 2 : resource.id === "wireguard" && action === "update" ? 3 : resource.id === "ospf_template" && action !== "delete" || resource.id === "wireguard_peer" || resource.id === "wireguard" ? 2 : 1 const previousVersion = action === "create" ? (["ospf_template", "wireguard_peer", "wireguard"].includes(resource.id) ? 3 : 2) : resource.id === "ipsec_peer" || resource.id === "certificate" && action === "delete" ? 2 : resource.id === "wireguard" && action === "update" ? 3 : resource.id === "ospf_template" && action !== "delete" || resource.id === "wireguard_peer" || resource.id === "wireguard" ? 2 : 1
add(`ros.${resource.id}.${action}`, `mm_${resource.id}_${action}_plan`, `${resource.title}: ${action}`, resource.permission.split(":")[1], resource.id === "certificate" && action === "update" ? ["mm:certificates:write", "mm:network:write"] : resource.permission + ":write", "write", "servers", schema, { add(`ros.${resource.id}.${action}`, `mm_${resource.id}_${action}_plan`, `${resource.title}: ${action}`, resource.permission.split(":")[1], resource.id === "certificate" && action === "update" ? ["mm:certificates:write", "mm:network:write"] : resource.permission + ":write", "write", "servers", schema, {
version: resource.id === "ipsec_peer" ? (action === "create" ? 5 : 3) : resource.id === "certificate" ? (action === "update" ? 4 : 3) : previousVersion + (delegated ? 0 : 1) + (resource.id === "ospf_template" ? 1 : 0) + (resource.id === "vrf" ? 1 : 0), version: resource.id === "ipsec_peer" ? (action === "create" ? 5 : 3) : resource.id === "certificate" ? (action === "update" ? 4 : 3) : previousVersion + (delegated ? 0 : 1) + (resource.id === "ospf_template" ? 1 : 0) + (resource.id === "vrf" ? (action === "delete" ? 3 : 2) : 0) + (["bridge", "bridge_port", "bridge_vlan"].includes(resource.id) ? 1 : 0),
prepare: async (input, context) => { prepare: async (input, context) => {
if (resource.id === "vrf" && input.productionAdmission !== true) throw new McpError("UNSUPPORTED_CAPABILITY", "VRF требует явного productionAdmission:true для проверенного native executor; raw план недоступен", 409) if (resource.id === "vrf" && input.productionAdmission !== true) throw new McpError("UNSUPPORTED_CAPABILITY", "VRF требует явного productionAdmission:true для проверенного native executor; raw план недоступен", 409)
const sid = Number(input.serverId), id = input.id as string | undefined const sid = Number(input.serverId), id = input.id as string | undefined
@@ -249,13 +254,24 @@ for (const resource of rosResources) {
if (dependent) throw new McpError("STATE_CONFLICT", "Объект используется: сначала подготовьте явное изменение зависимостей", 409) if (dependent) throw new McpError("STATE_CONFLICT", "Объект используется: сначала подготовьте явное изменение зависимостей", 409)
} }
} }
const linkedVlan = await prepareGenericBridgeVlanChange({ serverId: sid, resource: resource.id, action, id, fields }, context)
if (linkedVlan) {
const captured = new Map(snapshots.map(snapshot => [snapshot.resource, snapshot]))
for (const snapshot of linkedVlan.snapshots) {
const previous = captured.get(snapshot.resource)
if (previous && (previous.fingerprint !== snapshot.fingerprint || previous.sourceProof !== snapshot.sourceProof)) throw new McpError("STATE_CONFLICT", "Зависимости VLAN изменились во время подготовки", 409)
captured.set(snapshot.resource, snapshot)
}
const objects = new Map([...references, ...linkedVlan.objects].map(object => [`${object.serverId}/${object.kind}/${object.id}`, object]))
return { ...linkedVlan, snapshots: [...captured.values()], objects: [...objects.values()] }
}
const placeBeforeId = resource.id === "vrf" && action === "create" ? before.rows[0]?.[".id"] : undefined const placeBeforeId = resource.id === "vrf" && action === "create" ? before.rows[0]?.[".id"] : undefined
if (resource.id === "vrf" && action === "create" && typeof placeBeforeId !== "string") throw new McpError("STATE_CONFLICT", "Не подтверждён порядок существующих VRF", 409) if (resource.id === "vrf" && action === "create" && typeof placeBeforeId !== "string") throw new McpError("STATE_CONFLICT", "Не подтверждён порядок существующих VRF", 409)
return { target: { scope: "servers", serverIds: [sid] }, objects: references, snapshots, return { target: { scope: "servers", serverIds: [sid] }, objects: references, snapshots,
...(resource.id === "vrf" && input.productionAdmission === true ? { vrfProductionAdmission: true as const } : {}), ...(resource.id === "vrf" && input.productionAdmission === true ? { vrfProductionAdmission: true as const } : {}),
steps: splitOspfPassiveUpdate({ serverId: sid, resource: resource.id, action, id, fields, ...(typeof placeBeforeId === "string" ? { placeBeforeId } : {}) }).map(step => ({ kind: "ros" as const, step })), steps: splitOspfPassiveUpdate({ serverId: sid, resource: resource.id, action, id, fields, ...(typeof placeBeforeId === "string" ? { placeBeforeId } : {}) }).map(step => ({ kind: "ros" as const, step })),
requiresHuman: true, requiresHuman: true,
warnings: ["Нативное изменение может затронуть управление устройством и требует подтверждения человеком.", "Автоматическое каскадное изменение зависимостей не выполняется.", ...(placeBeforeId ? [`VRF создаётся перед записью ${placeBeforeId}; порядок определяет принадлежность интерфейсов.`] : [])], warnings: ["Нативное изменение может затронуть управление устройством и требует подтверждения человеком.", "Автоматическое каскадное изменение зависимостей не выполняется.", ...(resource.id === "vrf" && action === "delete" ? ["При удалении VRF RouterOS может удалить автоматический IPv6 link-local и его connected-маршрут пустого bridge; статические адреса сохраняются."] : []), ...(placeBeforeId ? [`VRF создаётся перед записью ${placeBeforeId}; порядок определяет принадлежность интерфейсов.`] : [])],
} }
}, },
}) })
@@ -267,7 +283,7 @@ for (const definition of [
{ id: "firewall.update", tool: "mm_firewall_rule_update_plan", title: "План изменения правила firewall", resources: ["ip_filter", "ip_nat", "ip_mangle", "ip_raw", "ipv6_filter", "ipv6_nat", "ipv6_mangle", "ipv6_raw"] }, { id: "firewall.update", tool: "mm_firewall_rule_update_plan", title: "План изменения правила firewall", resources: ["ip_filter", "ip_nat", "ip_mangle", "ip_raw", "ipv6_filter", "ipv6_nat", "ipv6_mangle", "ipv6_raw"] },
] as const) { ] as const) {
add(definition.id, definition.tool, definition.title, "network", "mm:network:write", "write", "servers", z.object({ serverId, resource: z.enum(definition.resources), id: rowId, fields: z.record(z.string().max(100), z.union([z.string().max(1000), z.number().finite(), z.boolean()])) }).strict(), { add(definition.id, definition.tool, definition.title, "network", "mm:network:write", "write", "servers", z.object({ serverId, resource: z.enum(definition.resources), id: rowId, fields: z.record(z.string().max(100), z.union([z.string().max(1000), z.number().finite(), z.boolean()])) }).strict(), {
version: definition.id === "interfaces.update" ? 4 : 2, version: definition.id === "interfaces.update" ? 5 : 2,
prepare: async (input, context) => { prepare: async (input, context) => {
const resource = resourceById(String(input.resource)), fields = resource.fields.safeParse(input.fields) const resource = resourceById(String(input.resource)), fields = resource.fields.safeParse(input.fields)
if (!fields.success) throw new McpError("INVALID_INPUT", "Поля не соответствуют типизированной схеме выбранного объекта", 400) if (!fields.success) throw new McpError("INVALID_INPUT", "Поля не соответствуют типизированной схеме выбранного объекта", 400)
@@ -321,6 +337,21 @@ for (const action of ["create", "update", "delete", "binding_add", "binding_remo
const ids = [...new Set([...(user?.servers ?? []).map(server => Number(server.serverId)), ...(binding?.serverId ? [binding.serverId] : []), const ids = [...new Set([...(user?.servers ?? []).map(server => Number(server.serverId)), ...(binding?.serverId ? [binding.serverId] : []),
...(before?.bindings ?? []).map(binding => binding.serverId), ...(before?.servers ?? []).map(server => Number(server.serverId))])] ...(before?.bindings ?? []).map(binding => binding.serverId), ...(before?.servers ?? []).map(server => Number(server.serverId))])]
if (ids.some(id => !context.connection.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Сервер не разрешён") if (ids.some(id => !context.connection.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Сервер не разрешён")
if (action === "binding_remove" && !before?.bindings.some(binding => binding.id === input.bindingId)) {
throw new McpError("NOT_FOUND", "Привязка не найдена", 404)
}
if (action === "binding_add") {
try {
await validateUserBindingCreate(String(input.id), userBindingCreateSchema.parse(input.binding), { signal: context.signal })
} catch (error) {
if (error instanceof UsersServiceError) {
if (error.status === 409) throw new McpError("STATE_CONFLICT", error.message, 409)
if (error.status === 400) throw new McpError("INVALID_INPUT", error.message, 400)
if (error.status === 404) throw new McpError("NOT_FOUND", error.message, 404)
}
throw error
}
}
return { target: { scope: "global" }, objects: [{ kind: "user", id: String(input.id ?? "new") }, ...ids.map(id => ({ kind: "server", id: String(id), serverId: id }))], snapshots: [], before, return { target: { scope: "global" }, objects: [{ kind: "user", id: String(input.id ?? "new") }, ...ids.map(id => ({ kind: "server", id: String(id), serverId: id }))], snapshots: [], before,
steps: [{ kind: "service", service: `users.${action}`, input }], warnings: ["Изменение /users не меняет JWT-права портала."] } steps: [{ kind: "service", service: `users.${action}`, input }], warnings: ["Изменение /users не меняет JWT-права портала."] }
}, },
@@ -349,16 +380,17 @@ function logFilters(input: Record<string, unknown>, context: ActionContext) {
} }
add("logs.query", "mm_logs_query", "История логов", "logs", "mm:servers:read", "read", "servers", logInput, { add("logs.query", "mm_logs_query", "История логов", "logs", "mm:servers:read", "read", "servers", logInput, {
read: async (input, context) => { read: async (input, context) => {
const filters = logFilters(input, context), generation = await accessGeneration() const generation = await accessGeneration()
let filters: ReturnType<typeof logFilters>
let cursor: string | undefined, watermark: string | undefined let cursor: string | undefined, watermark: string | undefined
if (input.cursor) { if (input.cursor) {
try { try {
const state = JSON.parse(await decryptSecret(String(input.cursor))) as { generation: string; scope: string; cursor: string; watermark: string; from: string } const state = JSON.parse(await decryptSecret(String(input.cursor))) as { generation: string; scope: string; cursor: string; watermark: string; from: string }
if (input.from === undefined) filters.from = state.from filters = logFilters({ ...input, from: input.from ?? state.from }, context)
if (state.generation !== generation || state.scope !== fingerprint({ filters, connection: context.connection.id })) throw new Error("cursor") if (state.generation !== generation || state.scope !== fingerprint({ filters, connection: context.connection.id })) throw new Error("cursor")
cursor = state.cursor; watermark = state.watermark cursor = state.cursor; watermark = state.watermark
} catch { throw new McpError("RESET_REQUIRED", "Курсор устарел или относится к другим фильтрам", 409) } } catch { throw new McpError("RESET_REQUIRED", "Курсор устарел или относится к другим фильтрам", 409) }
} } else filters = logFilters(input, context)
const result = await readLogPage(filters, { cursor, watermark, limit: Number(input.limit) }) const result = await readLogPage(filters, { cursor, watermark, limit: Number(input.limit) })
return { ...result, nextCursor: result.nextCursor ? await encryptSecret(JSON.stringify({ generation, scope: fingerprint({ filters, connection: context.connection.id }), cursor: result.nextCursor, watermark: result.watermark, from: filters.from })) : null } return { ...result, nextCursor: result.nextCursor ? await encryptSecret(JSON.stringify({ generation, scope: fingerprint({ filters, connection: context.connection.id }), cursor: result.nextCursor, watermark: result.watermark, from: filters.from })) : null }
}, },
@@ -373,30 +405,41 @@ for (const context of [false, true]) add(context ? "logs.context" : "logs.get",
return context ? readLogContext(String(input.id)) : entry return context ? readLogContext(String(input.id)) : entry
}, },
}) })
add("logs.follow", "mm_logs_follow", "Новые события", "logs", "mm:servers:read", "read", "servers", logInput.safeExtend({ serverId, cursor: z.string().max(8192).optional(), waitSeconds: z.number().int().min(0).max(30).default(30) }), { add("logs.follow", "mm_logs_follow", "Новые события", "logs", "mm:servers:read", "read", "servers", logInput.safeExtend({ serverId, limit: logInput.shape.limit.default(200), cursor: z.string().max(8192).optional(), waitSeconds: z.number().int().min(0).max(30).default(30) }), {
read: async (input, context) => { read: async (input, context) => {
const maintenance = context.logsFollowMaintenance
const signal = context.signal
const waitSignal = maintenance && signal ? AbortSignal.any([maintenance, signal]) : maintenance ?? signal
const assertActive = () => { const failure = logsFollowCancellation(maintenance, signal); if (failure) throw failure }
assertActive()
const { server } = await routerClient(Number(input.serverId)); void server const { server } = await routerClient(Number(input.serverId)); void server
assertActive()
const release = subscribeLogDevice(Number(input.serverId)), until = Date.now() + Number(input.waitSeconds) * 1000 const release = subscribeLogDevice(Number(input.serverId)), until = Date.now() + Number(input.waitSeconds) * 1000
try { try {
const filters = logFilters(input, context), generation = await accessGeneration() const generation = await accessGeneration()
let filters: ReturnType<typeof logFilters>
let cursor = "0" let cursor = "0"
if (input.cursor) { if (input.cursor) {
try { try {
const saved = JSON.parse(await decryptSecret(String(input.cursor))) const saved = JSON.parse(await decryptSecret(String(input.cursor)))
filters.from = saved.from filters = logFilters({ ...input, from: input.from ?? saved.from }, context)
if (saved.kind !== "follow" || saved.generation !== generation || saved.scope !== fingerprint({ filters, connection: context.connection.id }) || !/^\d{1,20}$/.test(saved.cursor)) throw new Error("cursor") if (saved.kind !== "follow" || saved.generation !== generation || saved.scope !== fingerprint({ filters, connection: context.connection.id }) || !/^\d{1,20}$/.test(saved.cursor)) throw new Error("cursor")
cursor = saved.cursor cursor = saved.cursor
} catch { throw new McpError("RESET_REQUIRED", "Курсор follow устарел или относится к другим фильтрам", 409) } } catch { throw new McpError("RESET_REQUIRED", "Курсор follow устарел или относится к другим фильтрам", 409) }
} else { } else {
filters = logFilters(input, context)
cursor = (await pool.query("SELECT COALESCE(max(id),0)::text AS cursor FROM router_log_entries WHERE server_id=$1", [input.serverId])).rows[0].cursor cursor = (await pool.query("SELECT COALESCE(max(id),0)::text AS cursor FROM router_log_entries WHERE server_id=$1", [input.serverId])).rows[0].cursor
} }
while (true) { while (true) {
assertActive()
const current = await validatePrincipal(context.principal) const current = await validatePrincipal(context.principal)
assertActive()
if (current.policy.version !== context.connection.policy.version) throw new McpError("RESET_REQUIRED", "Права или область доступа изменились; начните новое чтение", 409) if (current.policy.version !== context.connection.policy.version) throw new McpError("RESET_REQUIRED", "Права или область доступа изменились; начните новое чтение", 409)
const batch = await readLogStream(filters, cursor) const batch = await readLogStream(filters, cursor, Number(input.limit))
assertActive()
cursor = batch.cursor cursor = batch.cursor
if (batch.entries.length || !batch.enabled || Date.now() >= until || context.signal?.aborted) return { ...batch, cursor: await encryptSecret(JSON.stringify({ kind: "follow", generation, scope: fingerprint({ filters, connection: context.connection.id }), cursor, from: filters.from })) } if (batch.entries.length || !batch.enabled || Date.now() >= until) return { ...batch, cursor: await encryptSecret(JSON.stringify({ kind: "follow", generation, scope: fingerprint({ filters, connection: context.connection.id }), cursor, from: filters.from })) }
await new Promise(resolve => setTimeout(resolve, 1000)) await sleep(1000, undefined, { signal: waitSignal })
} }
} finally { release() } } finally { release() }
}, },
@@ -473,7 +516,12 @@ add("changes.cancel", "mm_change_cancel", "Отменить свой незап
add("operations.read", "mm_operation_get", "Результат своей операции", "mcp", null, "read", "connection", z.object({ operationId: z.string().uuid() }).strict(), { add("operations.read", "mm_operation_get", "Результат своей операции", "mcp", null, "read", "connection", z.object({ operationId: z.string().uuid() }).strict(), {
read: async (input, context) => { read: async (input, context) => {
const { rows: [operation] } = await pool.query("SELECT * FROM mcp_operations WHERE id=$1 AND connection_id=$2", [input.operationId, context.connection.id]) const { rows: [operation] } = await pool.query("SELECT * FROM mcp_operations WHERE id=$1 AND connection_id=$2", [input.operationId, context.connection.id])
if (!operation) throw new McpError("NOT_FOUND", "Операция не найдена", 404) if (!operation) {
const tombstone = await findOperationReplayTombstone(pool, context.connection.id, String(input.operationId))
if (!tombstone) throw new McpError("NOT_FOUND", "Операция не найдена", 404)
assertOperationReplayScope(tombstone, context.connection)
operationHistoryExpired()
}
await assertPlanReadScope(await (await import("./runtime.js")).loadPlan(operation.plan_id), context) await assertPlanReadScope(await (await import("./runtime.js")).loadPlan(operation.plan_id), context)
const { rows: steps } = await pool.query("SELECT * FROM mcp_operation_steps WHERE operation_id=$1 ORDER BY step_index", [input.operationId]) const { rows: steps } = await pool.query("SELECT * FROM mcp_operation_steps WHERE operation_id=$1 ORDER BY step_index", [input.operationId])
return { ...operation, steps } return { ...operation, steps }
@@ -17,6 +17,7 @@ try{
await pool.query("INSERT INTO backup_storage_settings(id) VALUES(1) ON CONFLICT(id) DO NOTHING") await pool.query("INSERT INTO backup_storage_settings(id) VALUES(1) ON CONFLICT(id) DO NOTHING")
await updateMcpSettings({enabled:true},actor) await updateMcpSettings({enabled:true},actor)
app=await buildApp({startScheduler:false});await app.ready() app=await buildApp({startScheduler:false});await app.ready()
await (await import("../../test/mcp-users-bindings.js")).verifyMcpUserBindings(actor)
await (await import("./routing-community-context-acceptance.js")).acceptRouteCommunityContextMcp(actor) await (await import("./routing-community-context-acceptance.js")).acceptRouteCommunityContextMcp(actor)
await (await import("../../test/mcp-acme-test.js")).verifyMcpAcmeTest(actor) await (await import("../../test/mcp-acme-test.js")).verifyMcpAcmeTest(actor)
await (await import("../../test/mcp-acme-settings.js")).verifyMcpAcmeSettings(actor) await (await import("../../test/mcp-acme-settings.js")).verifyMcpAcmeSettings(actor)
@@ -0,0 +1,153 @@
import { z } from "zod"
import { isIP } from "node:net"
import { bridgeVlanConfigurationRequestSchema } from "@mmapp/contracts/bridge-vlan"
import { buildBridgeVlanConfiguration, bridgeVlanCurrentDesired, BridgeVlanConfigurationError, type BridgeVlanState } from "../../services/bridge-vlan-configuration.js"
import { readRosSnapshot, routerClient, rosSnapshotSourceProof, type RosSnapshot, type RosStep } from "./resources.js"
import { verifyBridgeManagementChannel } from "./management.js"
import { McpError } from "./security.js"
import { canonicalBridgeVlanValue } from "../../services/bridge-vlan-values.js"
import type { ActionContext, ChangeEvidence } from "./action-types.js"
import type { add } from "./actions.js"
const interfaceResources = ["ethernet", "bridge", "vlan", "gre", "wireguard", "vxlan"]
const resources = [...interfaceResources, "bridge_port", "bridge_vlan", "ipv4_address", "ipv6_address"]
export async function readBridgeVlanState(serverId: number, context: Pick<ActionContext, "signal">): Promise<{ state: BridgeVlanState; snapshots: RosSnapshot[] }> {
const snapshots: RosSnapshot[] = []
const active = async () => { if (context.signal?.aborted) throw new McpError("READ_CANCELLED", "Чтение конфигурации VLAN отменено", 409) }
// Bind every catalogue to the same configured device source. The executor
// rechecks these complete configuration snapshots before each native write.
for (const resource of resources) snapshots.push(await readRosSnapshot(serverId, resource, active, snapshots[0]?.sourceProof))
const rows = (resource: string) => snapshots.find(snapshot => snapshot.resource === resource)!.rows
const state: BridgeVlanState = {
bridges: rows("bridge"), ports: rows("bridge_port"), vlans: rows("bridge_vlan"),
interfaces: snapshots.filter(snapshot => interfaceResources.includes(snapshot.resource)).flatMap(snapshot => snapshot.rows.map(row => ({
resource: snapshot.resource, id: String(row[".id"]), name: String(row.name), disabled: row.disabled,
}))),
vlanInterfaces: rows("vlan"), ipv4Addresses: rows("ipv4_address"), ipv6Addresses: rows("ipv6_address"),
}
return { state, snapshots }
}
export async function prepareBridgeVlanConfiguration(raw: unknown, context: ActionContext): Promise<ChangeEvidence> {
const input = bridgeVlanConfigurationRequestSchema.parse(raw)
let management: Awaited<ReturnType<typeof verifyBridgeManagementChannel>> | undefined
try { management = await verifyBridgeManagementChannel(input.serverId) }
catch (error) {
// Only lack of a current alternate channel permits primary-path admission.
// Identity, transport and other verification failures stay fail-closed.
if (!(error instanceof McpError) || error.code !== "APPROVAL_REQUIRED") throw error
}
const { state, snapshots } = await readBridgeVlanState(input.serverId, context)
const primaryManagement = management === undefined
if (!management) {
const { server } = await routerClient(input.serverId)
if (!isIP(server.host)) throw new McpError("MANAGEMENT_UNSAFE", "Основной адрес управления должен быть буквальным IP, подтверждённым в конфигурации устройства", 409)
if (!snapshots[0]?.sourceProof || await rosSnapshotSourceProof(server) !== snapshots[0].sourceProof) throw new McpError("STATE_CONFLICT", "Источник основного управления изменился во время подготовки VLAN", 409)
const expiresAt = Math.min(Date.parse(context.connection.policy.expiresAt), context.connection.owner_valid_until.getTime(), Date.parse(context.principal.credentialExpiresAt))
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) throw new McpError("APPROVAL_REQUIRED", "Нет действующего допуска для основного канала управления", 409)
management = { channelId: "primary", serverId: input.serverId, host: server.host, expiresAt: new Date(expiresAt).toISOString() }
}
try {
const plan = buildBridgeVlanConfiguration({ ...input, state, management, now: Date.now() })
if (!plan.steps.length) throw new McpError("STATE_CONFLICT", "Конфигурация VLAN уже соответствует плану", 409)
return {
target: { scope: "servers", serverIds: [input.serverId] }, objects: plan.objects,
snapshots, steps: plan.steps.map(step => ({ kind: "ros" as const, step })),
requiresHuman: true, validUntil: management.expiresAt,
...(primaryManagement ? {} : { managementChannels: [{ channelId: management.channelId, serverId: management.serverId, expiresAt: management.expiresAt }] }),
warnings: ["Связанный план изменяет VLAN memberships и PVID в указанном порядке; включение filtering выполняется последним.",
"Каждая подтверждённая запись проверяется до следующей. При неизвестном результате дальнейшие записи останавливаются.",
primaryManagement ? "Используется основной адрес управления; независимость канала не подтверждена. Обязательно подтверждение человеком; каждый префикс плана сохраняет подтверждённый management-доступ."
: "Проверяется конфигурация и доступность зарегистрированного management-канала. Передача пользовательского трафика требует отдельной сетевой проверки."],
}
} catch (error) {
if (error instanceof BridgeVlanConfigurationError) throw new McpError(error.code, error.message, 409)
throw error
}
}
/** A legacy single-row tool must pass the same effective-state checks, but may
* not acquire authority to edit other resource kinds through this delegation. */
export async function prepareGenericBridgeVlanChange(step: RosStep, context: ActionContext): Promise<ChangeEvidence | undefined> {
const sensitive = step.resource === "bridge" ? ["pvid", "vlan-filtering", "frame-types", "ingress-filtering"] : step.resource === "bridge_port"
? ["bridge", "interface", "pvid", "ingress-filtering", "frame-types", "disabled"] : ["bridge", "vlan-ids", "tagged", "untagged", "disabled"]
if (!["bridge", "bridge_port", "bridge_vlan"].includes(step.resource) || step.resource === "bridge" && step.action !== "update"
|| step.action === "update" && !Object.keys(step.fields).some(key => sensitive.includes(key))) return undefined
const { state } = await readBridgeVlanState(step.serverId, context)
const rows = step.resource === "bridge" ? state.bridges : step.resource === "bridge_port" ? state.ports : state.vlans
const row = rows.find(item => item[".id"] === step.id)
const bridgeName = step.resource === "bridge" ? row?.name : step.fields.bridge ?? row?.bridge
const bridges = state.bridges.filter(item => item.name === bridgeName)
if (bridges.length !== 1 || step.resource !== "bridge" && row && row.bridge !== bridgeName) throw new McpError("STATE_CONFLICT", "Смена bridge требует отдельных явных планов удаления и добавления порта", 409)
const bridgeId = String(bridges[0][".id"])
try {
const desired = bridgeVlanCurrentDesired(state, bridgeId)
const bool = (value: unknown): boolean => {
if (value === "true" || value === true) return true
if (value === "false" || value === false) return false
throw new McpError("INVALID_INPUT", "Нужны явные параметры порта VLAN", 400)
}
const ref = (name: unknown) => {
const matches = state.interfaces.filter(iface => iface.name === name)
if (matches.length !== 1) throw new McpError("INVALID_INPUT", "Не подтверждён стабильный ID участника VLAN", 400)
return { resource: matches[0].resource, id: matches[0].id }
}
if (step.resource === "bridge") {
if (Object.keys(step.fields).some(key => !sensitive.includes(key))) throw new McpError("INVALID_INPUT", "Подготовьте свойства bridge и VLAN filtering отдельными планами", 400)
if (step.fields.pvid !== undefined) desired.pvid = Number(step.fields.pvid)
if (step.fields["vlan-filtering"] !== undefined) desired.filtering = bool(step.fields["vlan-filtering"])
if (step.fields["frame-types"] !== undefined) desired.frameTypes = bridgeVlanConfigurationRequestSchema.shape.desired.shape.frameTypes.parse(step.fields["frame-types"])
if (step.fields["ingress-filtering"] !== undefined) desired.ingressFiltering = bool(step.fields["ingress-filtering"])
} else if (step.resource === "bridge_port") {
const index = desired.ports.findIndex(port => port.id === step.id)
if (step.action === "delete") {
if (index < 0) throw new McpError("NOT_FOUND", "Статический порт не найден", 404)
desired.ports.splice(index, 1); desired.removePortIds.push(step.id!)
} else {
const effective = { ...row, ...step.fields }
const port = { ...(step.action === "update" ? { id: step.id } : {}), interface: ref(effective.interface), pvid: Number(effective.pvid),
ingressFiltering: bool(effective["ingress-filtering"]), frameTypes: effective["frame-types"], disabled: bool(effective.disabled),
...(effective.comment === undefined ? {} : { comment: effective.comment }) }
const parsed = bridgeVlanConfigurationRequestSchema.shape.desired.shape.ports.element.parse(port)
if (step.action === "create") desired.ports.push(parsed)
else { if (index < 0) throw new McpError("NOT_FOUND", "Статический порт не найден", 404); desired.ports[index] = parsed }
}
} else {
const index = desired.vlans.findIndex(vlan => vlan.id === step.id)
if (step.action === "delete") {
if (index < 0) throw new McpError("NOT_FOUND", "Статическая VLAN membership не найдена", 404)
desired.vlans.splice(index, 1); desired.removeVlanIds.push(step.id!)
} else {
const effective = { ...row, ...step.fields }
const members = (value: unknown) => value === undefined || value === "" ? [] : String(value).split(",").map(ref)
const vlan = { ...(step.action === "update" ? { id: step.id } : {}), vlanIds: effective["vlan-ids"], tagged: members(effective.tagged), untagged: members(effective.untagged),
disabled: effective.disabled === undefined && step.action === "create" ? false : bool(effective.disabled), ...(effective.comment === undefined ? {} : { comment: effective.comment }) }
const parsed = bridgeVlanConfigurationRequestSchema.shape.desired.shape.vlans.element.parse(vlan)
if (step.action === "create") desired.vlans.push(parsed)
else { if (index < 0) throw new McpError("NOT_FOUND", "Статическая VLAN membership не найдена", 404); desired.vlans[index] = parsed }
}
}
const evidence = await prepareBridgeVlanConfiguration({ serverId: step.serverId, bridgeId, desired }, context)
if (evidence.steps.length !== 1 || evidence.steps[0].kind !== "ros" || evidence.steps[0].step.resource !== step.resource || evidence.steps[0].step.action !== step.action || evidence.steps[0].step.id !== step.id) {
throw new McpError("APPROVAL_REQUIRED", "Для связанных изменений используйте явный план mm_bridge_vlan_configuration_plan с соответствующим разрешением", 409)
}
const native = evidence.steps[0].step
if (step.action === "update" && Object.entries(native.fields).some(([key, value]) => !Object.hasOwn(step.fields, key)
|| (step.resource === "bridge_vlan" ? canonicalBridgeVlanValue(key, value) !== canonicalBridgeVlanValue(key, step.fields[key]) : value !== step.fields[key]))) {
throw new McpError("STATE_CONFLICT", "Состояние VLAN изменилось во время подготовки; незапрошенные поля не будут перезаписаны", 409)
}
return evidence
} catch (error) {
if (error instanceof BridgeVlanConfigurationError) throw new McpError(error.code, error.message, 409)
throw error
}
}
export function registerBridgeVlanActions(register: typeof add) {
register("network.bridge-vlan.read", "mm_bridge_vlan_configuration_get", "Полная конфигурация bridge VLAN и стабильные ID участников", "network", "mm:network:read", "read", "servers", z.object({ serverId: z.number().int().positive() }).strict(), {
read: async (input, context) => ({ state: (await readBridgeVlanState(Number(input.serverId), context)).state }),
})
register("network.bridge-vlan.configure", "mm_bridge_vlan_configuration_plan", "Связанный план VLAN membership, PVID и bridge filtering", "network", "mm:network:write", "write", "servers", bridgeVlanConfigurationRequestSchema, {
prepare: prepareBridgeVlanConfiguration,
})
}
@@ -0,0 +1,21 @@
import assert from "node:assert/strict"
process.env.AUTH_REQUIRED = "true"
process.env.AUTH_JWT_SECRET = "bridge-vlan-owned-acceptance-secret"
process.env.AUTH_ISSUER = "https://auth.test.local"
const { pool, closePool } = await import("../../db/index.js")
const { applySqlMigrations } = await import("../../db/migrate.js")
const { updateMcpSettings } = await import("./store.js")
const { buildApp } = await import("../../index.js")
const actor = { id: "bridge-vlan-owner", email: "bridge@example.invalid", name: "Bridge VLAN owner", apps: ["mm"], permissions: ["mm:settings:admin", "mm:network:admin"], expiresAt: Date.now() + 3600000 }
let app: Awaited<ReturnType<typeof buildApp>> | undefined
try {
await applySqlMigrations(pool)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM servers")).rows[0].n, 0, "Disposable empty database required")
await updateMcpSettings({ enabled: true }, actor)
app = await buildApp({ startScheduler: false }); await app.ready()
await (await import("../../test/mcp-bridge-vlan-configuration.js")).verifyMcpBridgeVlanConfiguration(actor, app)
} finally {
await (await import("../../services/scheduler.js")).stopSchedulerForDatabaseOperation()
await (await import("../../services/traffic-flow-ingest.js")).stopTrafficFlowListenerForDatabaseOperation()
await app?.close(); await closePool()
}
@@ -0,0 +1,76 @@
import assert from "node:assert/strict"
import test from "node:test"
import { configurationRows, resourceById, rosSnapshotVersion, type ConfigurationRow, type RosSnapshot, type RosStep } from "./resources.js"
import { createRosPrefix } from "./ros-prefix.js"
import { verifyRosStepSnapshots } from "./ros-step-verification.js"
import { fingerprint, McpError } from "./security.js"
const snapshot = (resource: string, raw: ConfigurationRow[]): RosSnapshot => {
const rows = configurationRows(resourceById(resource), raw)
return { serverId: 1, resource, rows, fingerprint: fingerprint(rows), sourceProof: "a".repeat(64), projectionVersion: rosSnapshotVersion(resource) }
}
const membership = { ".id": "*1", bridge: "br", "vlan-ids": "20-22", tagged: "ether2,ether3", dynamic: "false", disabled: "false" }
test("VLAN readback accepts equivalent VID/member sets and omitted empty fields, retaining exact native identity", () => {
const before = snapshot("bridge_vlan", [membership])
const after = snapshot("bridge_vlan", [{ ...membership, "vlan-ids": "22,20,21", tagged: "ether3,ether2" }])
const step: RosStep = { serverId: 1, resource: "bridge_vlan", action: "update", id: "*1", fields: { "vlan-ids": "20-22", tagged: "ether3,ether2", untagged: "", comment: "" } }
assert.equal(verifyRosStepSnapshots(step, {}, before, after), true)
assert.equal(verifyRosStepSnapshots(step, {}, before, snapshot("bridge_vlan", [{ ...membership, "vlan-ids": "20-23" }])), false)
assert.equal(verifyRosStepSnapshots(step, {}, before, snapshot("bridge_vlan", [{ ...membership, ".id": "*2" }])), false)
assert.throws(() => snapshot("bridge_vlan", [{ ...membership, "vlan-ids": "20,20-22" }]), /Недостоверная/)
})
test("PVID-generated rows may change while the sealed static VLAN configuration remains exact", async () => {
const port = { ".id": "*2", bridge: "br", interface: "ether2", pvid: "10", disabled: "false" }
const dynamic = { ".id": "*D", bridge: "br", "vlan-ids": "10", untagged: "ether2", dynamic: "true" }
const raw = new Map<string, ConfigurationRow[]>([["bridge_port", [port]], ["bridge_vlan", [membership, dynamic]]])
const initial = [...raw].map(([resource, rows]) => snapshot(resource, rows))
const read = async (_server: number, resource: string, active: () => Promise<unknown> = async () => {}) => { await active(); return snapshot(resource, raw.get(resource)!) }
const prefix = createRosPrefix(initial, read)
await prefix.check()
raw.set("bridge_port", [{ ...port, pvid: "20" }])
raw.set("bridge_vlan", [membership, { ...dynamic, ".id": "*E", "vlan-ids": "20" }])
const step: RosStep = { serverId: 1, resource: "bridge_port", action: "update", id: "*2", fields: { pvid: "20" } }
await prefix.advance(step, {}, await read(1, "bridge_port"))
await prefix.check()
raw.set("bridge_vlan", [{ ...membership, tagged: "ether2" }, dynamic])
await assert.rejects(prefix.check(), error => error instanceof McpError && error.code === "STATE_CONFLICT")
})
test("full VLAN ranges and long sparse VID sets stay representable in plans and inverse snapshots", () => {
const resource = resourceById("bridge_vlan")
const expanded = Array.from({ length: 4094 }, (_, index) => String(index + 1)).join(",")
const sparse = Array.from({ length: 2047 }, (_, index) => String(index * 2 + 1)).join(",")
assert.ok(sparse.length > 100)
for (const value of ["1-4094", sparse]) assert.equal(resource.fields.safeParse({ "vlan-ids": value }).success, true)
for (const value of ["0", "4095", "10-9", "10,10-12"]) assert.equal(resource.fields.safeParse({ "vlan-ids": value }).success, false)
const before = snapshot("bridge_vlan", [membership])
const after = snapshot("bridge_vlan", [{ ...membership, "vlan-ids": expanded }])
assert.equal(after.rows[0]["vlan-ids"], "1-4094", "Native expansion must not overflow the public inverse DTO")
assert.equal(verifyRosStepSnapshots({ serverId: 1, resource: "bridge_vlan", action: "update", id: "*1", fields: { "vlan-ids": "1-4094" } }, {}, before, after), true)
})
test("unknown dynamic flags are retained; only explicit RouterOS-generated VLAN rows are excluded", () => {
assert.equal(snapshot("bridge_vlan", [{ ...membership, dynamic: "unknown" }]).rows.length, 1)
for (const dynamic of [true, "true", "yes"]) assert.equal(snapshot("bridge_vlan", [{ ...membership, dynamic }]).rows.length, 0)
assert.equal(snapshot("bridge_vlan", [{ ...membership, dynamic: "false" }]).rows.length, 1)
})
test("old bridge CPU, port and VLAN projection contracts cannot execute after semantic projection changes", () => {
for (const resource of ["bridge", "bridge_port", "bridge_vlan"]) {
const current = snapshot(resource, resource === "bridge_vlan" ? [membership] : [{ ".id": "*2", bridge: "br", interface: "ether2", pvid: "10" }])
assert.equal(current.projectionVersion, 4)
assert.throws(() => createRosPrefix([{ ...current, projectionVersion: 3 }]), error => error instanceof McpError && error.code === "PLAN_EXPIRED")
}
})
test("CPU frame admission and MVRP configuration are sealed, with no MVRP write permission", () => {
const bridge = { ".id": "*1", name: "br", pvid: "10", "vlan-filtering": "true", "frame-types": "admit-all", "ingress-filtering": "true", mvrp: "false", running: "true" }
const original = snapshot("bridge", [bridge])
for (const field of ["frame-types", "ingress-filtering", "mvrp"]) {
const changed = snapshot("bridge", [{ ...bridge, [field]: field === "frame-types" ? "admit-only-vlan-tagged" : "false" === bridge[field as keyof typeof bridge] ? "true" : "false" }])
assert.notEqual(changed.fingerprint, original.fingerprint)
}
assert.equal(snapshot("bridge", [{ ...bridge, running: "false" }]).fingerprint, original.fingerprint)
assert.equal(resourceById("bridge").fields.safeParse({ mvrp: true }).success, false)
})
@@ -0,0 +1,135 @@
import { bridgeVlanStateSchema, type BridgeVlanDesired, type BridgeVlanState } from "@mmapp/contracts/bridge-vlan"
import { bridgeVlanCurrentDesired } from "../../services/bridge-vlan-configuration.js"
import type { ActionContext, ChangeEvidence } from "./action-types.js"
import { prepareBridgeVlanConfiguration, readBridgeVlanState } from "./bridge-vlan-actions.js"
import { acknowledgedCreateId, assertNoCallerCreateId, recoveryRosStepId } from "./native-create-identity.js"
import { readRosSnapshot, rosSnapshotVersion, type RosSnapshot, type RosStep } from "./resources.js"
import { verifyRosStepSnapshots } from "./ros-step-verification.js"
import { decryptSecret, fingerprint, McpError } from "./security.js"
type Checkpoint = { step_index: number; before_sealed: string; after_sealed: string | null }
type Intended = { step_index: number; state: string; result: unknown }
const key = (snapshot: Pick<RosSnapshot, "serverId" | "resource">) => `${snapshot.serverId}:${snapshot.resource}`
function conflict(message: string): never { throw new McpError("STATE_CONFLICT", message, 409) }
export function needsBridgeVlanRecovery(sourceAction: string, evidence: ChangeEvidence): boolean {
if (sourceAction === "network.bridge-vlan.configure") return true
return evidence.steps.some(change => {
if (change.kind !== "ros") return false
const step = change.step
if (step.resource === "bridge") return step.action === "update" && Object.keys(step.fields).some(field => ["pvid", "vlan-filtering", "frame-types", "ingress-filtering"].includes(field))
const sensitive = step.resource === "bridge_port" ? ["bridge", "interface", "pvid", "ingress-filtering", "frame-types", "disabled"]
: step.resource === "bridge_vlan" ? ["bridge", "vlan-ids", "tagged", "untagged", "disabled"] : []
return sensitive.length > 0 && (step.action !== "update" || Object.keys(step.fields).some(field => sensitive.includes(field)))
})
}
function validSnapshot(snapshot: RosSnapshot): boolean {
return snapshot.projectionVersion === rosSnapshotVersion(snapshot.resource) && !!snapshot.sourceProof && Array.isArray(snapshot.rows) && snapshot.fingerprint === fingerprint(snapshot.rows)
}
function sameSnapshot(left: RosSnapshot, right: RosSnapshot): boolean {
return validSnapshot(left) && validSnapshot(right) && key(left) === key(right) && left.fingerprint === right.fingerprint && left.sourceProof === right.sourceProof
}
function stateFromSnapshots(serverId: number, snapshots: Map<string, RosSnapshot>): BridgeVlanState {
const rows = (resource: string) => {
const snapshot = snapshots.get(key({ serverId, resource }))
if (!snapshot || !validSnapshot(snapshot)) conflict("Нет полного исходного снимка bridge VLAN с актуальной проекцией")
return snapshot.rows
}
return bridgeVlanStateSchema.parse({ bridges: rows("bridge"), ports: rows("bridge_port"), vlans: rows("bridge_vlan"),
vlanInterfaces: rows("vlan"), ipv4Addresses: rows("ipv4_address"), ipv6Addresses: rows("ipv6_address"),
interfaces: ["ethernet", "bridge", "vlan", "gre", "wireguard", "vxlan"].flatMap(resource => rows(resource).map(row => ({ resource, id: String(row[".id"]), name: String(row.name), disabled: row.disabled }))),
})
}
/** Fresh typed inverse of a completely verified transition. Deleted port/VLAN
* rows are recreated without their old IDs; new rows are removed only by ACK. */
export async function prepareBridgeVlanRecovery(input: {
operationId: string; evidence: ChangeEvidence; checkpoints: Checkpoint[]; intended: Intended[];
}, context: ActionContext): Promise<ChangeEvidence> {
const { operationId, evidence, checkpoints, intended } = input
if (!evidence.steps.length || evidence.steps.some(change => change.kind !== "ros" || !["bridge", "bridge_port", "bridge_vlan"].includes(change.step.resource))) {
throw new McpError("UNSUPPORTED_CAPABILITY", "Обратный VLAN план не поддерживает смешанные ресурсы", 409)
}
const native = evidence.steps.map(change => (change as { kind: "ros"; step: RosStep }).step)
if (native.some(step => step.resource === "bridge" && step.action !== "update")) {
throw new McpError("UNSUPPORTED_CAPABILITY", "Создание или удаление самого bridge не поддерживает типизированный обратный VLAN план", 409)
}
const serverIds = [...new Set(native.map(step => step.serverId))]
if (serverIds.length !== 1 || evidence.target.scope !== "servers" || evidence.target.serverIds.length !== 1 || evidence.target.serverIds[0] !== serverIds[0]) conflict("Нет однозначного сервера исходного VLAN плана")
const serverId = serverIds[0]
if (!context.connection.policy.scope.serverIds.includes(serverId)) throw new McpError("SERVER_DENIED", "VLAN сервер вне текущего допуска")
if (intended.length !== native.length || checkpoints.length !== native.length || new Set(intended.map(item => item.step_index)).size !== native.length || new Set(checkpoints.map(item => item.step_index)).size !== native.length) conflict("Нет полного набора подтверждённых шагов VLAN")
const baseline = new Map<string, RosSnapshot>()
for (const snapshot of evidence.snapshots) {
if (snapshot.serverId !== serverId || !validSnapshot(snapshot) || baseline.has(key(snapshot))) conflict("Исходные VLAN снимки или их версии не подтверждены")
baseline.set(key(snapshot), snapshot)
}
const baselineState = stateFromSnapshots(serverId, baseline), expected = new Map(baseline)
const created: Array<{ resource: "bridge_port" | "bridge_vlan"; id: string }> = []
const deleted: Array<{ resource: "bridge_port" | "bridge_vlan"; id: string }> = []
for (let index = 0; index < native.length; index++) {
const step = native[index], checkpoint = checkpoints.find(item => item.step_index === index), receipt = intended.find(item => item.step_index === index)
assertNoCallerCreateId(step)
if (!checkpoint?.before_sealed || !checkpoint.after_sealed || receipt?.state !== "verified") conflict("Нет полного проверенного checkpoint VLAN; требуется ручная сверка")
const before = JSON.parse(await decryptSecret(checkpoint.before_sealed)) as RosSnapshot
const after = JSON.parse(await decryptSecret(checkpoint.after_sealed)) as RosSnapshot
const previous = expected.get(key({ serverId, resource: step.resource }))
if (!previous || !sameSnapshot(previous, before) || !validSnapshot(after) || before.sourceProof !== after.sourceProof || !verifyRosStepSnapshots(step, receipt.result, before, after)) conflict("Исходный переход VLAN или источник не подтверждён")
if (step.action === "create") {
const ack = acknowledgedCreateId(receipt.result), actual = recoveryRosStepId(step, before.rows, after.rows)
if (!ack || actual !== ack || !["bridge_port", "bridge_vlan"].includes(step.resource)) conflict("Нет точного ACK и checkpoint ID созданного VLAN объекта")
created.push({ resource: step.resource as "bridge_port" | "bridge_vlan", id: ack })
} else if (step.action === "delete") {
if (!step.id || !["bridge_port", "bridge_vlan"].includes(step.resource)) conflict("Не подтверждён ID удалённого VLAN объекта")
deleted.push({ resource: step.resource as "bridge_port" | "bridge_vlan", id: step.id })
}
expected.set(key(after), after)
}
const bridgeNames = new Set(native.map(step => {
const rows = step.resource === "bridge" ? baselineState.bridges : step.resource === "bridge_port" ? baselineState.ports : baselineState.vlans
const row = rows.find(item => item[".id"] === step.id)
const name = step.resource === "bridge" ? row?.name : step.fields.bridge ?? row?.bridge
if (typeof name !== "string" || !name) conflict("Не подтверждён исходный bridge VLAN")
return name
}))
const bridges = baselineState.bridges.filter(row => bridgeNames.has(String(row.name)))
if (bridgeNames.size !== 1 || bridges.length !== 1) conflict("Обратный VLAN план требует один однозначный bridge")
const bridgeId = String(bridges[0][".id"]), desired: BridgeVlanDesired = bridgeVlanCurrentDesired(baselineState, bridgeId)
for (const item of deleted) {
const terminal = expected.get(key({ serverId, resource: item.resource }))
const original = item.resource === "bridge_port" ? desired.ports.find(row => row.id === item.id) : desired.vlans.find(row => row.id === item.id)
if (!original || !terminal || terminal.rows.some(row => row[".id"] === item.id)) conflict("Нет подтверждённого удаления исходной VLAN identity")
delete original.id
}
for (const item of created) {
const originalRows = baseline.get(key({ serverId, resource: item.resource }))!.rows
const terminalRows = expected.get(key({ serverId, resource: item.resource }))!.rows
if (originalRows.some(row => row[".id"] === item.id) || !terminalRows.some(row => row[".id"] === item.id)) conflict("ACK ID VLAN не является подтверждённым новым объектом")
if (item.resource === "bridge_port") desired.removePortIds.push(item.id)
else desired.removeVlanIds.push(item.id)
}
const compare = async (snapshots: RosSnapshot[]): Promise<RosSnapshot[]> => {
const captured = new Map(snapshots.map(snapshot => [key(snapshot), snapshot]))
for (const snapshot of snapshots) {
const terminal = expected.get(key(snapshot))
if (!terminal || !sameSnapshot(terminal, snapshot)) conflict("После операции изменились VLAN конфигурация, зависимости или источник; обратный план запрещён")
}
for (const [resourceKey, terminal] of expected) {
const current = captured.get(resourceKey) ?? await readRosSnapshot(serverId, terminal.resource, undefined, terminal.sourceProof)
if (!sameSnapshot(terminal, current)) conflict("После операции обнаружен drift зависимости VLAN; обратный план запрещён")
captured.set(resourceKey, current)
}
return [...captured.values()]
}
await compare((await readBridgeVlanState(serverId, context)).snapshots)
// Reconstruct through the same model, with a freshly verified management
// channel. Old receipts never confer authority to run the inverse.
const inverse = await prepareBridgeVlanConfiguration({ serverId, bridgeId, desired }, context)
const snapshots = await compare(inverse.snapshots)
const objects = new Map([...evidence.objects, ...inverse.objects].map(object => [`${object.serverId}/${object.kind}/${object.id}`, object]))
return { ...inverse, snapshots, objects: [...objects.values()], requiresHuman: true,
warnings: [`Типизированный обратный VLAN план операции ${operationId}; все исходные шаги и ACK проверены.`, ...inverse.warnings,
"Удалённые port/VLAN rows создаются с новыми ID RouterOS. Unknown операции не повторяются; восстановление самого bridge не поддерживается."],
}
}
@@ -1,3 +1,4 @@
import { isConfirmedRoutingFibTable } from "../../services/routing-table-fib.js"
import type { RouteAiProfile } from "@mmapp/contracts/mcp-routing" import type { RouteAiProfile } from "@mmapp/contracts/mcp-routing"
import { nativeBgpRows, type NativeBgpRow } from "../../services/bgp-export-model.js" import { nativeBgpRows, type NativeBgpRow } from "../../services/bgp-export-model.js"
import { stableBgpPolicyRow } from "../../services/bgp-filter-binding-model.js" import { stableBgpPolicyRow } from "../../services/bgp-filter-binding-model.js"
@@ -56,7 +57,7 @@ export async function readControlPathContext(profile: RouteAiProfile, candidateI
return matching[0] return matching[0]
} }
const sourceId = profile.serverId, table = unique(await get(sourceId, "/routing/table"), profile.probeVrf) const sourceId = profile.serverId, table = unique(await get(sourceId, "/routing/table"), profile.probeVrf)
if (![true, "true", "yes"].includes(table.fib as string | boolean) || [true, "true", "yes"].includes(table.disabled as string | boolean) || [true, "true", "yes"].includes(table.invalid as string | boolean)) throw new McpError("STATE_UNAVAILABLE", "FIB probe VRF контрольного пути не подтверждён", 409) if (!isConfirmedRoutingFibTable(table)) throw new McpError("STATE_UNAVAILABLE", "FIB probe VRF контрольного пути не подтверждён", 409)
const vrfs = await get(sourceId, "/ip/vrf"), effectiveVrfs: NativeBgpRow[] = [] const vrfs = await get(sourceId, "/ip/vrf"), effectiveVrfs: NativeBgpRow[] = []
await enabled(sourceId, "/ip/vrf", unique(vrfs, profile.probeVrf)) await enabled(sourceId, "/ip/vrf", unique(vrfs, profile.probeVrf))
for (const row of vrfs) effectiveVrfs.push([true, "true", "yes"].includes(row.disabled as string | boolean) ? row : await enabled(sourceId, "/ip/vrf", row)) for (const row of vrfs) effectiveVrfs.push([true, "true", "yes"].includes(row.disabled as string | boolean) ? row : await enabled(sourceId, "/ip/vrf", row))
@@ -31,6 +31,15 @@ const verifyData = process.env.MMAPP_OWN_TERMINAL_DATA_EQUIVALENCE === "true"
let dataExpectation: Awaited<ReturnType<typeof import("./database-restore-data-equivalence.sql-helper.js")["prepareOwnedRestoreDataExpectation"]>> | undefined let dataExpectation: Awaited<ReturnType<typeof import("./database-restore-data-equivalence.sql-helper.js")["prepareOwnedRestoreDataExpectation"]>> | undefined
const originalConnect = pool.connect const originalConnect = pool.connect
let migrationCaptures = 0 let migrationCaptures = 0
async function waitForDatabaseRetirement() {
let timer: ReturnType<typeof setTimeout> | undefined
try {
await Promise.race([sharedDatabaseGate.whenOpen(), new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(Error("Terminal restore did not retire its database admission")), 10000)
})])
} finally { clearTimeout(timer) }
}
try { try {
// Every owned restore profile archives inert domain collectors, not only the // Every owned restore profile archives inert domain collectors, not only the
// data-equivalence profile. Otherwise restart can launch unrelated downloads // data-equivalence profile. Otherwise restart can launch unrelated downloads
@@ -138,6 +147,10 @@ try {
await assert.rejects(assertPortalClaimsCurrent(revokedOwner)) await assert.rejects(assertPortalClaimsCurrent(revokedOwner))
await assert.rejects(authenticateMcpToken(issued.token)) await assert.rejects(authenticateMcpToken(issued.token))
await assert.rejects(readRestoreReceipt(operation.id, { ...actor, permissions: ["mm:mcp:read"] })) await assert.rejects(readRestoreReceipt(operation.id, { ...actor, permissions: ["mm:mcp:read"] }))
// The applied receipt is durable before the terminal callback releases its
// last SQL lease/exclusive admission. Await that existing owner, rather than
// racing its cleanup with the stale-callback/no-replay assertion below.
await waitForDatabaseRetirement()
// A stale callback or process replay cannot invoke pg_restore a second time. // A stale callback or process replay cannot invoke pg_restore a second time.
await pool.query("UPDATE mcp_restore_acceptance_marker SET value='after-receipt'") await pool.query("UPDATE mcp_restore_acceptance_marker SET value='after-receipt'")
await executeMcpDatabaseRestore(operation.id, principal) await executeMcpDatabaseRestore(operation.id, principal)
@@ -165,6 +178,7 @@ try {
await delay(100) await delay(100)
} }
assert.equal(receipt.state, "blocked", JSON.stringify(receipt)) assert.equal(receipt.state, "blocked", JSON.stringify(receipt))
await waitForDatabaseRetirement()
assert.equal(await accessGeneration(), generationBeforeCorruption) assert.equal(await accessGeneration(), generationBeforeCorruption)
assert.equal((await pool.query("SELECT value FROM mcp_restore_acceptance_marker")).rows[0].value, "after-receipt") assert.equal((await pool.query("SELECT value FROM mcp_restore_acceptance_marker")).rows[0].value, "after-receipt")
console.log("✓ Tampered archive is blocked before data replacement") console.log("✓ Tampered archive is blocked before data replacement")
+15 -3
View File
@@ -5,7 +5,7 @@ import { z } from "zod"
import { pool } from "../../db/index.js" import { pool } from "../../db/index.js"
import { hasPermission, type AuthUser } from "../../lib/permissions.js" import { hasPermission, type AuthUser } from "../../lib/permissions.js"
import { validateSystemDatabaseRestoreArchive } from "../../services/system-database-restore-execution.js" import { validateSystemDatabaseRestoreArchive } from "../../services/system-database-restore-execution.js"
import { restoreSystemDatabaseBackup, assertSystemDatabaseRestoreTools } from "../../services/system-database-backup.js" import { restoreSystemDatabaseBackup, assertSystemDatabaseRestoreTools, withSystemDatabaseRestoreAdmission } from "../../services/system-database-backup.js"
import { withVerifiedConfigurationChange } from "../../services/config-mutation-policy.js" import { withVerifiedConfigurationChange } from "../../services/config-mutation-policy.js"
import { accessGeneration, decryptBinary, encryptBinary, decryptSecret, McpError, mcpStateDirectory } from "./security.js" import { accessGeneration, decryptBinary, encryptBinary, decryptSecret, McpError, mcpStateDirectory } from "./security.js"
import { audit, validatePrincipal, type McpPrincipal } from "./store.js" import { audit, validatePrincipal, type McpPrincipal } from "./store.js"
@@ -14,6 +14,7 @@ import type { add } from "./actions.js"
import type { ChangeEvidence } from "./action-types.js" import type { ChangeEvidence } from "./action-types.js"
import { withApplicationMutationLock } from "../../services/config-mutation-lock.js" import { withApplicationMutationLock } from "../../services/config-mutation-lock.js"
import {assertOriginalMcpTemporaryGrant,type OriginalServiceGrant} from "./temporary-grant-origin.js" import {assertOriginalMcpTemporaryGrant,type OriginalServiceGrant} from "./temporary-grant-origin.js"
import {reconcileRestoreReceiptAudit,inspectRestoreReceiptAudit} from "../../services/database-restore-audit.js"
export const MAX_RESTORE_ARCHIVE_BYTES = 64 * 1024 * 1024 export const MAX_RESTORE_ARCHIVE_BYTES = 64 * 1024 * 1024
const metadataSchema = z.object({ id: z.string().uuid(), ownerId: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/), sizeBytes: z.number().int().positive().max(MAX_RESTORE_ARCHIVE_BYTES), generation: z.string(), expiresAt: z.string().datetime(), createdAt: z.string().datetime() }).strict() const metadataSchema = z.object({ id: z.string().uuid(), ownerId: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/), sizeBytes: z.number().int().positive().max(MAX_RESTORE_ARCHIVE_BYTES), generation: z.string(), expiresAt: z.string().datetime(), createdAt: z.string().datetime() }).strict()
@@ -76,6 +77,10 @@ export async function readRestoreReceipt(id: string, actor: AuthUser) {
try { return JSON.parse(await readFile(filename(id, ".receipt.json"), "utf8")) as Record<string, unknown> } try { return JSON.parse(await readFile(filename(id, ".receipt.json"), "utf8")) as Record<string, unknown> }
catch { throw new McpError("NOT_FOUND", "Внешняя квитанция восстановления не найдена", 404) } catch { throw new McpError("NOT_FOUND", "Внешняя квитанция восстановления не найдена", 404) }
} }
export async function readRestoreReceiptAuditStatus(id:string,actor:AuthUser){
await readRestoreReceipt(id,actor)
return inspectRestoreReceiptAudit(pool,mcpStateDirectory(),id)
}
export function registerDatabaseRestore(register: typeof add) { export function registerDatabaseRestore(register: typeof add) {
register("database.restore", "mm_database_restore_plan", "Восстановить БД из загруженного человеком архива", "settings", "mm:settings:admin", "write", "global", z.object({ archiveRef: z.string().uuid(), expectedSha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict(), { register("database.restore", "mm_database_restore_plan", "Восстановить БД из загруженного человеком архива", "settings", "mm:settings:admin", "write", "global", z.object({ archiveRef: z.string().uuid(), expectedSha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict(), {
prepare: async (input, context) => { prepare: async (input, context) => {
@@ -91,6 +96,7 @@ export function registerDatabaseRestore(register: typeof add) {
} }
/** This terminal operation must never hold a database-request admission while asking for an exclusive restore. */ /** This terminal operation must never hold a database-request admission while asking for an exclusive restore. */
export async function executeMcpDatabaseRestore(operationId: string, principal: McpPrincipal,originalGrant?:OriginalServiceGrant) { export async function executeMcpDatabaseRestore(operationId: string, principal: McpPrincipal,originalGrant?:OriginalServiceGrant) {
return withSystemDatabaseRestoreAdmission(async () => {
const lease = await pool.connect() const lease = await pool.connect()
let locked = false, unsafe = false let locked = false, unsafe = false
try { try {
@@ -100,6 +106,7 @@ export async function executeMcpDatabaseRestore(operationId: string, principal:
if (locked) try { await lease.query("SELECT pg_advisory_unlock(hashtextextended($1,0))", ["mcp-operation:" + operationId]) } catch { unsafe = true } if (locked) try { await lease.query("SELECT pg_advisory_unlock(hashtextextended($1,0))", ["mcp-operation:" + operationId]) } catch { unsafe = true }
lease.release(unsafe) lease.release(unsafe)
} }
})
} }
async function executeRestore(operationId: string, principal: McpPrincipal,originalGrant?:OriginalServiceGrant) { async function executeRestore(operationId: string, principal: McpPrincipal,originalGrant?:OriginalServiceGrant) {
const { loadPlan } = await import("./runtime.js") const { loadPlan } = await import("./runtime.js")
@@ -110,7 +117,7 @@ async function executeRestore(operationId: string, principal: McpPrincipal,origi
let receipt: Record<string, unknown> = { operationId, archiveRef: payload.archiveRef, state: "running", startedAt: new Date().toISOString(), automaticRetry: false } let receipt: Record<string, unknown> = { operationId, archiveRef: payload.archiveRef, state: "running", startedAt: new Date().toISOString(), automaticRetry: false }
try { await durableWrite(filename(operationId, ".receipt.json"), JSON.stringify(receipt), true) } try { await durableWrite(filename(operationId, ".receipt.json"), JSON.stringify(receipt), true) }
catch (error) { if (error instanceof Error && "code" in error && error.code === "EEXIST") return; throw error } catch (error) { if (error instanceof Error && "code" in error && error.code === "EEXIST") return; throw error }
let attempted = false, stage = "authority" let attempted = false, stage = "authority", restoreActorId:string|undefined
const assertAuthority = async () => { const assertAuthority = async () => {
await assertOriginalMcpTemporaryGrant(pool,principal.connectionId,principal.credentialId,originalGrant) await assertOriginalMcpTemporaryGrant(pool,principal.connectionId,principal.credentialId,originalGrant)
const connection = await validatePrincipal(principal) const connection = await validatePrincipal(principal)
@@ -124,6 +131,7 @@ async function executeRestore(operationId: string, principal: McpPrincipal,origi
} }
try { try {
const { connection, ids } = await assertAuthority(), source = await archive(payload.archiveRef, connection.owner_id) const { connection, ids } = await assertAuthority(), source = await archive(payload.archiveRef, connection.owner_id)
restoreActorId=connection.owner_id
stage = "archive-read" stage = "archive-read"
if (source.sha256 !== payload.expectedSha256) throw new McpError("STATE_CONFLICT", "Архив подменён", 409) if (source.sha256 !== payload.expectedSha256) throw new McpError("STATE_CONFLICT", "Архив подменён", 409)
const buffer = await decryptBinary(await readFile(filename(source.id, ".archive.bin"))) const buffer = await decryptBinary(await readFile(filename(source.id, ".archive.bin")))
@@ -139,7 +147,6 @@ async function executeRestore(operationId: string, principal: McpPrincipal,origi
receipt = { ...receipt, state: "applied", finishedAt: new Date().toISOString(), accessRevoked: true, warnings: Object.keys(outcome).filter(key => key.endsWith("Error") && outcome[key as keyof typeof outcome]) } receipt = { ...receipt, state: "applied", finishedAt: new Date().toISOString(), accessRevoked: true, warnings: Object.keys(outcome).filter(key => key.endsWith("Error") && outcome[key as keyof typeof outcome]) }
stage = "receipt-publish" stage = "receipt-publish"
await durableWrite(filename(operationId, ".receipt.json"), JSON.stringify(receipt)) await durableWrite(filename(operationId, ".receipt.json"), JSON.stringify(receipt))
await audit(pool, connection.owner_id, "database.restore.receipt", null, receipt).catch(() => {})
} catch (error) { } catch (error) {
// The isolated destructive acceptance runner can print bounded diagnostics // The isolated destructive acceptance runner can print bounded diagnostics
// without archive SQL, server credentials, or arbitrary error text. // without archive SQL, server credentials, or arbitrary error text.
@@ -154,4 +161,9 @@ async function executeRestore(operationId: string, principal: McpPrincipal,origi
await durableWrite(filename(operationId, ".receipt.json"), JSON.stringify(receipt)) await durableWrite(filename(operationId, ".receipt.json"), JSON.stringify(receipt))
if (!attempted) await pool.query("UPDATE mcp_operations SET state='rejected',error_code=$2,finished_at=now() WHERE id=$1", [operationId, receipt.errorCode]).catch(() => {}) if (!attempted) await pool.query("UPDATE mcp_operations SET state='rejected',error_code=$2,finished_at=now() WHERE id=$1", [operationId, receipt.errorCode]).catch(() => {})
} }
// Audit failure is a separate pending/unconfirmed import AFTER durable
// applied receipt publication. It must never relabel confirmed restore.
if(receipt.state==="applied"){
try{await reconcileRestoreReceiptAudit(pool,mcpStateDirectory(),operationId,restoreActorId)}catch{/* Startup reconciles the existing external receipt; never replay restore. */}
}
} }
@@ -1,6 +1,9 @@
import assert from "node:assert/strict" import assert from "node:assert/strict"
import { createHash, randomUUID } from "node:crypto" import { createHash, randomUUID } from "node:crypto"
import { createDiagnosticCancellationSourceForTests, createReadServerProbeExecutionSourceForTests } from "./diagnostic-cancellation-source.js" import { mkdtemp, readdir, rm, rmdir } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { createDiagnosticCancellationSourceForTests, createReadServerProbeExecutionSourceForTests, readServerProbeExecutionSource } from "./diagnostic-cancellation-source.js"
import { fingerprint, redact } from "./security.js" import { fingerprint, redact } from "./security.js"
const proof = async (value: unknown) => createHash("sha256").update(fingerprint(value)).digest("hex") const proof = async (value: unknown) => createHash("sha256").update(fingerprint(value)).digest("hex")
@@ -25,6 +28,7 @@ function fixture() {
return { rows } return { rows }
} } } }
return { load: () => load(client, operationId, owner), read: () => read(snapshot, operationId, owner), readSql, return { load: () => load(client, operationId, owner), read: () => read(snapshot, operationId, owner), readSql,
readWithExistingKey: () => readServerProbeExecutionSource(snapshot, operationId, owner),
refresh, binding, input, evidence, row, sealed, setRows: (value: typeof rows) => { rows=value }, calls: () => calls, operationId, credentialId } refresh, binding, input, evidence, row, sealed, setRows: (value: typeof rows) => { rows=value }, calls: () => calls, operationId, credentialId }
} }
{ {
@@ -100,4 +104,22 @@ for (const mutate of mutations) {
assert.equal(source.originalContext.tool, "bandwidth"); assert.equal(source.stageBudget, 1) assert.equal(source.originalContext.tool, "bandwidth"); assert.equal(source.stageBudget, 1)
assert.equal(source.originalContext.identity.credentialId, f.credentialId) assert.equal(source.originalContext.identity.credentialId, f.credentialId)
} }
console.log(`diagnostic-cancellation-source: locked/read-only source, expired original contexts and ${mutations.length} shared tamper checks passed`) {
const state = await mkdtemp(join(tmpdir(), "mm-legacy-source-test-"))
const previousState = process.env.MCP_STATE_DIR
process.env.MCP_STATE_DIR = state
try {
const f = fixture()
await f.refresh()
f.row.evidence = { sealed: Buffer.alloc(32).toString("base64") }
await assert.rejects(f.readWithExistingKey, { code: "STATE_CONFLICT" })
assert.equal(f.readSql.length, 1)
assert.deepEqual(await readdir(state), [], "Historical source reads must leave missing encryption state untouched")
} finally {
if (previousState === undefined) delete process.env.MCP_STATE_DIR
else process.env.MCP_STATE_DIR = previousState
await rm(join(state, "encryption-key"), { force: true })
await rmdir(state)
}
}
console.log(`diagnostic-cancellation-source: locked/read-only source, missing key, expired original contexts and ${mutations.length} shared tamper checks passed`)
@@ -3,7 +3,7 @@ import type { ProbeStorageScope, ProbeStorageSql } from "../../services/server-p
import type { ServerProbeJournalContext } from "../../services/server-probe-journal.js" import type { ServerProbeJournalContext } from "../../services/server-probe-journal.js"
import { SERVER_PROBE_OPERATION } from "../../services/server-probe-authority.js" import { SERVER_PROBE_OPERATION } from "../../services/server-probe-authority.js"
import { openServerProbeMcpBinding } from "./server-probe-actions.js" import { openServerProbeMcpBinding } from "./server-probe-actions.js"
import { decryptSecret, fingerprint, privateSecretFingerprint, redact, McpError } from "./security.js" import { decryptSecret, decryptSecretExisting, fingerprint, privateFingerprinterExisting, privateSecretFingerprint, redact, McpError } from "./security.js"
const MAX_BYTES = 2097152 const MAX_BYTES = 2097152
const uuid = z.string().uuid() const uuid = z.string().uuid()
@@ -113,5 +113,14 @@ export const loadDiagnosticCancellationSource = sourceLoader(productionCrypto)
/** Test-only crypto seam. Never accept these dependencies from tool JSON. */ /** Test-only crypto seam. Never accept these dependencies from tool JSON. */
export const createDiagnosticCancellationSourceForTests = sourceLoader export const createDiagnosticCancellationSourceForTests = sourceLoader
/** Historical authentication only; confers no execution/cancellation grant. */ /** Historical authentication only; confers no execution/cancellation grant. */
export const readServerProbeExecutionSource = readSourceLoader(productionCrypto) export const readServerProbeExecutionSource = readSourceLoader({
open: decryptSecretExisting,
proof: async value => (await privateFingerprinterExisting("server-probe-mcp-binding-v1"))(fingerprint(value)),
})
export const createReadServerProbeExecutionSourceForTests = readSourceLoader export const createReadServerProbeExecutionSourceForTests = readSourceLoader
/** Exact previously locked source projection for private history conversion. */
export function authenticateServerProbeExecutionSourceRow(row: Record<string, unknown>, operationId: string,
owner: Readonly<DiagnosticCancellationOwner>) {
checkLookup(operationId, owner)
return validateSourceRow(row, operationId, owner, productionCrypto)
}
@@ -0,0 +1,73 @@
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { test } from "node:test"
import type { McpAction, ActionContext } from "./action-types.js"
import { registerGlobalReconciliationActionsForTests } from "./global-reconciliation-actions.js"
type Register = Parameters<typeof registerGlobalReconciliationActionsForTests>[0]
type Dependencies = Parameters<typeof registerGlobalReconciliationActionsForTests>[1]
function fixture() {
const connectionId = randomUUID(), operationId = randomUUID(), ownerId = "current-retired-owner"
const context = { principal: { connectionId }, connection: { id: connectionId, owner_id: ownerId, owner_permissions: ["mm:servers:write"],
policy: { permissions: ["mm:servers:write"], scope: { allowGlobal: true, operationIds: ["servers.poll", "operations.global.reconcile"], serverIds: [1, 2] } } } } as ActionContext
let action!: Pick<McpAction, "target" | "read">
let history: { affectedServerIds: number[] } | undefined = { affectedServerIds: [1, 2] }
let revoke = false, retiredCalls = 0, live = false
const deps = {
connection: async () => {
if (revoke) throw Error("revoked-current-grant")
return structuredClone(context.connection)
},
query: async (sql: string) => {
if (sql.startsWith("SELECT id FROM servers")) return [{ id: 1 }]
if (live) return [{ id: operationId, state: "applied", operation_id: "servers.poll", evidence: { sealed: "live" } }]
return []
},
operation: (id: string) => ({ id, scope: "global", requiredPermissions: ["mm:servers:write"] }),
service: () => ({ reconcile: async () => ({ matchesDesiredState: true, matchesOriginalState: false }) }),
decrypt: async () => JSON.stringify({ target: { scope: "global" }, objects: [{ serverId: 1 }], steps: [{ kind: "service", service: "servers.poll", input: {} }] }),
retired: async (identity, authorize) => {
retiredCalls++
assert.deepEqual(identity, { operationId, connectionId, ownerId })
await authorize()
return history
},
} satisfies Omit<Dependencies, "operation" | "service"> & Partial<Record<"operation" | "service", unknown>>
registerGlobalReconciliationActionsForTests(((...args: Parameters<Register>) => { action = args[8] }) as Register, deps as unknown as Dependencies)
return { context, operationId, history: (value: typeof history) => { history = value }, live: () => { live = true },
revoke: () => { revoke = true }, calls: () => retiredCalls, read: () => action.read!({ operationId }, context),
target: () => action.target({ operationId }, context) }
}
test("global action reconciles retired poll only under current ownership and returns no archived material", async () => {
const f = fixture()
assert.deepEqual(await f.target(), { scope: "global" })
const result = await f.read() as { observedAt: string }
const { observedAt, ...rest } = result
assert(Number.isFinite(Date.parse(observedAt)))
assert.deepEqual(rest, { operationId: f.operationId, state: "applied", automaticRetry: false,
results: [{ stepIndex: 0, service: "servers.poll", matchesDesiredState: true, matchesOriginalState: false }] })
assert.equal(f.calls(), 2)
})
test("global action refuses missing or inaccessible retired history", async () => {
const f = fixture()
f.history(undefined)
await assert.rejects(f.read(), { code: "NOT_FOUND" })
f.history({ affectedServerIds: [1, 2] })
f.context.connection.policy.scope.serverIds = [1]
await assert.rejects(f.read(), { code: "SERVER_DENIED" })
f.context.connection.policy.scope.serverIds = [1, 2]
f.context.connection.policy.scope.operationIds = ["operations.global.reconcile"]
await assert.rejects(f.read(), { code: "PERMISSION_DENIED" })
f.revoke()
await assert.rejects(f.read(), /revoked-current-grant/)
})
test("global action uses the live reconciliation path while original source exists", async () => {
const f = fixture()
f.live()
await f.read()
assert.equal(f.calls(), 0)
})
@@ -1,5 +1,6 @@
import { z } from "zod" import { z } from "zod"
import { pool } from "../../db/index.js" import { pool } from "../../db/index.js"
import { readRetiredServerPollHistory } from "../../services/server-poll-history-reader.js"
import { hasPermission } from "../../lib/permissions.js" import { hasPermission } from "../../lib/permissions.js"
import { effectiveMcpConnection } from "./store.js" import { effectiveMcpConnection } from "./store.js"
import { decryptSecret, fingerprint, McpError } from "./security.js" import { decryptSecret, fingerprint, McpError } from "./security.js"
@@ -7,40 +8,84 @@ import { findMcpOperation } from "./registry.js"
import { findMcpService } from "./service-registry.js" import { findMcpService } from "./service-registry.js"
import type { add } from "./actions.js" import type { add } from "./actions.js"
import type { ActionContext, ChangeEvidence } from "./action-types.js" import type { ActionContext, ChangeEvidence } from "./action-types.js"
import type { ConnectionRow } from "./store.js"
async function authorizedSource(operationId: string, context: ActionContext) { const production = {
connection: effectiveMcpConnection,
query: async (sql: string, values: unknown[] = []) => (await pool.query(sql, values)).rows,
operation: findMcpOperation,
service: findMcpService,
decrypt: decryptSecret,
retired: readRetiredServerPollHistory,
}
type Dependencies = typeof production
const notFound = () => new McpError("NOT_FOUND", "Операция не найдена", 404)
async function currentAccess(context: ActionContext, deps: Dependencies) {
context.signal?.throwIfAborted() context.signal?.throwIfAborted()
const current = await effectiveMcpConnection(context.principal) const current = await deps.connection(context.principal)
if (!current.policy.scope.allowGlobal || fingerprint(current.policy) !== fingerprint(context.connection.policy) || fingerprint(current.owner_permissions) !== fingerprint(context.connection.owner_permissions)) throw new McpError("PERMISSION_DENIED", "Глобальный допуск сверки изменился", 403) if (!current.policy.scope.allowGlobal || fingerprint(current.policy) !== fingerprint(context.connection.policy) || fingerprint(current.owner_permissions) !== fingerprint(context.connection.owner_permissions)) throw new McpError("PERMISSION_DENIED", "Глобальный допуск сверки изменился", 403)
const ids = (await pool.query("SELECT id FROM servers ORDER BY id LIMIT 1001")).rows.map(row => Number(row.id)) const ids = (await deps.query("SELECT id FROM servers ORDER BY id LIMIT 1001")).map(row => Number(row.id))
if (ids.length > 1000) throw new McpError("LIMIT_EXCEEDED", "Каталог глобальной сверки превышает лимит", 409) if (ids.length > 1000) throw new McpError("LIMIT_EXCEEDED", "Каталог глобальной сверки превышает лимит", 409)
if (ids.some(id => !current.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Глобальная сверка требует доступа ко всему каталогу серверов", 403) if (ids.some(id => !current.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Глобальная сверка требует доступа ко всему каталогу серверов", 403)
const row = (await pool.query("SELECT o.id,o.state,p.operation_id,p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id WHERE o.id=$1 AND o.connection_id=$2", [operationId, current.id])).rows[0] return current
if (!row) throw new McpError("NOT_FOUND", "Операция не найдена", 404) }
const original = findMcpOperation(row.operation_id)
function requireOriginal(actionId: string, current: ConnectionRow, deps: Dependencies) {
const original = deps.operation(actionId)
if (!original || original.scope !== "global" || !current.policy.scope.operationIds.includes(original.id) || original.requiredPermissions.some(permission => !hasPermission(current.policy.permissions, permission) || !hasPermission(current.owner_permissions, permission))) throw new McpError("PERMISSION_DENIED", "Утрачено право на исходную глобальную операцию", 403) if (!original || original.scope !== "global" || !current.policy.scope.operationIds.includes(original.id) || original.requiredPermissions.some(permission => !hasPermission(current.policy.permissions, permission) || !hasPermission(current.owner_permissions, permission))) throw new McpError("PERMISSION_DENIED", "Утрачено право на исходную глобальную операцию", 403)
const evidence = JSON.parse(await decryptSecret(row.evidence.sealed)) as ChangeEvidence }
if (evidence.target.scope !== "global" || !evidence.steps.length || evidence.steps.some(step => step.kind !== "service" || !findMcpService(step.service)?.reconcile)) throw new McpError("UNSUPPORTED_CAPABILITY", "Для глобальной операции нет безопасной read-only сверки", 409)
const declared = evidence.objects.flatMap(object => object.serverId === undefined ? [] : [object.serverId]) function requireServerScope(declared: number[], current: ConnectionRow) {
if (declared.some(id => !current.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Зависимость операции вне текущего глобального допуска", 403) if (declared.some(id => !current.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Зависимость операции вне текущего глобального допуска", 403)
}
async function authorizedSource(operationId: string, context: ActionContext, deps: Dependencies) {
const current = await currentAccess(context, deps)
const row = (await deps.query("SELECT o.id,o.state,p.operation_id,p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id WHERE o.id=$1 AND o.connection_id=$2", [operationId, current.id]))[0]
if (!row) {
const authorize = async () => {
const fresh = await currentAccess(context, deps)
if (fresh.id !== current.id || fresh.owner_id !== current.owner_id) throw notFound()
}
const archived = await deps.retired({ operationId, connectionId: current.id, ownerId: current.owner_id }, authorize,
{ deadlineAt: Date.now() + 10000, signal: context.signal })
context.signal?.throwIfAborted()
if (!archived) throw notFound()
requireOriginal("servers.poll", current, deps)
requireServerScope(archived.affectedServerIds, current)
return { kind: "retired-poll" as const }
}
requireOriginal(row.operation_id, current, deps)
const evidence = JSON.parse(await deps.decrypt(row.evidence.sealed)) as ChangeEvidence
if (evidence.target.scope !== "global" || !evidence.steps.length || evidence.steps.some(step => step.kind !== "service" || !deps.service(step.service)?.reconcile)) throw new McpError("UNSUPPORTED_CAPABILITY", "Для глобальной операции нет безопасной read-only сверки", 409)
requireServerScope(evidence.objects.flatMap(object => object.serverId === undefined ? [] : [object.serverId]), current)
if (["pending", "running"].includes(row.state)) throw new McpError("STATE_CONFLICT", "Операция ещё выполняется", 409) if (["pending", "running"].includes(row.state)) throw new McpError("STATE_CONFLICT", "Операция ещё выполняется", 409)
context.signal?.throwIfAborted() context.signal?.throwIfAborted()
return { row, evidence } return { kind: "live" as const, row, evidence }
} }
export function registerGlobalReconciliationActions(register: typeof add) {
function registerActions(register: typeof add, deps: Dependencies) {
register("operations.global.reconcile", "mm_global_operation_reconcile", "Сверить глобальную сервисную операцию без повторной записи", "mcp", null, "read", "global", z.object({ operationId: z.string().uuid() }).strict(), { register("operations.global.reconcile", "mm_global_operation_reconcile", "Сверить глобальную сервисную операцию без повторной записи", "mcp", null, "read", "global", z.object({ operationId: z.string().uuid() }).strict(), {
target: async (input, context) => { await authorizedSource(String(input.operationId), context); return { scope: "global" } }, target: async (input, context) => { await authorizedSource(String(input.operationId), context, deps); return { scope: "global" } },
read: async (input, context) => { read: async (input, context) => {
const { row, evidence } = await authorizedSource(String(input.operationId), context) const operationId = String(input.operationId)
const authorize = async () => { await authorizedSource(String(input.operationId), context) } const source = await authorizedSource(operationId, context, deps)
if (source.kind === "retired-poll") return { operationId, state: "applied", observedAt: new Date().toISOString(),
results: [{ stepIndex: 0, service: "servers.poll", matchesDesiredState: true, matchesOriginalState: false }], automaticRetry: false }
const { row, evidence } = source
const authorize = async () => { if ((await authorizedSource(operationId, context, deps)).kind !== "live") throw notFound() }
const results = [] const results = []
for (const [index, step] of evidence.steps.entries()) { for (const [index, step] of evidence.steps.entries()) {
if (step.kind !== "service") throw new McpError("UNSUPPORTED_CAPABILITY", "Нативная глобальная сверка недоступна", 409) if (step.kind !== "service") throw new McpError("UNSUPPORTED_CAPABILITY", "Нативная глобальная сверка недоступна", 409)
await authorize() await authorize()
results.push({ stepIndex: index, service: step.service, ...await findMcpService(step.service)!.reconcile!(step.input, authorize) }) results.push({ stepIndex: index, service: step.service, ...await deps.service(step.service)!.reconcile!(step.input, authorize) })
} }
await authorize() await authorize()
return { operationId: row.id, state: row.state, observedAt: new Date().toISOString(), results, automaticRetry: false } return { operationId: row.id, state: row.state, observedAt: new Date().toISOString(), results, automaticRetry: false }
}, },
}) })
} }
export const registerGlobalReconciliationActions = (register: typeof add) => registerActions(register, production)
export const registerGlobalReconciliationActionsForTests = registerActions
+72 -1
View File
@@ -2889,6 +2889,53 @@ try {
assert.ok(!result.isError) assert.ok(!result.isError)
console.log("✓ Official SDK HTTP initialize, filtered tools/list and tools/call") console.log("✓ Official SDK HTTP initialize, filtered tools/list and tools/call")
} finally { await client.close() } } finally { await client.close() }
{
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({
...input, name: "Logs follow maintenance", permissions: ["mm:servers:read"],
scope: { serverIds: [1], operationIds: ["logs.follow"], allowGlobal: false },
}), actor)
const follower = new Client({ name: "logs-follow-maintenance", version: "1.0.0" })
const { sharedDatabaseGate } = await import("../../services/shared-database-gate.js")
const original = pool.query
let arrived!: () => void, release!: () => void
const reading = new Promise<void>(resolve => { arrived = resolve })
const held = new Promise<void>(resolve => { release = resolve })
try {
await follower.connect(new StreamableHTTPClientTransport(new URL(endpoint), { requestInit: { headers: { Authorization: `Bearer ${access.token}` } } }))
pool.query = (async (...args: Parameters<typeof original>) => {
const text = typeof args[0] === "string" ? args[0] : args[0]?.text
const result = await Reflect.apply(original, pool, args)
if (typeof text === "string" && text.startsWith("SELECT GREATEST(COALESCE(max(id),0)")) {
arrived()
await held
}
return result
}) as typeof original
const following = follower.callTool({ name: "mm_logs_follow", arguments: { serverId: 1, waitSeconds: 30 } })
try {
const observation = new AbortController()
try {
await Promise.race([reading, following.then(() => { throw new Error("Follow finished before its stream query") }),
delay(5000, undefined, { signal: observation.signal }).then(() => { throw new Error("Follow stream query was not observed") })])
} finally { observation.abort() }
let exclusiveEntered = false
const maintenance = sharedDatabaseGate.exclusive(async () => { exclusiveEntered = true }, 5000)
try {
await delay(25)
assert.equal(exclusiveEntered, false, "maintenance must wait for the pending database read")
} finally {
release()
await maintenance
}
const result = await following
assert.equal(result.isError, true)
assert.match(JSON.stringify(result), /RESET_REQUIRED/)
assert.doesNotMatch(JSON.stringify(result), /"cursor"/)
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_read_leases WHERE connection_id=$1", [access.connection.id])).rows[0].count, 0)
} finally { release(); await following.catch(() => undefined) }
} finally { pool.query = original; await follower.close() }
}
await (await import("./logs-follow-sdk-cancellation-acceptance.js")).verifyLogsFollowSdkCancellation(app, endpoint, actor)
const stdio = new Client({ name: "stdio-integration", version: "1.0.0" }) const stdio = new Client({ name: "stdio-integration", version: "1.0.0" })
try { try {
await stdio.connect(new StdioClientTransport({ command: process.execPath, args: ["--import", "tsx", fileURLToPath(new URL("../../scripts/mcp-stdio.ts", import.meta.url))], env: { MCP_URL: endpoint, MCP_TOKEN: rotated.token }, stderr: "pipe" })) await stdio.connect(new StdioClientTransport({ command: process.execPath, args: ["--import", "tsx", fileURLToPath(new URL("../../scripts/mcp-stdio.ts", import.meta.url))], env: { MCP_URL: endpoint, MCP_TOKEN: rotated.token }, stderr: "pipe" }))
@@ -2907,17 +2954,41 @@ try {
await (await import("../../test/mcp-host-files.js")).verifyMcpHostFiles(actor,app) await (await import("../../test/mcp-host-files.js")).verifyMcpHostFiles(actor,app)
const readinessAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Actual readiness handshake", scope: { ...input.scope, operationIds: ["servers.list"] } }), actor) const readinessAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Actual readiness handshake", scope: { ...input.scope, operationIds: ["servers.list"] } }), actor)
const readinessHeaders = { authorization: "Bearer " + jwt } const readinessHeaders = { authorization: "Bearer " + jwt }
const simulatorHeaders = { authorization: "Bearer " + await new SignJWT({ sub: "policy-preview-admin", apps: ["mm"], permissions: ["mm:mcp:admin"] })
.setProtectedHeader({ alg: "HS256" }).setIssuer(env.authIssuer).setExpirationTime("1h").sign(new TextEncoder().encode(env.jwtSecret)) }
const policyPreview = (payload: Record<string, unknown>) => app!.inject({ method: "POST", url: "/api/mcp/policy/preview", headers: simulatorHeaders, payload })
const simulatedServerRead = { connectionId: readinessAccess.connection.id, operationId: "servers.list", target: { scope: "servers", serverIds: [1] } }
const simulation = await policyPreview(simulatedServerRead)
assert.equal(simulation.statusCode, 200, simulation.body)
assert.equal(simulation.json().result.decision, "allowed", "an administrator simulates the stored owner's rights, not the administrator's rights")
assert.equal(simulation.json().executionAvailable, false)
assert.equal((await policyPreview({ ...simulatedServerRead, ownerPermissions: ["mm:settings:admin"] })).statusCode, 400)
const ready = await app.inject({ method: "POST", url: `/api/mcp/connections/${readinessAccess.connection.id}/check`, headers: readinessHeaders, payload: {} }) const ready = await app.inject({ method: "POST", url: `/api/mcp/connections/${readinessAccess.connection.id}/check`, headers: readinessHeaders, payload: {} })
assert.equal(ready.statusCode, 200, ready.body) assert.equal(ready.statusCode, 200, ready.body)
assert.equal(ready.json().ok, true, ready.body); assert.equal(ready.json().checks.protocolHandshake, true) assert.equal(ready.json().ok, true, ready.body); assert.equal(ready.json().checks.protocolHandshake, true)
assert.equal(ready.json().protocolProbe.status, "passed"); assert.equal(ready.json().protocolProbe.clientCredentialsVerified, false) assert.equal(ready.json().protocolProbe.status, "passed"); assert.equal(ready.json().protocolProbe.clientCredentialsVerified, false)
assert.equal(ready.json().protocolProbe.capabilitiesRead, "skipped")
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_credentials WHERE connection_id=$1 AND client_id='mcp-readiness-probe'", [readinessAccess.connection.id])).rows[0].n, 0) assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_credentials WHERE connection_id=$1 AND client_id='mcp-readiness-probe'", [readinessAccess.connection.id])).rows[0].n, 0)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_grants WHERE connection_id=$1", [readinessAccess.connection.id])).rows[0].n, 0) assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_grants WHERE connection_id=$1", [readinessAccess.connection.id])).rows[0].n, 0)
const duplicateReady = await app.inject({ method: "POST", url: `/api/mcp/connections/${readinessAccess.connection.id}/check`, headers: readinessHeaders, payload: {} }) const duplicateReady = await app.inject({ method: "POST", url: `/api/mcp/connections/${readinessAccess.connection.id}/check`, headers: readinessHeaders, payload: {} })
assert.equal(duplicateReady.statusCode, 429) assert.equal(duplicateReady.statusCode, 429)
await setConnectionStatus(readinessAccess.connection.id, "paused", actor) await setConnectionStatus(readinessAccess.connection.id, "paused", actor)
const pausedSimulation = await policyPreview(simulatedServerRead)
assert.equal(pausedSimulation.statusCode, 200, pausedSimulation.body)
assert.equal(pausedSimulation.json().result.decision, "denied")
assert.ok(pausedSimulation.json().result.reasons.some((reason: { code: string }) => reason.code === "CONNECTION_INACTIVE"))
const pausedReady = await app.inject({ method: "POST", url: `/api/mcp/connections/${readinessAccess.connection.id}/check`, headers: readinessHeaders, payload: {} }) const pausedReady = await app.inject({ method: "POST", url: `/api/mcp/connections/${readinessAccess.connection.id}/check`, headers: readinessHeaders, payload: {} })
assert.equal(pausedReady.statusCode, 200); assert.equal(pausedReady.json().protocolProbe.status, "skipped") assert.equal(pausedReady.statusCode, 200); assert.equal(pausedReady.json().protocolProbe.status, "skipped")
const readableAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Actual readiness capability read", scope: { ...input.scope, operationIds: ["servers.list", "capabilities.read"] } }), actor)
const readable = await app.inject({ method: "POST", url: `/api/mcp/connections/${readableAccess.connection.id}/check`, headers: readinessHeaders, payload: {} })
assert.equal(readable.statusCode, 200, readable.body)
assert.equal(readable.json().ok, true, readable.body)
assert.equal(readable.json().protocolProbe.capabilitiesRead, "passed")
assert.equal(readable.json().protocolProbe.credentialType, "temporary-capabilities-read")
const readinessReads = (await pool.query("SELECT tool FROM mcp_requests WHERE connection_id=$1 AND tool NOT LIKE 'protocol:%'", [readableAccess.connection.id])).rows
assert.deepEqual(readinessReads.map(row => row.tool), ["mm_capabilities_get"])
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_credentials WHERE connection_id=$1 AND client_id='mcp-readiness-probe'", [readableAccess.connection.id])).rows[0].n, 0)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_grants WHERE connection_id=$1", [readableAccess.connection.id])).rows[0].n, 0)
const emptyGrantAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Readiness has no operation authority" }), actor) const emptyGrantAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Readiness has no operation authority" }), actor)
const { withMcpProbeCredential } = await import("./readiness.js") const { withMcpProbeCredential } = await import("./readiness.js")
let endedProbeToken = "" let endedProbeToken = ""
@@ -2933,7 +3004,7 @@ try {
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_grants WHERE connection_id=$1", [emptyGrantAccess.connection.id])).rows[0].n, 0) assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_grants WHERE connection_id=$1", [emptyGrantAccess.connection.id])).rows[0].n, 0)
const readinessAudit = (await pool.query("SELECT payload FROM mcp_audit WHERE connection_id=$1", [emptyGrantAccess.connection.id])).rows const readinessAudit = (await pool.query("SELECT payload FROM mcp_audit WHERE connection_id=$1", [emptyGrantAccess.connection.id])).rows
assert.ok(!JSON.stringify(readinessAudit).includes(endedProbeToken)) assert.ok(!JSON.stringify(readinessAudit).includes(endedProbeToken))
console.log("✓ Actual readiness initialize/tools-list over public HTTP: temporary empty grant, no tool authority, cleanup on success/error, cooldown and paused precheck skip") console.log("✓ Actual readiness initialize/tools-list and optional capabilities read over public HTTP: narrow grant, cleanup on success/error, cooldown and paused precheck skip")
const deviceConnection = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Web interfaces", scope: { ...input.scope, operationIds: ["ros.bridge.list", "ros.bridge.create"] } }), actor) const deviceConnection = await createMcpConnection(mcpConnectionCreateSchema.parse({ ...input, name: "Web interfaces", scope: { ...input.scope, operationIds: ["ros.bridge.list", "ros.bridge.create"] } }), actor)
const deviceHeaders = { authorization: "Bearer " + jwt } const deviceHeaders = { authorization: "Bearer " + jwt }
const deviceCatalog = await app.inject({ url: "/api/mcp/devices/catalog?connectionId=" + deviceConnection.connection.id, headers: deviceHeaders }) const deviceCatalog = await app.inject({ url: "/api/mcp/devices/catalog?connectionId=" + deviceConnection.connection.id, headers: deviceHeaders })
@@ -0,0 +1,6 @@
import { McpError } from "./security.js"
export function logsFollowCancellation(maintenance?: AbortSignal, caller?: AbortSignal): McpError | undefined {
if (maintenance?.aborted) return maintenance.reason
if (caller?.aborted) return new McpError("READ_CANCELLED", "Ожидание новых событий отменено", 409)
}
@@ -0,0 +1,143 @@
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import type { IncomingMessage, ServerResponse } from "node:http"
import { setTimeout as delay } from "node:timers/promises"
import type { FastifyInstance } from "fastify"
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"
import { mcpConnectionCreateSchema } from "@mmapp/contracts/mcp"
import { pool } from "../../db/index.js"
import type { AuthUser } from "../../lib/permissions.js"
import { createMcpConnection } from "./store.js"
export async function verifyLogsFollowSdkCancellation(app: FastifyInstance, endpoint: string, actor: AuthUser) {
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({
name: "Logs follow SDK cancellation", mode: "approval", permissions: ["mm:servers:read"],
expiresAt: new Date(Math.min(actor.expiresAt ?? Infinity, Date.now() + 600_000)).toISOString(),
scope: { serverIds: [1], operationIds: ["logs.follow", "logs.query"], allowGlobal: false },
}), actor)
const client = new Client({ name: "logs-follow-sdk-cancellation", version: "1.0.0" }, {
versionNegotiation: { mode: { pin: "2026-07-28" } },
})
const caller = new AbortController(), bound = new AbortController()
const deadline = delay(5000, undefined, { signal: bound.signal }).then(() => { throw new Error("SDK cancellation fixture deadline") })
void deadline.catch(() => undefined)
let entered!: () => void, release!: () => void, closed!: () => void
const enteredRead = new Promise<void>(resolve => { entered = resolve })
const holdRead = new Promise<void>(resolve => { release = resolve })
const responseClosed = new Promise<void>(resolve => { closed = resolve })
let readCount = 0
const originalQuery = pool.query
const observeClose = (request: IncomingMessage, response: ServerResponse) => {
if (request.method === "POST" && request.url === "/mcp") {
response.once("close", () => { if (!response.writableFinished) closed() })
}
}
try {
await client.connect(new StreamableHTTPClientTransport(new URL(endpoint), {
requestInit: { headers: { Authorization: `Bearer ${access.token}` } },
}))
app.server.on("request", observeClose)
pool.query = (async (...args: unknown[]) => {
const query = args[0]
const sql = typeof query === "object" && query !== null && "text" in query ? query.text : query
const result = await Reflect.apply(originalQuery, pool, args)
if (typeof sql === "string" && sql.startsWith("SELECT GREATEST(COALESCE(max(id),0)")) {
readCount++
entered()
await holdRead
}
return result
}) as typeof originalQuery
const follow = client.callTool({ name: "mm_logs_follow", arguments: { serverId: 1, waitSeconds: 30 } }, { signal: caller.signal })
const refused = assert.rejects(follow)
try {
await Promise.race([enteredRead, deadline])
caller.abort()
await Promise.race([Promise.all([refused, responseClosed]), deadline])
const live = await pool.query("SELECT count(*)::int AS count FROM mcp_read_leases WHERE connection_id=$1", [access.connection.id])
assert.equal(live.rows[0].count, 1, "caller cancellation must wait for the pending read before releasing the lease")
release()
for (;;) {
const { rows: [audit] } = await pool.query(
"SELECT status,error_code,result FROM mcp_requests WHERE connection_id=$1 AND tool='mm_logs_follow'", [access.connection.id],
)
const protocol = await pool.query("SELECT count(*)::int AS count FROM mcp_requests WHERE connection_id=$1 AND tool='protocol:tools/call' AND finished_at IS NOT NULL", [access.connection.id])
if (audit && audit.status !== "running" && protocol.rows[0].count === 1) {
assert.equal(audit.status, "error")
assert.equal(audit.error_code, "READ_CANCELLED")
assert.doesNotMatch(JSON.stringify(audit.result), /"cursor"/)
break
}
await Promise.race([delay(25), deadline])
}
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_read_leases WHERE connection_id=$1", [access.connection.id])).rows[0].count, 0)
assert.equal(readCount, 1, "cancelled follow does not dispatch a second read")
} finally {
caller.abort()
release()
await refused.catch(() => undefined)
}
pool.query = originalQuery
const from = "2000-01-01T00:00:00Z", to = "2000-01-01T00:30:00Z"
const followOnce = (filters: { from?: string; cursor?: string }) => client.callTool({
name: "mm_logs_follow", arguments: { serverId: 1, waitSeconds: 0, to, ...filters },
})
const initial = await followOnce({ from })
assert.ok(!initial.isError)
const cursor = (initial.structuredContent as { result?: { cursor?: unknown } } | undefined)?.result?.cursor
assert.ok(typeof cursor === "string" && cursor.length > 0)
assert.ok(!(await followOnce({ from, cursor })).isError)
assert.ok(!(await followOnce({ cursor })).isError, "omitting from must resume the historical cursor's period")
const changed = await followOnce({ from: "2000-01-01T00:01:00Z", cursor })
assert.equal(changed.isError, true)
assert.match(JSON.stringify(changed), /RESET_REQUIRED/)
assert.doesNotMatch(JSON.stringify(changed), /"cursor"/)
console.log("✓ MCP logs follow: historical cursor resumes, changed from requires reset")
const marker = `mcp-logs-cursor-${randomUUID()}`
const readFollowPage = async (options: { cursor?: string; limit?: number }) => {
const result = await client.callTool({ name: "mm_logs_follow", arguments: { serverId: 1, from, to, q: marker, waitSeconds: 0, ...options } })
assert.ok(!result.isError)
return (result.structuredContent as { result: { entries: { id: string }[]; cursor: string } }).result
}
const start = await readFollowPage({})
assert.deepEqual(start.entries, [])
try {
await pool.query(`INSERT INTO router_log_entries(server_id,server_name,host,router_id,raw_time,received_at,effective_at,topics,level,message,generation)
SELECT 1,'cursor fixture','127.0.0.1','fixture','00:02:00',now(),$1::timestamptz+n*interval '2 minutes',ARRAY['system'],'info',$2,0
FROM generate_series(1,2) AS n`, [from, marker])
const defaultFollow = await readFollowPage({ cursor: start.cursor })
assert.equal(defaultFollow.entries.length, 2)
const firstFollow = await readFollowPage({ cursor: start.cursor, limit: 1 })
const secondFollow = await readFollowPage({ cursor: firstFollow.cursor, limit: 1 })
assert.equal(firstFollow.entries.length, 1)
assert.equal(secondFollow.entries.length, 1)
assert.deepEqual([...firstFollow.entries, ...secondFollow.entries].map(row => row.id), defaultFollow.entries.map(row => row.id))
assert.deepEqual((await readFollowPage({ cursor: secondFollow.cursor, limit: 1 })).entries, [])
console.log("✓ MCP logs follow: requested batch limit and lossless cursor progression")
const query = (filters: { from?: string; cursor?: string }) => client.callTool({
name: "mm_logs_query", arguments: { serverId: 1, q: marker, limit: 1, to, ...filters },
})
type Page = { entries: { id: string }[]; nextCursor: string | null }
const first = await query({ from })
assert.ok(!first.isError)
const firstPage = (first.structuredContent as { result: Page }).result
assert.equal(firstPage.entries.length, 1)
assert.ok(firstPage.nextCursor)
const next = await query({ cursor: firstPage.nextCursor })
assert.ok(!next.isError)
const nextPage = (next.structuredContent as { result: Page }).result
assert.equal(nextPage.entries.length, 1)
assert.notEqual(nextPage.entries[0].id, firstPage.entries[0].id)
assert.equal(nextPage.nextCursor, null)
console.log("✓ MCP logs query: historical pagination resumes without resending from")
} finally {
await pool.query("DELETE FROM router_log_entries WHERE server_id=1 AND message=$1", [marker])
}
} finally {
bound.abort()
pool.query = originalQuery
app.server.off("request", observeClose)
await client.close()
}
}
+12 -2
View File
@@ -41,7 +41,7 @@ export async function registerManagementChannel(raw: unknown, actor: AuthUser) {
}) })
return { id, serverId: input.serverId, expiresAt: input.expiresAt, verification: "identity-and-reachability", independence: "human-attestation" } return { id, serverId: input.serverId, expiresAt: input.expiresAt, verification: "identity-and-reachability", independence: "human-attestation" }
} }
export async function verifyManagementChannel(serverId: number, requiredId?: string) { async function verifiedManagementChannel(serverId: number, requiredId?: string) {
const { rows: [row] } = await pool.query("SELECT * FROM mcp_management_channels WHERE server_id=$1 AND revoked_at IS NULL AND expires_at>now()", [serverId]) const { rows: [row] } = await pool.query("SELECT * FROM mcp_management_channels WHERE server_id=$1 AND revoked_at IS NULL AND expires_at>now()", [serverId])
if (!row || requiredId && row.id !== requiredId || row.generation !== await accessGeneration()) throw new McpError("APPROVAL_REQUIRED", "Нет актуального проверенного независимого канала управления", 409) if (!row || requiredId && row.id !== requiredId || row.generation !== await accessGeneration()) throw new McpError("APPROVAL_REQUIRED", "Нет актуального проверенного независимого канала управления", 409)
const config = JSON.parse(await decryptSecret(row.config_sealed)) as MikrotikConnectParams const config = JSON.parse(await decryptSecret(row.config_sealed)) as MikrotikConnectParams
@@ -50,7 +50,17 @@ export async function verifyManagementChannel(serverId: number, requiredId?: str
if (hashes.some(value => value !== row.identity_hash)) throw new McpError("STATE_CONFLICT", "Идентичность основного или management-канала изменилась", 409) if (hashes.some(value => value !== row.identity_hash)) throw new McpError("STATE_CONFLICT", "Идентичность основного или management-канала изменилась", 409)
const changed = await pool.query("UPDATE mcp_management_channels SET verified_at=now() WHERE id=$1 AND revoked_at IS NULL AND expires_at>now() RETURNING id", [row.id]) const changed = await pool.query("UPDATE mcp_management_channels SET verified_at=now() WHERE id=$1 AND revoked_at IS NULL AND expires_at>now() RETURNING id", [row.id])
if (!changed.rowCount) throw new McpError("APPROVAL_REQUIRED", "Допуск management-канала был отозван во время проверки", 409) if (!changed.rowCount) throw new McpError("APPROVAL_REQUIRED", "Допуск management-канала был отозван во время проверки", 409)
return { channelId: row.id as string, serverId, expiresAt: row.expires_at.toISOString() as string } return { channelId: row.id as string, serverId, expiresAt: row.expires_at.toISOString() as string, host: config.host }
}
export async function verifyManagementChannel(serverId: number, requiredId?: string) {
const { host: _host, ...receipt } = await verifiedManagementChannel(serverId, requiredId)
return receipt
}
/** Internal VLAN preparation uses the verified endpoint to trace its actual
* address/interface. It is not accepted from tool input or returned in the
* public channel receipt. */
export async function verifyBridgeManagementChannel(serverId: number, requiredId?: string) {
return verifiedManagementChannel(serverId, requiredId)
} }
export async function managementEvidence(serverIds: number[]) { export async function managementEvidence(serverIds: number[]) {
try { return { requiresHuman: false, managementChannels: await Promise.all([...new Set(serverIds)].map(serverId => verifyManagementChannel(serverId))) } } try { return { requiresHuman: false, managementChannels: await Promise.all([...new Set(serverIds)].map(serverId => verifyManagementChannel(serverId))) } }
@@ -11,7 +11,7 @@ const creates = rosResources.filter(resource => !resource.readOnly && resource.c
assert.equal(creates.length, 30) assert.equal(creates.length, 30)
for (const resource of creates) { for (const resource of creates) {
const action = findAction(`mm_${resource.id}_create_plan`) const action = findAction(`mm_${resource.id}_create_plan`)
assert.equal(action.operation.version, ["ospf_template", "ipsec_peer"].includes(resource.id) ? 5 : ["wireguard", "vrf"].includes(resource.id) ? 4 : 3) assert.equal(action.operation.version, ["ospf_template", "ipsec_peer", "vrf"].includes(resource.id) ? 5 : ["wireguard", "bridge", "bridge_port", "bridge_vlan"].includes(resource.id) ? 4 : 3)
assert.equal(action.schema.safeParse({ serverId: 1, fields: {} }).success, true) assert.equal(action.schema.safeParse({ serverId: 1, fields: {} }).success, true)
for (const id of ["*OLD", undefined, null, ""]) { for (const id of ["*OLD", undefined, null, ""]) {
const rejected = action.schema.safeParse({ serverId: 1, fields: {}, id }) const rejected = action.schema.safeParse({ serverId: 1, fields: {}, id })
@@ -5,6 +5,8 @@ await prepareOwnedBandwidthState()
const { pool, closePool } = await import("../../db/index.js") const { pool, closePool } = await import("../../db/index.js")
try { try {
const { applySqlMigrations } = await import("../../db/migrate.js"); await applySqlMigrations(pool) const { applySqlMigrations } = await import("../../db/migrate.js"); await applySqlMigrations(pool)
const { acceptOperationReplayTombstones } = await import("./operation-replay-tombstones.sql-helper.js")
await acceptOperationReplayTombstones()
const { acceptOwnedMcpOperationRoot } = await import("./operation-execution-origin.sql-helper.js") const { acceptOwnedMcpOperationRoot } = await import("./operation-execution-origin.sql-helper.js")
console.log("Owned actual MCP operation root:", JSON.stringify(await acceptOwnedMcpOperationRoot())) console.log("Owned actual MCP operation root:", JSON.stringify(await acceptOwnedMcpOperationRoot()))
} finally { await closePool() } } finally { await closePool() }
@@ -9,6 +9,7 @@ import { performance } from "node:perf_hooks"
import type { QueryConfig, PoolClient } from "pg" import type { QueryConfig, PoolClient } from "pg"
import { mcpTemporaryGrantSchema, type McpObjectRef } from "@mmapp/contracts/mcp" import { mcpTemporaryGrantSchema, type McpObjectRef } from "@mmapp/contracts/mcp"
import { pool } from "../../db/index.js" import { pool } from "../../db/index.js"
import { sharedDatabaseGate } from "../../services/shared-database-gate.js"
import { assertNoConfigurationLockContext } from "../../services/config-mutation-lock.js" import { assertNoConfigurationLockContext } from "../../services/config-mutation-lock.js"
import { assertNoOperationConfigurationLockContext } from "../../services/operation-config-lock-lease.js" import { assertNoOperationConfigurationLockContext } from "../../services/operation-config-lock-lease.js"
import { validatePrincipal, audit, type McpPrincipal, type StoredOperation } from "./store.js" import { validatePrincipal, audit, type McpPrincipal, type StoredOperation } from "./store.js"
@@ -200,7 +201,10 @@ async function interrupt(entry: Admission) {
const stopped = new Promise<never>((_yes, no) => { timeout = setTimeout(() => { retired = true; client?.release(true); client = undefined; no(Error("Operation root cleanup deadline")) }, 2500) }) const stopped = new Promise<never>((_yes, no) => { timeout = setTimeout(() => { retired = true; client?.release(true); client = undefined; no(Error("Operation root cleanup deadline")) }, 2500) })
const work = async () => { const work = async () => {
const owned = await connect const owned = await connect
const query = (text: string, values: unknown[] = []) => owned.query({ text, values, query_timeout: Math.max(1, Math.min(1000, deadline - Date.now())) } as QueryConfig) const query = (text: string, values: unknown[] = []) => {
if (retired || Date.now() >= deadline || !sharedDatabaseGate.hasActiveAdmission()) throw Error("Late cleanup SQL")
return owned.query({ text, values, query_timeout: Math.max(1, Math.min(1000, deadline - Date.now())) } as QueryConfig)
}
if ((await query("BEGIN")).command !== "BEGIN") throw Error("No cleanup BEGIN ACK") if ((await query("BEGIN")).command !== "BEGIN") throw Error("No cleanup BEGIN ACK")
const result = await query(`UPDATE public.mcp_operations SET state='unknown',verification='inconclusive',error_code='PROCESS_INTERRUPTED', const result = await query(`UPDATE public.mcp_operations SET state='unknown',verification='inconclusive',error_code='PROCESS_INTERRUPTED',
result=jsonb_build_object('needsReview',true,'automaticRetry',false),finished_at=now() WHERE id=$1 AND plan_id=$2 AND connection_id=$3 result=jsonb_build_object('needsReview',true,'automaticRetry',false),finished_at=now() WHERE id=$1 AND plan_id=$2 AND connection_id=$3
@@ -217,11 +221,21 @@ async function interrupt(entry: Admission) {
try { await Promise.race([work(), stopped]) } catch { console.error("MCP operation root cleanup deferred", { operationId: entry.operationId, retryAllowed: false }) } try { await Promise.race([work(), stopped]) } catch { console.error("MCP operation root cleanup deferred", { operationId: entry.operationId, retryAllowed: false }) }
finally { retired = true; clearTimeout(timeout); if (client) { client.release(!safe); client = undefined } } finally { retired = true; clearTimeout(timeout); if (client) { client.release(!safe); client = undefined } }
} }
/** Cleanup SQL owns a live request/exclusive admission. Waiting for repair is
* detached from runner slots; only admission is retried, never native work. */
async function interruptWithinDatabaseAdmission(entry: Admission): Promise<void> {
if (sharedDatabaseGate.hasActiveAdmission()) return sharedDatabaseGate.withinAdmission(() => interrupt(entry))
if (!sharedDatabaseGate.isPaused()) return sharedDatabaseGate.request(() => interrupt(entry))
void (async () => {
while (sharedDatabaseGate.isPaused()) await sharedDatabaseGate.whenOpen()
await sharedDatabaseGate.request(() => interrupt(entry))
})().catch(() => console.error("MCP operation root cleanup deferred", { operationId: entry.operationId, retryAllowed: false }))
}
/** Aborted admission/queue ownership only. A serialized/foreign cap is inert. */ /** Aborted admission/queue ownership only. A serialized/foreign cap is inert. */
export async function retireMcpOperationAdmission(admission: unknown): Promise<void> { export async function retireMcpOperationAdmission(admission: unknown): Promise<void> {
const entry = admission && typeof admission === "object" ? admissions.get(admission) : undefined const entry = admission && typeof admission === "object" ? admissions.get(admission) : undefined
if (!entry) return if (!entry) return
admissions.delete(admission as object); await interrupt(entry) admissions.delete(admission as object); await interruptWithinDatabaseAdmission(entry)
} }
/** Called exactly by the ordinary runtime before its first pool checkout/lock. /** Called exactly by the ordinary runtime before its first pool checkout/lock.
* The callback is fixed runner code; callers supply no metadata/authorizer. */ * The callback is fixed runner code; callers supply no metadata/authorizer. */
@@ -270,7 +284,7 @@ export async function withMcpOperationExecutionOrigin(admission: unknown, work:
await roots.run(entry, work) await roots.run(entry, work)
} finally { } finally {
if (entry) { entry.active = false; clearTimeout(entry.timer); entry.controller.abort() } if (entry) { entry.active = false; clearTimeout(entry.timer); entry.controller.abort() }
if (interruptOwn) await interrupt(admitted) if (interruptOwn) await interruptWithinDatabaseAdmission(admitted)
} }
} }
export async function captureMcpOperationExecutionOrigin(): Promise<McpOperationExecutionOrigin> { export async function captureMcpOperationExecutionOrigin(): Promise<McpOperationExecutionOrigin> {
@@ -0,0 +1,74 @@
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { assertOwnedBandwidthSql } from "../../services/bandwidth-admission-process.sql-helper.js"
export async function acceptOperationReplayTombstones() {
assertOwnedBandwidthSql()
assert.equal(process.env.MMAPP_OWN_MCP_OPERATION_ORIGIN, "true")
const { pool } = await import("../../db/index.js")
const { mcpConnectionCreateSchema } = await import("@mmapp/contracts/mcp")
const { createMcpConnection, authenticateMcpToken } = await import("./store.js")
const { applyPlan, invokeMcpTool } = await import("./runtime.js")
const { operationIdempotencyDigest } = await import("./operation-replay-tombstones.js")
const { encryptSecret, McpError } = await import("./security.js")
const { default: Fastify } = await import("fastify")
const { default: authPlugin } = await import("../../plugins/auth.js")
const { createMcpRoutes } = await import("../../routes/mcp.js")
const { env } = await import("../../config.js")
const { SignJWT } = await import("jose")
const makeConnection = async () => {
const actor = { id: randomUUID(), email: "owned-tombstone@example.test", name: "Owned replay fixture", apps: ["mm"],
permissions: ["mm:users:write", "mm:mcp:read"], issuedAt: Date.now()-1000, expiresAt: Date.now()+300000 }
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: actor.name, mode: "approval",
permissions: actor.permissions, expiresAt: new Date(actor.expiresAt).toISOString(),
scope: { serverIds: [], operationIds: ["users.create", "operations.read", "changes.apply"], allowGlobal: true },
limits: { callsPerMinute: 100, mutationsPerHour: 10 } }), actor)
return { actor, principal: await authenticateMcpToken(issued.token) }
}
const app = Fastify()
const ids: string[] = []
const matches = (code: string, status: number) => (error: unknown) =>
error instanceof McpError && error.code === code && error.statusCode === status
await pool.query("UPDATE public.mcp_settings SET enabled=true WHERE id=1")
try {
const owner = await makeConnection(), foreign = await makeConnection()
// A real completed plan has already initialized the owned encryption state.
// The fixture seeds its tombstone directly, so exercise the same initializer.
await encryptSecret("owned replay fixture state")
const connectionId = owner.principal.connectionId
ids.push(connectionId, foreign.principal.connectionId)
const operationId = randomUUID(), planId = randomUUID(), key = "owned-replay-" + randomUUID()
await pool.query(`INSERT INTO public.mcp_operation_replay_tombstones
(operation_id,plan_id,connection_id,idempotency_digest,action_id,target_server_ids,target_global)
VALUES($1,$2,$3,$4,'users.create','{}',true)`, [operationId, planId, connectionId, operationIdempotencyDigest(connectionId,key)])
await assert.rejects(applyPlan(owner.principal, planId, key), matches("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(applyPlan(owner.principal, planId, randomUUID()), matches("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(applyPlan(owner.principal, randomUUID(), key), matches("STATE_CONFLICT",409))
await assert.rejects(applyPlan(foreign.principal, planId, key), matches("NOT_FOUND",404))
await assert.rejects(invokeMcpTool(owner.principal,"mm_change_apply",{ planId,idempotencyKey:key }), matches("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(invokeMcpTool(owner.principal,"mm_operation_get",{ operationId }), matches("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(invokeMcpTool(foreign.principal,"mm_operation_get",{ operationId }), matches("NOT_FOUND",404))
await app.register(authPlugin)
await app.register(createMcpRoutes(async () => {}))
for (const subject of [owner, foreign]) {
const token = await new SignJWT({ sub: subject.actor.id, email: subject.actor.email, name: subject.actor.name,
apps: subject.actor.apps, permissions: subject.actor.permissions })
.setProtectedHeader({ alg:"HS256" }).setIssuer(env.authIssuer).setIssuedAt().setExpirationTime("4m")
.sign(new TextEncoder().encode(env.jwtSecret))
const reply = await app.inject({ method: "POST", url: "/mcp/plans/"+planId+"/apply", headers: { authorization:"Bearer "+token } })
assert.equal(reply.statusCode, subject === owner ? 410 : 404)
assert.equal(reply.json().code, subject === owner ? "OPERATION_HISTORY_EXPIRED" : "NOT_FOUND")
}
await pool.query("UPDATE public.mcp_connections SET policy=jsonb_set(policy,'{scope,allowGlobal}','false') WHERE id=$1", [connectionId])
await assert.rejects(applyPlan(owner.principal, planId, key), matches("NOT_FOUND",404))
await assert.rejects(invokeMcpTool(owner.principal,"mm_operation_get",{ operationId }), matches("NOT_FOUND",404))
const { rows:[proof] } = await pool.query(`SELECT
NOT EXISTS(SELECT 1 FROM public.mcp_operations WHERE connection_id=ANY($1::uuid[])) AS no_operation,
NOT EXISTS(SELECT 1 FROM public.mcp_budget_buckets WHERE connection_id=ANY($1::uuid[]) AND kind='mutations') AS no_write_budget`, [ids])
assert.deepEqual(proof,{no_operation:true,no_write_budget:true})
console.log("Owned operation tombstones: scoped MCP and Web 410, replay conflict and no dispatch")
} finally {
await app.close()
await pool.query("DELETE FROM public.mcp_operation_replay_tombstones WHERE connection_id=ANY($1::uuid[])", [ids])
}
}
@@ -0,0 +1,64 @@
import { createHash } from "node:crypto"
import type { PoolClient } from "pg"
import { hasPermission } from "../../lib/permissions.js"
import { listActions } from "./actions.js"
import { McpError } from "./security.js"
import { effectiveMcpConnection, type ConnectionRow, type McpPrincipal } from "./store.js"
type Query = Pick<PoolClient, "query">
export interface OperationReplayTombstone {
operation_id: string
plan_id: string
connection_id: string
action_id: string
target_server_ids: string[]
target_global: boolean
}
// Idempotency tokens are caller input and already stored in plaintext on live
// operations. This domain-separated digest avoids copying that plaintext and
// survives DB restore and access-key rotation. It is not a keyed secret proof.
export function operationIdempotencyDigest(connectionId: string, key: string): string {
return createHash("sha256").update("mcp-operation-replay-v1\0").update(connectionId).update("\0").update(key).digest("hex")
}
export function operationHistoryExpired(): never {
throw new McpError("OPERATION_HISTORY_EXPIRED", "Срок хранения завершённой операции истёк. Повторное применение запрещено.", 410)
}
export function assertOperationReplayScope(row: OperationReplayTombstone, connection: ConnectionRow): void {
const scope = connection.policy.scope
if (row.connection_id !== connection.id || !scope.operationIds.includes(row.action_id)) throw operationReplayNotFound()
const action = listActions().find(candidate => candidate.operation.id === row.action_id)
if (!action || action.operation.requiredPermissions.some(permission =>
!hasPermission(connection.policy.permissions, permission) || !hasPermission(connection.owner_permissions, permission))) throw operationReplayNotFound()
if (row.target_global && !scope.allowGlobal || row.target_server_ids.some(id => !scope.serverIds.some(server => String(server) === id))) throw operationReplayNotFound()
}
function operationReplayNotFound(): McpError {
return new McpError("NOT_FOUND", "Операция не найдена", 404)
}
const projection = "t.operation_id,t.plan_id,t.connection_id,t.action_id,t.target_server_ids::text[] AS target_server_ids,t.target_global"
export async function findOperationReplayTombstone(db: Query, connectionId: string, operationId: string) {
return (await db.query<OperationReplayTombstone>(`SELECT ${projection} FROM mcp_operation_replay_tombstones t
WHERE t.connection_id=$1 AND t.operation_id=$2`, [connectionId, operationId])).rows[0]
}
export async function findOwnerPlanReplayTombstone(db: Query, ownerId: string, planId: string) {
return (await db.query<OperationReplayTombstone>(`SELECT ${projection} FROM mcp_operation_replay_tombstones t
JOIN mcp_connections c ON c.id=t.connection_id WHERE c.owner_id=$1 AND t.plan_id=$2`, [ownerId, planId])).rows[0]
}
export async function refuseExpiredOperationReplay(db: Query, principal: McpPrincipal, connectionId: string, planId: string, key: string): Promise<void> {
const rows = (await db.query<OperationReplayTombstone>(`SELECT ${projection} FROM mcp_operation_replay_tombstones t
WHERE t.connection_id=$1 AND (t.plan_id=$2 OR t.idempotency_digest=$3)`,
[connectionId, planId, operationIdempotencyDigest(connectionId, key)])).rows
if (!rows.length) return
const conflict = rows.find(row => row.plan_id !== planId.toLowerCase())
const current = await effectiveMcpConnection(principal, db)
assertOperationReplayScope(conflict ?? rows[0], current)
if (conflict) throw new McpError("STATE_CONFLICT", "Ключ уже используется другим планом", 409)
operationHistoryExpired()
}
@@ -38,7 +38,7 @@ assert.throws(() => action.schema.parse({ serverId: 1, expectedServerProof: "for
assert.throws(() => action.schema.parse({ serverId: 1, version: 1 })) assert.throws(() => action.schema.parse({ serverId: 1, version: 1 }))
assert.equal(action.schema.parse({ serverId: 1 }).family, "ipv4") assert.equal(action.schema.parse({ serverId: 1 }).family, "ipv4")
assert.equal(findAction("mm_routing_measurement_plan").operation.version, 2) assert.equal(findAction("mm_routing_measurement_plan").operation.version, 2)
assert.equal(findAction("mm_operation_recovery_plan").operation.version, 7) assert.equal(findAction("mm_operation_recovery_plan").operation.version, 8)
assert.equal(findAction("mm_ospf_template_create_plan").operation.version, 5) assert.equal(findAction("mm_ospf_template_create_plan").operation.version, 5)
assert.equal(findAction("mm_ospf_template_update_plan").operation.version, 4) assert.equal(findAction("mm_ospf_template_update_plan").operation.version, 4)
assert.equal(findAction("mm_ospf_template_delete_plan").operation.version, 3) assert.equal(findAction("mm_ospf_template_delete_plan").operation.version, 3)
@@ -49,11 +49,11 @@ assert.equal(findAction("mm_wireguard_update_plan").operation.version, 3)
assert.equal(findAction("mm_wireguard_interface_edit_plan").operation.version, 3) assert.equal(findAction("mm_wireguard_interface_edit_plan").operation.version, 3)
assert.equal(findAction("mm_wireguard_interface_create_plan").operation.version, 3) assert.equal(findAction("mm_wireguard_interface_create_plan").operation.version, 3)
assert.equal(findAction("mm_wireguard_create_plan").operation.version, 4) assert.equal(findAction("mm_wireguard_create_plan").operation.version, 4)
assert.equal(findAction("mm_interfaces_update_plan").operation.version, 4) assert.equal(findAction("mm_interfaces_update_plan").operation.version, 5)
assert.equal(findAction("mm_ipsec_peer_delete_plan").operation.version, 3) assert.equal(findAction("mm_ipsec_peer_delete_plan").operation.version, 3)
assert.equal(findAction("mm_ipsec_peer_update_plan").operation.version, 3) assert.equal(findAction("mm_ipsec_peer_update_plan").operation.version, 3)
assert.equal(findAction("mm_certificate_delete_plan").operation.version, 3) assert.equal(findAction("mm_certificate_delete_plan").operation.version, 3)
for (const [tool, version] of [["mm_route_optimizer_plan", 7], ["mm_route_profile_save_plan", 4], ["mm_route_path_verify", 2], ["mm_operation_recovery_plan", 7], ["mm_ecmp_plan", 2], ["mm_rpf_plan", 2], ["mm_vrf_plan", 3], ["mm_firewall_rule_update_plan", 2]] as const) { for (const [tool, version] of [["mm_route_optimizer_plan", 7], ["mm_route_profile_save_plan", 4], ["mm_route_path_verify", 2], ["mm_operation_recovery_plan", 8], ["mm_ecmp_plan", 2], ["mm_rpf_plan", 2], ["mm_vrf_plan", 4], ["mm_firewall_rule_update_plan", 2]] as const) {
const routed = findAction(tool) const routed = findAction(tool)
assert.equal(routed.operation.version, version) assert.equal(routed.operation.version, version)
assert.equal(findMcpOperation(routed.operation.id)?.version, version) assert.equal(findMcpOperation(routed.operation.id)?.version, version)
@@ -0,0 +1,226 @@
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { setTimeout as delay } from "node:timers/promises"
import { assertOwnedBandwidthSql,prepareOwnedBandwidthState } from "../../services/bandwidth-admission-process.sql-helper.js"
assertOwnedBandwidthSql()
assert.equal(process.env.MMAPP_OWN_MCP_OPERATION_ORIGIN,"true")
await prepareOwnedBandwidthState()
const {pool,closePool}=await import("../../db/index.js")
const {applySqlMigrations}=await import("../../db/migrate.js")
const {mcpConnectionCreateSchema}=await import("@mmapp/contracts/mcp")
const {createMcpConnection,authenticateMcpToken}=await import("./store.js")
const {invokeMcpTool,approvePlan,applyPlan}=await import("./runtime.js")
const {OrdinaryRetirementCommitUnknown,retireOrdinaryMcpOperations}=await import("./ordinary-operation-retirement.js")
const {tickMcpWorkers}=await import("./workers.js")
const {pauseSchedulerJobsAndWait,resumeSchedulerJobs}=await import("../../services/scheduler-running.js")
const {Client}=await import("pg")
const {McpError}=await import("./security.js")
const {touchGeoipRunMeta}=await import("../../services/geoip-settings.js")
const {touchBackupScheduleRunMeta}=await import("../../services/backup-service.js")
const errorIs=(code:string,status:number)=>(error:unknown)=>error instanceof McpError&&error.code===code&&error.statusCode===status
try {
await applySqlMigrations(pool)
await pauseSchedulerJobsAndWait()
await pool.query("UPDATE public.mcp_settings SET enabled=true WHERE id=1")
const serverId=Number((await pool.query("INSERT INTO servers(name,host,enabled) VALUES('owned logs retention','127.0.0.1',false) RETURNING id")).rows[0].id)
async function access(){
const actor={id:randomUUID(),email:"owned-logs-retirement@example.test",name:"Owned settings retention",apps:["mm"],
permissions:["mm:settings:admin","mm:mcp:approve","mm:traffic:write","mm:uptime:write","mm:servers:write","mm:network:write","mm:backups:write"],
issuedAt:Date.now()-1000,expiresAt:Date.now()+300000}
const issued=await createMcpConnection(mcpConnectionCreateSchema.parse({name:actor.name,mode:"approval",
permissions:actor.permissions,expiresAt:new Date(actor.expiresAt).toISOString(),
scope:{serverIds:[serverId],operationIds:["logs.settings.update","traffic.settings.update","uptime.settings.update",
"servers.api-ping.settings.update","internet-path.settings.update","geoip.settings.update","backups.schedule.update","operations.read","changes.apply"],allowGlobal:true},
limits:{callsPerMinute:100,mutationsPerHour:20}}),actor)
return {actor,principal:await authenticateMcpToken(issued.token)}
}
const {actor,principal}=await access(),foreign=(await access()).principal
async function operation(retentionDays:number,ageMs:number,tool="mm_logs_settings_update_plan",
settings:Record<string,unknown>={retentionDays}){
const plan=await invokeMcpTool(principal,tool,{settings}) as {planId:string;state:string}
if(tool==="mm_geoip_settings_update_plan"||tool==="mm_backup_schedule_update_plan")assert.equal(plan.state,"awaiting-approval")
if(plan.state==="awaiting-approval")await approvePlan(plan.planId,actor,true)
const key=randomUUID(),applied=await applyPlan(principal,plan.planId,key)
let terminal
for(const deadline=Date.now()+15000;Date.now()<deadline;){
terminal=(await pool.query("SELECT state,verification,error_code FROM mcp_operations WHERE id=$1",[applied.id])).rows[0]
if(terminal&&!["pending","running"].includes(terminal.state))break
await delay(50)
}
assert.deepEqual(terminal,{state:"applied",verification:"passed",error_code:null})
// Only the disposable fixture ages rows, after real authenticated apply.
// No cutoff override or production encryption/approval bypass.
const finished=new Date(Date.now()-ageMs),created=new Date(finished.getTime()-1000)
await pool.query("UPDATE mcp_plans SET created_at=$2,approved_at=$2,expires_at=$3 WHERE id=$1",
[plan.planId,created,new Date(finished.getTime()+1000)])
await pool.query("UPDATE mcp_operations SET created_at=$2,finished_at=$3 WHERE id=$1",[applied.id,created,finished])
await pool.query("UPDATE mcp_operation_steps SET created_at=$2,finished_at=$3 WHERE operation_id=$1",[applied.id,created,finished])
return {id:applied.id,planId:plan.planId,key}
}
const age=180*86400000
const held=await operation(51,age+180000),spent=await operation(52,age+120000)
const due=await operation(53,age+60000),young=await operation(54,age-60000)
const boundary=await pool.connect()
try{
await boundary.query("BEGIN")
await boundary.query("UPDATE mcp_operations SET finished_at=transaction_timestamp()-interval '180 days' WHERE id=$1",[young.id])
await assert.rejects(boundary.query(`INSERT INTO mcp_ordinary_retirement_claims
(operation_id,plan_id,connection_id,source_proof,claim_token) VALUES($1,$2,$3,$4,$5)`,
[young.id,young.planId,principal.connectionId,"a".repeat(64),randomUUID()]),/cannot be replaced or reopened/)
}finally{await boundary.query("ROLLBACK");boundary.release()}
await pool.query("INSERT INTO mcp_drift(id,operation_id,expected,observed) VALUES($1,$2,'{}','{}')",[randomUUID(),held.id])
const ids=[held.id,spent.id,due.id,young.id]
const projection=async()=>(await pool.query(`SELECT i.id,o.state,p.state AS plan_state,s.state AS step_state,
k.operation_id AS checkpoint,t.operation_id AS tombstone,c.claim_token
FROM unnest($1::uuid[]) i(id)
LEFT JOIN mcp_operations o ON o.id=i.id LEFT JOIN mcp_plans p ON p.id=o.plan_id
LEFT JOIN mcp_operation_steps s ON s.operation_id=i.id LEFT JOIN mcp_operation_checkpoints k ON k.operation_id=i.id
LEFT JOIN mcp_operation_replay_tombstones t ON t.operation_id=i.id
LEFT JOIN mcp_ordinary_retirement_claims c ON c.operation_id=i.id ORDER BY i.id`,[ids])).rows
const originalQuery=Client.prototype.query
let commits=0
try{
Client.prototype.query=function(this:InstanceType<typeof Client>,...args:unknown[]){
if((args[0] as {text?:string})?.text==="COMMIT"&&++commits===3)
return Promise.reject(Error("owned ordinary destructive COMMIT transport loss"))
return Reflect.apply(originalQuery,this,args)
} as typeof Client.prototype.query
await assert.rejects(retireOrdinaryMcpOperations(),
(error:unknown)=>error instanceof OrdinaryRetirementCommitUnknown&&error.automaticReplay===false)
}finally{Client.prototype.query=originalQuery}
assert.equal(commits,3)
const before=await projection(),spentBefore=before.find(row=>row.id===spent.id)
assert.equal(spentBefore?.state,"applied");assert(spentBefore?.claim_token);assert.equal(spentBefore?.tombstone,null)
await tickMcpWorkers()
const after=await projection()
assert.deepEqual(after.filter(row=>row.id!==due.id),before.filter(row=>row.id!==due.id))
const retired=after.find(row=>row.id===due.id)
assert(retired?.claim_token);assert.equal(retired?.tombstone,due.id)
for(const field of ["state","plan_state","step_state","checkpoint"])assert.equal(retired[field],null)
assert.equal((await pool.query("SELECT 1 FROM mcp_plans WHERE id=$1",[due.planId])).rows.length,0)
assert.equal((await pool.query("SELECT retention_days FROM router_log_settings WHERE id=1")).rows[0].retention_days,54)
await tickMcpWorkers()
assert.deepEqual(await projection(),after)
await assert.rejects(applyPlan(principal,due.planId,due.key),errorIs("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(applyPlan(principal,randomUUID(),due.key),errorIs("STATE_CONFLICT",409))
await assert.rejects(invokeMcpTool(foreign,"mm_operation_get",{operationId:due.id}),errorIs("NOT_FOUND",404))
const telemetry=[
{action:"traffic.settings.update",version:2,record:await operation(55,age+60000,"mm_traffic_settings_update_plan")},
{action:"uptime.settings.update",version:2,record:await operation(56,age+60000,"mm_uptime_settings_update_plan",{enabled:false,retentionDays:56})},
{action:"servers.api-ping.settings.update",version:2,record:await operation(0,age+60000,"mm_servers_api_ping_settings_update_plan",{intervalSec:123})},
{action:"internet-path.settings.update",version:1,record:await operation(57,age+60000,"mm_internet_path_settings_update_plan")},
]
const telemetryRows=async()=>(await pool.query(`SELECT p.operation_id,p.operation_version,o.id
FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id WHERE o.id=ANY($1::uuid[]) ORDER BY p.operation_id`,
[telemetry.map(item=>item.record.id)])).rows
assert.deepEqual((await telemetryRows()).map(row=>[row.operation_id,row.operation_version]),
telemetry.map(item=>[item.action,item.version]).sort((a,b)=>String(a[0]).localeCompare(String(b[0]))))
const telemetrySettings=async()=>(await pool.query(`SELECT to_jsonb(t) AS traffic,to_jsonb(u) AS uptime,
to_jsonb(a) AS api_ping,to_jsonb(i) AS internet FROM traffic_settings t,uptime_settings u,
servers_api_ping_settings a,internet_path_settings i WHERE t.id=1 AND u.id=1 AND a.id=1 AND i.id=1`)).rows[0]
const settingsBefore=await telemetrySettings()
const traffic=telemetry[0].record
const {rows:[originalStep]}=await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1",[traffic.id])
await pool.query("UPDATE mcp_operation_steps SET result=jsonb_set(result,'{retentionDays}','999') WHERE operation_id=$1",[traffic.id])
await tickMcpWorkers()
assert.equal((await pool.query("SELECT 1 FROM mcp_ordinary_retirement_claims WHERE operation_id=$1",[traffic.id])).rows.length,0)
assert.equal((await pool.query("SELECT 1 FROM mcp_operations WHERE id=$1",[traffic.id])).rows.length,1)
await pool.query("UPDATE mcp_operation_steps SET result=$2 WHERE operation_id=$1",[traffic.id,originalStep.result])
await tickMcpWorkers()
await tickMcpWorkers()
// The keyset owner may need one tick to wrap after the rejected oldest row.
await tickMcpWorkers()
assert.deepEqual(await telemetryRows(),[])
assert.deepEqual(await telemetrySettings(),settingsBefore)
for(const {action,record} of telemetry){
const {rows:[proof]}=await pool.query(`SELECT t.action_id,t.target_global,t.target_server_ids::text[] AS server_ids,
NOT EXISTS(SELECT 1 FROM mcp_plans WHERE id=$2) AS plan_gone,
NOT EXISTS(SELECT 1 FROM mcp_operation_steps WHERE operation_id=$1) AS step_gone,
NOT EXISTS(SELECT 1 FROM mcp_operation_checkpoints WHERE operation_id=$1) AS checkpoint_gone
FROM mcp_operation_replay_tombstones t WHERE t.operation_id=$1 AND t.plan_id=$2`,[record.id,record.planId])
assert.deepEqual(proof,{action_id:action,target_global:true,server_ids:[String(serverId)],plan_gone:true,step_gone:true,checkpoint_gone:true})
await assert.rejects(applyPlan(principal,record.planId,record.key),errorIs("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(invokeMcpTool(foreign,"mm_operation_get",{operationId:record.id}),errorIs("NOT_FOUND",404))
}
// Keep the owned fixture offline even if a product scheduler refresh occurs.
await pool.query("UPDATE geoip_settings SET enabled=false WHERE id=1")
await touchGeoipRunMeta({lastCheckAt:new Date().toISOString(),lastError:"owned GeoIP metadata",etags:{"owned-test":"unchanged"}})
const geoip=await operation(0,age+60000,"mm_geoip_settings_update_plan",{enabled:false,updateIntervalSec:86400})
const geoipState=async()=>(await pool.query("SELECT to_jsonb(g) AS row FROM geoip_settings g WHERE id=1")).rows[0].row
const geoipBefore=await geoipState()
assert.equal(geoipBefore.enabled,false);assert.equal(geoipBefore.update_interval_sec,86400)
assert.equal(geoipBefore.last_error,"owned GeoIP metadata");assert.deepEqual(geoipBefore.etags_json,{"owned-test":"unchanged"})
const {rows:[geoipStep]}=await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1",[geoip.id])
const assertGeoipUnclaimed=async()=>{
assert.equal((await pool.query("SELECT 1 FROM mcp_ordinary_retirement_claims WHERE operation_id=$1",[geoip.id])).rows.length,0)
assert.equal((await pool.query("SELECT 1 FROM mcp_operations WHERE id=$1",[geoip.id])).rows.length,1)
assert.deepEqual(await geoipState(),geoipBefore)
}
await pool.query("UPDATE mcp_operation_steps SET result=jsonb_set(result,'{settings,updateIntervalSec}','172800') WHERE operation_id=$1",[geoip.id])
await tickMcpWorkers();await tickMcpWorkers();await assertGeoipUnclaimed()
await pool.query("UPDATE mcp_operation_steps SET result=$2 WHERE operation_id=$1",[geoip.id,geoipStep.result])
const geoipHold=randomUUID()
await pool.query("INSERT INTO mcp_drift(id,operation_id,expected,observed) VALUES($1,$2,'{}','{}')",[geoipHold,geoip.id])
await tickMcpWorkers();await assertGeoipUnclaimed()
await pool.query("DELETE FROM mcp_drift WHERE id=$1",[geoipHold])
await tickMcpWorkers();await tickMcpWorkers()
const {rows:[geoipTombstone]}=await pool.query(`SELECT t.action_id,t.target_global,t.target_server_ids::text[] AS server_ids,
NOT EXISTS(SELECT 1 FROM mcp_operations WHERE id=$1) AS operation_gone,
NOT EXISTS(SELECT 1 FROM mcp_plans WHERE id=$2) AS plan_gone,
NOT EXISTS(SELECT 1 FROM mcp_operation_steps WHERE operation_id=$1) AS step_gone,
NOT EXISTS(SELECT 1 FROM mcp_operation_checkpoints WHERE operation_id=$1) AS checkpoint_gone
FROM mcp_operation_replay_tombstones t WHERE t.operation_id=$1 AND t.plan_id=$2`,[geoip.id,geoip.planId])
assert.deepEqual(geoipTombstone,{action_id:"geoip.settings.update",target_global:true,server_ids:[String(serverId)],
operation_gone:true,plan_gone:true,step_gone:true,checkpoint_gone:true})
await assert.rejects(applyPlan(principal,geoip.planId,geoip.key),errorIs("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(applyPlan(principal,randomUUID(),geoip.key),errorIs("STATE_CONFLICT",409))
await assert.rejects(invokeMcpTool(foreign,"mm_operation_get",{operationId:geoip.id}),errorIs("NOT_FOUND",404))
assert.deepEqual(await geoipState(),geoipBefore)
await pool.query("UPDATE backup_schedule_settings SET enabled=false,server_ids_json='[]' WHERE id=1")
await touchBackupScheduleRunMeta({lastRunAt:new Date().toISOString(),lastDurationMs:123,lastError:"owned backup scheduler metadata"})
await pool.query(`INSERT INTO backup_entries(id,server_id,server_name,filename,size_bytes,kind,notes,created_at)
VALUES($1,$2,'owned fixture','owned-retention.rsc',123,'auto','keep',clock_timestamp())`,[randomUUID(),serverId])
const backupState=async()=>(await pool.query(`SELECT to_jsonb(s) AS schedule,
(SELECT jsonb_agg(to_jsonb(b) ORDER BY id) FROM backup_entries b) AS entries
FROM backup_schedule_settings s WHERE id=1`)).rows[0]
const backupOutside=await operation(0,age+120000,"mm_backup_schedule_update_plan",{enabled:false,serverIds:[String(serverId+1000)]})
const outsideState=await backupState()
await tickMcpWorkers();await tickMcpWorkers()
assert.equal((await pool.query("SELECT 1 FROM mcp_ordinary_retirement_claims WHERE operation_id=$1",[backupOutside.id])).rows.length,0)
assert.equal((await pool.query("SELECT 1 FROM mcp_operations WHERE id=$1",[backupOutside.id])).rows.length,1)
assert.deepEqual(await backupState(),outsideState)
await pool.query("UPDATE backup_schedule_settings SET server_ids_json='[]' WHERE id=1")
const backup=await operation(0,age+60000,"mm_backup_schedule_update_plan",{enabled:false,keepCount:9,serverIds:[String(serverId)]})
const backupBefore=await backupState()
assert.equal(backupBefore.schedule.enabled,false);assert.equal(backupBefore.schedule.keep_count,9)
assert.deepEqual(backupBefore.schedule.server_ids_json,[String(serverId)])
assert.equal(backupBefore.schedule.last_duration_ms,123);assert.equal(backupBefore.schedule.last_error,"owned backup scheduler metadata")
assert.deepEqual(backupBefore.entries,outsideState.entries)
const {rows:[backupStep]}=await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1",[backup.id])
await pool.query("UPDATE mcp_operation_steps SET result=jsonb_set(result,'{keepCount}','10') WHERE operation_id=$1",[backup.id])
await tickMcpWorkers();await tickMcpWorkers()
assert.equal((await pool.query("SELECT 1 FROM mcp_ordinary_retirement_claims WHERE operation_id=$1",[backup.id])).rows.length,0)
assert.deepEqual(await backupState(),backupBefore)
await pool.query("UPDATE mcp_operation_steps SET result=$2 WHERE operation_id=$1",[backup.id,backupStep.result])
await tickMcpWorkers();await tickMcpWorkers()
const {rows:[backupTombstone]}=await pool.query(`SELECT t.action_id,t.target_global,t.target_server_ids::text[] AS server_ids,
NOT EXISTS(SELECT 1 FROM mcp_operations WHERE id=$1) AS operation_gone,
NOT EXISTS(SELECT 1 FROM mcp_plans WHERE id=$2) AS plan_gone,
NOT EXISTS(SELECT 1 FROM mcp_operation_steps WHERE operation_id=$1) AS step_gone,
NOT EXISTS(SELECT 1 FROM mcp_operation_checkpoints WHERE operation_id=$1) AS checkpoint_gone
FROM mcp_operation_replay_tombstones t WHERE t.operation_id=$1 AND t.plan_id=$2`,[backup.id,backup.planId])
assert.deepEqual(backupTombstone,{action_id:"backups.schedule.update",target_global:true,server_ids:[String(serverId)],
operation_gone:true,plan_gone:true,step_gone:true,checkpoint_gone:true})
await assert.rejects(applyPlan(principal,backup.planId,backup.key),errorIs("OPERATION_HISTORY_EXPIRED",410))
await assert.rejects(applyPlan(principal,randomUUID(),backup.key),errorIs("STATE_CONFLICT",409))
await assert.rejects(invokeMcpTool(foreign,"mm_operation_get",{operationId:backup.id}),errorIs("NOT_FOUND",404))
assert.deepEqual(await backupState(),backupBefore)
await pool.query("UPDATE mcp_connections SET policy=jsonb_set(policy,'{scope,serverIds}','[]') WHERE id=$1",[principal.connectionId])
await assert.rejects(applyPlan(principal,due.planId,due.key),errorIs("NOT_FOUND",404))
for(const {record} of telemetry)await assert.rejects(applyPlan(principal,record.planId,record.key),errorIs("NOT_FOUND",404))
await assert.rejects(applyPlan(principal,geoip.planId,geoip.key),errorIs("NOT_FOUND",404))
await assert.rejects(applyPlan(principal,backup.planId,backup.key),errorIs("NOT_FOUND",404))
console.log("Owned settings operations: seven exact families, authenticated apply, physical 180-day purge, preserved settings and scoped replay")
}finally{resumeSchedulerJobs();await closePool()}
@@ -0,0 +1,221 @@
import { randomUUID } from "node:crypto"
import { logSettingsPatchSchema, logSettingsSchema } from "@mmapp/contracts"
import { geoipSettingsDtoSchema, geoipSettingsPatchSchema } from "@mmapp/contracts/geoip"
import { backupScheduleSettingsDtoSchema, putBackupScheduleSettingsSchema } from "@mmapp/contracts/backups"
import { z } from "zod"
import { connectProbeStorage } from "../../services/server-probe-storage-connection.js"
import type { ProbeStorageSql } from "../../services/server-probe-journal-maintenance.js"
import { sharedDatabaseGate } from "../../services/shared-database-gate.js"
import { operationIdempotencyDigest } from "./operation-replay-tombstones.js"
import { decryptSecretExisting, fingerprint, privateFingerprinterExisting, redact } from "./security.js"
const uuid = z.string().uuid()
const positive = z.number().int().positive().safe()
const object = z.record(z.string(),z.unknown())
const sealed = z.object({sealed:z.string().min(1).max(262144)}).strict()
const ids = z.array(positive).max(1000)
const retention = z.number().int().min(1).max(365)
const bool = z.boolean()
const interval = (min:number,max:number)=>z.number().int().min(min).max(max)
const backupServerIds=z.array(z.string().regex(/^[1-9]\d{0,15}$/).refine(value=>Number.isSafeInteger(Number(value))))
.max(1000).refine(values=>new Set(values).size===values.length)
const backupSchedule=backupScheduleSettingsDtoSchema.extend({serverIds:backupServerIds}).strict()
const backupBefore=z.object({value:backupSchedule,serverIds:ids}).strict()
// Drizzle's string-mode timestamptz keeps PostgreSQL's space and hour-only
// offset. Accept that exact wire form as well as an ISO timestamptz.
const timestamp = z.string().regex(/^\d{4}-\d\d-\d\d[ T]\d\d:\d\d:\d\d(?:\.\d{1,6})?(?:Z|[+-]\d\d(?::\d\d)?)$/)
.refine(value=>Number.isFinite(Date.parse(value.replace(" ","T").replace(/([+-]\d\d)$/,"$1:00"))))
const metadata = {
id:z.literal(1),createdAt:timestamp,updatedAt:timestamp,
lastCollectedAt:timestamp.nullable(),lastDurationMs:z.number().int().safe().nullable(),
lastError:z.string().max(16384).nullable(),
}
function telemetryFamily(version:number,domain:string,settings:z.ZodObject,hasEpoch:boolean,hasConfigurationProof:boolean){
const originalSettings=hasConfigurationProof?settings.extend({configurationProof:z.string().regex(/^[0-9a-f]{64}$/)}):settings
const effect=settings.extend({...metadata,...(hasEpoch?{pollConfigurationEpoch:uuid}:{})}).strict()
return {version,domain,patch:settings.partial().strict().refine(value=>Object.keys(value).length>0),
before:z.object({value:originalSettings.strict(),serverIds:ids}).strict(),effect}
}
const families={
"logs.settings.update":{version:1,domain:null,patch:logSettingsPatchSchema,before:logSettingsSchema.strict(),effect:logSettingsSchema.strict()},
"traffic.settings.update":telemetryFamily(2,"traffic-settings",z.object({enabled:bool,intervalSec:interval(5,3600),retentionDays:retention}),true,true),
"uptime.settings.update":telemetryFamily(2,"uptime-settings",z.object({enabled:bool,resourcesEnabled:bool,pingEnabled:bool,
speedEnabled:bool,intervalSec:interval(5,86400),probeIntervalSec:interval(5,86400),speedIntervalSec:interval(10,86400),retentionDays:retention}),true,true),
"servers.api-ping.settings.update":telemetryFamily(2,"servers-api-ping-settings",z.object({enabled:bool,intervalSec:interval(10,86400)}),false,true),
"internet-path.settings.update":telemetryFamily(1,"internet-path-settings",z.object({enabled:bool,intervalSec:interval(30,86400),retentionDays:retention}),true,false),
"geoip.settings.update":{
version:1,domain:"geoip-settings",
patch:geoipSettingsPatchSchema.strict().refine(value=>Object.keys(value).length>0),
before:z.object({value:z.object({enabled:bool,updateIntervalSec:positive}).strict(),serverIds:ids}).strict(),
effect:z.object({settings:geoipSettingsDtoSchema.strict(),verificationScope:z.literal("configuration-persistence")})
.strict().transform(result=>result.settings),
},
"backups.schedule.update":{
version:1,domain:"backup-schedule",
patch:putBackupScheduleSettingsSchema.extend({serverIds:backupServerIds.optional()}).strict().refine(value=>Object.keys(value).length>0),
before:backupBefore,effect:backupSchedule,
},
} as const
const planEvidence = z.object({
target:z.object({scope:z.literal("global")}).strict(),
objects:z.array(z.object({kind:z.string(),id:z.string(),serverId:positive.optional()}).strict()).max(1001),
steps:z.tuple([z.object({kind:z.literal("service"),service:z.string(),input:z.object({settings:object}).strict()}).strict()]),
snapshots:z.tuple([]),before:object,warnings:z.array(z.string()).max(8),
verificationScope:z.literal("configuration").optional(),
requiresHuman:z.literal(true).optional(),
}).strict()
class RetirementRefused extends Error {}
export class OrdinaryRetirementCommitUnknown extends Error {
readonly automaticReplay = false
constructor() { super("Ordinary operation retirement outcome unknown; automatic replay forbidden") }
}
const requireThat: (value: unknown) => asserts value = value => {
if (!value) throw new RetirementRefused()
}
// Fixed reviewed settings families without artifact, historic receipt or
// inverse-plan consumers. Other action families need separate review.
const ELIGIBLE = `FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=0
JOIN public.mcp_operation_checkpoints k ON k.operation_id=o.id AND k.step_index=0
WHERE (p.operation_id,p.operation_version) IN (
('logs.settings.update',1),('traffic.settings.update',2),('uptime.settings.update',2),
('servers.api-ping.settings.update',2),('internet-path.settings.update',1),('geoip.settings.update',1),('backups.schedule.update',1)
) AND p.state='completed'
AND o.state='applied' AND o.verification='passed' AND o.error_code IS NULL
AND isfinite(o.finished_at) AND o.finished_at<transaction_timestamp()-interval '180 days'
AND isfinite(p.expires_at) AND p.expires_at<transaction_timestamp()
AND s.state='verified' AND s.server_id IS NULL AND isfinite(s.finished_at)
AND k.after_sealed IS NOT NULL
AND (SELECT count(*) FROM public.mcp_operation_steps WHERE operation_id=o.id)=1
AND (SELECT count(*) FROM public.mcp_operation_checkpoints WHERE operation_id=o.id)=1
AND NOT EXISTS(SELECT 1 FROM public.mcp_drift WHERE operation_id=o.id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_recovery_intents WHERE operation_id=o.id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_scoped_recovery_intents WHERE source_operation_id=o.id OR recovery_operation_id=o.id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_routing_reservations WHERE source_operation_id=o.id OR source_plan_id=p.id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_routing_forward_charges WHERE source_operation_id=o.id OR source_plan_id=p.id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_routing_manual_inverse_charges WHERE source_operation_id=o.id OR recovery_operation_id=o.id OR source_plan_id=p.id OR recovery_plan_id=p.id)
AND NOT EXISTS(SELECT 1 FROM public.mcp_diagnostic_cancellations WHERE operation_id=o.id)
AND NOT EXISTS(SELECT 1 FROM public.server_probe_storage_admissions WHERE identity_kind='mcp' AND execution_id=o.id)
AND NOT EXISTS(SELECT 1 FROM public.server_poll_storage_admissions WHERE identity_kind='mcp' AND execution_id=o.id)
AND NOT EXISTS(SELECT 1 FROM public.vrf_production_tickets WHERE plan_id=p.id)`
const CANDIDATES = `SELECT o.id,o.connection_id,o.finished_at::text AS finished_at ${ELIGIBLE}
AND NOT EXISTS(SELECT 1 FROM public.mcp_ordinary_retirement_claims WHERE operation_id=o.id)
AND ($1::timestamptz IS NULL OR (o.finished_at,o.id)>($1::timestamptz,$2::uuid))
ORDER BY o.finished_at,o.id LIMIT 8`
const SOURCE = `SELECT to_jsonb(o) AS operation,to_jsonb(p) AS plan,to_jsonb(s) AS step,to_jsonb(k) AS checkpoint ${ELIGIBLE}
AND o.id=$1 AND o.connection_id=$2
AND octet_length(to_jsonb(o)::text)+octet_length(to_jsonb(p)::text)+octet_length(to_jsonb(s)::text)+octet_length(to_jsonb(k)::text)<=1048576
FOR UPDATE OF o,p,s,k NOWAIT`
async function transaction<T>(deadlineAt:number, work:(sql:ProbeStorageSql)=>Promise<T>):Promise<T> {
const sql = await connectProbeStorage({deadlineAt})
let committing=false,committed=false
try {
await sql.query("BEGIN")
await sql.query("SELECT set_config('TimeZone','UTC',true),set_config('statement_timeout','5000ms',true),set_config('lock_timeout','1000ms',true)")
const result=await work(sql)
committing=true
await sql.query("COMMIT")
committed=true
return result
} catch(error) {
if(committing)throw new OrdinaryRetirementCommitUnknown()
try{await sql.query("ROLLBACK")}catch{/* Destroy this checkout. */}
throw error
} finally {
sql.release(!committed)
}
}
async function prepare(sql:ProbeStorageSql, operationId:string, connectionId:string) {
requireThat((await sql.query("SELECT id FROM public.mcp_connections WHERE id=$1 FOR UPDATE NOWAIT",[connectionId])).rows.length===1)
requireThat((await sql.query("SELECT pg_try_advisory_xact_lock(hashtextextended($1,0)) AS locked",["mcp-operation:"+operationId])).rows[0]?.locked===true)
const rows=(await sql.query(SOURCE,[operationId,connectionId])).rows
requireThat(rows.length===1)
const raw=rows[0], operation=object.parse(raw.operation), plan=object.parse(raw.plan)
const step=object.parse(raw.step),checkpoint=object.parse(raw.checkpoint)
const open=async(value:unknown)=>JSON.parse(await decryptSecretExisting(sealed.parse(value).sealed))
const original=planEvidence.parse(await open(plan.evidence))
const action=z.enum(Object.keys(families) as [keyof typeof families,...(keyof typeof families)[]]).parse(plan.operation_id)
const family=families[action],service=original.steps[0]
requireThat(plan.operation_version===family.version&&service.service===action)
requireThat(fingerprint(await open(plan.payload))===fingerprint(service.input))
requireThat(fingerprint(step.intended_state)===fingerprint(redact(service)))
const patch=object.parse(family.patch.parse(service.input.settings))
const effect=object.parse(family.effect.parse(step.result))
requireThat(Object.entries(patch).every(([key,value])=>fingerprint(effect[key])===fingerprint(value)))
requireThat(fingerprint(operation.result)===fingerprint({confirmedWrites:1,verificationScope:"configuration"}))
for(const value of [checkpoint.before_sealed,checkpoint.after_sealed]){
requireThat(typeof value==="string"&&value.length<=1024&&fingerprint(await open({sealed:value}))===fingerprint([]))
}
const serverObjects=original.objects.filter(item=>item.kind==="server")
const serverIds=ids.parse(serverObjects.map(item=>item.serverId))
requireThat(new Set(serverIds).size===serverIds.length&&serverObjects.every(item=>item.id===String(item.serverId)))
const domainObjects=original.objects.filter(item=>item.kind!=="server")
const before=family.before.parse(original.before)
if(family.domain===null){
requireThat(domainObjects.length===0&&original.requiresHuman===undefined)
}else{
requireThat(original.requiresHuman===true&&original.verificationScope==="configuration"
&&domainObjects.length===1&&domainObjects[0].kind===family.domain&&domainObjects[0].id==="1"&&domainObjects[0].serverId===undefined)
requireThat("serverIds" in before&&fingerprint(before.serverIds)===fingerprint(serverIds))
}
if(action==="backups.schedule.update"){
const scope=new Set(serverIds.map(String))
requireThat([...backupBefore.parse(before).value.serverIds,...backupSchedule.parse(effect).serverIds].every(id=>scope.has(id)))
}
const signer=await privateFingerprinterExisting("mcp-ordinary-retirement-source-v1")
return {planId:uuid.parse(plan.id),action,serverIds,key:z.string().min(1).max(200).parse(operation.idempotency_key),proof:signer(fingerprint(raw))}
}
async function retire(operationId:string,connectionId:string,deadlineAt:number) {
const token=randomUUID()
await transaction(deadlineAt,async sql=>{
const source=await prepare(sql,operationId,connectionId)
const claim=await sql.query(`INSERT INTO public.mcp_ordinary_retirement_claims(operation_id,plan_id,connection_id,source_proof,claim_token)
VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING RETURNING operation_id`,[operationId,source.planId,connectionId,source.proof,token])
requireThat(claim.rows.length===1&&claim.rows[0].operation_id===operationId)
})
await transaction(deadlineAt,async sql=>{
const claim=(await sql.query(`SELECT source_proof,plan_id FROM public.mcp_ordinary_retirement_claims
WHERE operation_id=$1 AND connection_id=$2 AND claim_token=$3 AND claim_xid<>pg_current_xact_id()`,[operationId,connectionId,token])).rows
requireThat(claim.length===1)
const source=await prepare(sql,operationId,connectionId)
requireThat(claim[0].source_proof===source.proof&&claim[0].plan_id===source.planId)
const tombstone=await sql.query(`INSERT INTO public.mcp_operation_replay_tombstones
(operation_id,plan_id,connection_id,idempotency_digest,action_id,target_server_ids,target_global)
VALUES($1,$2,$3,$4,$5,$6,true) RETURNING operation_id`,
[operationId,source.planId,connectionId,operationIdempotencyDigest(connectionId,source.key),source.action,source.serverIds])
requireThat(tombstone.rows.length===1&&tombstone.rows[0].operation_id===operationId)
const checkpoint=await sql.query("DELETE FROM public.mcp_operation_checkpoints WHERE operation_id=$1 AND step_index=0 RETURNING operation_id",[operationId])
const step=await sql.query("DELETE FROM public.mcp_operation_steps WHERE operation_id=$1 AND step_index=0 RETURNING operation_id",[operationId])
const operation=await sql.query("DELETE FROM public.mcp_operations WHERE id=$1 AND connection_id=$2 AND plan_id=$3 RETURNING id AS operation_id",[operationId,connectionId,source.planId])
const plan=await sql.query("DELETE FROM public.mcp_plans WHERE id=$1 AND connection_id=$2 RETURNING id AS plan_id",[source.planId,connectionId])
requireThat([checkpoint,step,operation].every(result=>result.rows.length===1&&result.rows[0].operation_id===operationId)
&&plan.rows.length===1&&plan.rows[0].plan_id===source.planId)
})
}
let cursor:{finishedAt:string;operationId:string}|undefined
export function retireOrdinaryMcpOperations() {
return sharedDatabaseGate.request(async()=>{
const deadlineAt=Date.now()+15000
const rows=await transaction(deadlineAt,async sql=>(await sql.query(CANDIDATES,[cursor?.finishedAt??null,cursor?.operationId??null])).rows)
requireThat(rows.length<=8)
let scanned=0,retired=0
for(const row of rows){
if(Date.now()>=deadlineAt-2000||retired>=2)break
const id=uuid.parse(row.id),connectionId=uuid.parse(row.connection_id)
requireThat(typeof row.finished_at==="string"&&Number.isFinite(Date.parse(row.finished_at)))
cursor={finishedAt:row.finished_at,operationId:id}
scanned++
try{await retire(id,connectionId,deadlineAt);retired++}
catch(error){if(error instanceof OrdinaryRetirementCommitUnknown)throw error}
}
if(scanned===rows.length&&rows.length<8)cursor=undefined
return {scanned,retired,automaticReplay:false as const}
})
}
@@ -2,7 +2,7 @@ import assert from "node:assert/strict"
import { createOspfOwnerSourceGuard, verifyOspfOwnerGraph, verifyOspfOwnerGraphAtPhase } from "./ospf-owner-graph.js" import { createOspfOwnerSourceGuard, verifyOspfOwnerGraph, verifyOspfOwnerGraphAtPhase } from "./ospf-owner-graph.js"
import { fingerprint, McpError, redact } from "./security.js" import { fingerprint, McpError, redact } from "./security.js"
import { createRosPrefix } from "./ros-prefix.js" import { createRosPrefix } from "./ros-prefix.js"
import { ROS_SNAPSHOT_VERSION, type RosSnapshot } from "./resources.js" import { rosSnapshotVersion, type RosSnapshot } from "./resources.js"
const ids = [1, 2, 3] const ids = [1, 2, 3]
const graph = { family: "ipv4" as const, sourceSecretProofs: ids.map(serverId => ({ serverId, proof: fingerprint({ serverId }) })), instances: ids.map(serverId => ({ serverId, fingerprint: fingerprint({ serverId, native: "instances" }) })) } const graph = { family: "ipv4" as const, sourceSecretProofs: ids.map(serverId => ({ serverId, proof: fingerprint({ serverId }) })), instances: ids.map(serverId => ({ serverId, fingerprint: fingerprint({ serverId, native: "instances" }) })) }
@@ -54,7 +54,7 @@ const catalogAuthorize = async () => {
await sourceGuard() await sourceGuard()
} }
const catalogRows = [{ ".id": "*A", name: "keep", disabled: "false" }] const catalogRows = [{ ".id": "*A", name: "keep", disabled: "false" }]
const catalog: RosSnapshot = { serverId: 1, resource: "bridge", rows: catalogRows, fingerprint: fingerprint(catalogRows), projectionVersion: ROS_SNAPSHOT_VERSION, sourceProof: "a".repeat(64) } const catalog: RosSnapshot = { serverId: 1, resource: "bridge", rows: catalogRows, fingerprint: fingerprint(catalogRows), projectionVersion: rosSnapshotVersion("bridge"), sourceProof: "a".repeat(64) }
reads = [] reads = []
const catalogPrefix = createRosPrefix([catalog], async (_id, _resource, beforeRead = async () => {}) => { const catalogPrefix = createRosPrefix([catalog], async (_id, _resource, beforeRead = async () => {}) => {
// Match readRosSnapshot's normal three grant/source fences without polling a // Match readRosSnapshot's normal three grant/source fences without polling a
@@ -3,6 +3,7 @@ import { z } from "zod"
export const pathProofPurposeSchema = z.enum(["community", "control-path"]) export const pathProofPurposeSchema = z.enum(["community", "control-path"])
export type PathProofPurpose = z.infer<typeof pathProofPurposeSchema> export type PathProofPurpose = z.infer<typeof pathProofPurposeSchema>
export const PATH_PROOF_VERSION = 2 export const PATH_PROOF_VERSION = 2
export const COMMUNITY_PATH_MAX_AGE_MS = 90000
export const pathProofTag = (purpose: PathProofPurpose) => "path-check:" + purpose + ":v" + PATH_PROOF_VERSION + ":" export const pathProofTag = (purpose: PathProofPurpose) => "path-check:" + purpose + ":v" + PATH_PROOF_VERSION + ":"
export function isCommunityPathEvidence(value: Record<string, unknown>) { export function isCommunityPathEvidence(value: Record<string, unknown>) {
return value.proofPurpose === "community" && value.proofVersion === PATH_PROOF_VERSION return value.proofPurpose === "community" && value.proofVersion === PATH_PROOF_VERSION
+3 -3
View File
@@ -8,7 +8,7 @@ import { fingerprint, McpError } from "./security.js"
import { readRouteCommunityContext, routeContextAccess, type RouteContextOptions } from "./routing-community-context.js" import { readRouteCommunityContext, routeContextAccess, type RouteContextOptions } from "./routing-community-context.js"
import type { add } from "./actions.js" import type { add } from "./actions.js"
import { readControlPathContext } from "./control-path-context.js" import { readControlPathContext } from "./control-path-context.js"
import { isCommunityPathEvidence, PATH_PROOF_VERSION, pathProofPurposeSchema, type PathProofPurpose } from "./path-proof-purpose.js" import { COMMUNITY_PATH_MAX_AGE_MS, isCommunityPathEvidence, PATH_PROOF_VERSION, pathProofPurposeSchema, type PathProofPurpose } from "./path-proof-purpose.js"
export function inspectControlPing(rows: Parameters<typeof parsePing>[0]) { export function inspectControlPing(rows: Parameters<typeof parsePing>[0]) {
const parsed = parsePing(rows) const parsed = parsePing(rows)
@@ -107,10 +107,10 @@ export function registerPathActions(register: typeof add) {
export async function requireRecentPath(profile: RouteAiProfile, candidateId: string, connectionId: string, options: RouteContextOptions = {}) { export async function requireRecentPath(profile: RouteAiProfile, candidateId: string, connectionId: string, options: RouteContextOptions = {}) {
// Compare the database timestamp with the database clock. The signed // Compare the database timestamp with the database clock. The signed
// observedAt below is independently checked against the application clock. // observedAt below is independently checked against the application clock.
const { rows: [row] } = await pool.query("SELECT id,status,created_at,evidence,created_at BETWEEN clock_timestamp()-interval '90 seconds' AND clock_timestamp() AS database_fresh FROM mcp_path_checks WHERE connection_id=$1 AND profile_hash=$2 AND candidate_id=$3 AND evidence->>'proofPurpose'='community' AND evidence->>'proofVersion'='2' ORDER BY created_at DESC,id DESC LIMIT 1", [connectionId, fingerprint(profile), candidateId]) const { rows: [row] } = await pool.query("SELECT id,status,created_at,evidence,created_at BETWEEN clock_timestamp()-$4::int * interval '1 millisecond' AND clock_timestamp() AS database_fresh FROM mcp_path_checks WHERE connection_id=$1 AND profile_hash=$2 AND candidate_id=$3 AND evidence->>'proofPurpose'='community' AND evidence->>'proofVersion'='2' ORDER BY created_at DESC,id DESC LIMIT 1", [connectionId, fingerprint(profile), candidateId, COMMUNITY_PATH_MAX_AGE_MS])
if (!row || row.status !== "passed" || row.database_fresh !== true) throw new McpError("MEASUREMENT_UNKNOWN", "Нет свежего подтверждения контрольного пути", 409) if (!row || row.status !== "passed" || row.database_fresh !== true) throw new McpError("MEASUREMENT_UNKNOWN", "Нет свежего подтверждения контрольного пути", 409)
const { proof, ...evidence } = row.evidence ?? {} const { proof, ...evidence } = row.evidence ?? {}
if (!isCommunityPathEvidence(evidence) || evidence.id !== row.id || evidence.connectionId !== connectionId || evidence.profileHash !== fingerprint(profile) || evidence.candidateId !== candidateId || evidence.status !== "passed" || !Array.isArray(evidence.sourceProofs) || !evidence.sourceProofs.length || !Number.isFinite(Date.parse(evidence.observedAt)) || Date.now() - Date.parse(evidence.observedAt) > 90000 || Date.parse(evidence.observedAt) > Date.now()) throw new McpError("MEASUREMENT_UNKNOWN", "Подтверждение пути не связано с Community purpose, источником и контекстом", 409) if (!isCommunityPathEvidence(evidence) || evidence.id !== row.id || evidence.connectionId !== connectionId || evidence.profileHash !== fingerprint(profile) || evidence.candidateId !== candidateId || evidence.status !== "passed" || !Array.isArray(evidence.sourceProofs) || !evidence.sourceProofs.length || !Number.isFinite(Date.parse(evidence.observedAt)) || Date.now() - Date.parse(evidence.observedAt) > COMMUNITY_PATH_MAX_AGE_MS || Date.parse(evidence.observedAt) > Date.now()) throw new McpError("MEASUREMENT_UNKNOWN", "Подтверждение пути не связано с Community purpose, источником и контекстом", 409)
const current = await readRouteCommunityContext(profile, { ...options, selectedCandidateId: candidateId, expectedSources: evidence.sourceProofs }) const current = await readRouteCommunityContext(profile, { ...options, selectedCandidateId: candidateId, expectedSources: evidence.sourceProofs })
if (!current.verifyEvidence(evidence, proof)) throw new McpError("MEASUREMENT_UNKNOWN", "Зависимости пути или сохранённое подтверждение изменились", 409) if (!current.verifyEvidence(evidence, proof)) throw new McpError("MEASUREMENT_UNKNOWN", "Зависимости пути или сохранённое подтверждение изменились", 409)
return row.id as string return row.id as string
@@ -5,6 +5,14 @@ import { preparedPlanExpiry } from "./plan-expiry.js"
const iso = (milliseconds: number) => new Date(Date.now() + milliseconds).toISOString() const iso = (milliseconds: number) => new Date(Date.now() + milliseconds).toISOString()
const poll = (validUntil?: string) => preparedPlanExpiry("servers.poll", "servers", { validUntil }) const poll = (validUntil?: string) => preparedPlanExpiry("servers.poll", "servers", { validUntil })
test("Community honors sealed validity but keeps the ordinary lifetime without a bound", () => {
const bound = iso(90000)
assert.equal(preparedPlanExpiry("routing.community.switch", "network", { validUntil: bound })?.toISOString(), bound)
assert.equal(preparedPlanExpiry("routing.community.switch", "network", { validUntil: iso(-1000) }), null)
const before = Date.now(), manual = preparedPlanExpiry("routing.community.switch", "network", {})!.getTime()
assert(manual >= before + 300000 && manual <= Date.now() + 300000)
})
test("Poll uses its existing sealed five-minute validity as the nominal window", () => { test("Poll uses its existing sealed five-minute validity as the nominal window", () => {
const bound = iso(300000) const bound = iso(300000)
assert.equal(poll(bound)?.toISOString(), bound) assert.equal(poll(bound)?.toISOString(), bound)
+5 -3
View File
@@ -3,11 +3,13 @@ import type { ChangeEvidence } from "./action-types.js"
/** Internal value calculation only; evidence comes from action.prepare, never /** Internal value calculation only; evidence comes from action.prepare, never
* caller JSON. Generic plans may outlive their preparation access credential. * caller JSON. Generic plans may outlive their preparation access credential.
* A missing result makes the runtime refuse Poll before storing a new plan. */ * A missing result refuses a time-bound plan before storage. */
export function preparedPlanExpiry(operationId: string, section: string, evidence: Pick<ChangeEvidence, "validUntil">): Date | null { export function preparedPlanExpiry(operationId: string, section: string, evidence: Pick<ChangeEvidence, "validUntil">): Date | null {
const now = Date.now(), ordinaryExpiry = now + (section === "network" ? 300000 : 900000) const now = Date.now(), ordinaryExpiry = now + (section === "network" ? 300000 : 900000)
if (operationId !== "servers.poll") return new Date(ordinaryExpiry) if (operationId !== "servers.poll" && operationId !== "routing.community.switch") return new Date(ordinaryExpiry)
const validity = z.string().datetime({ offset: true }).safeParse(evidence.validUntil) const validUntil = evidence.validUntil
if (operationId === "routing.community.switch" && validUntil === undefined) return new Date(ordinaryExpiry)
const validity = z.string().datetime({ offset: true }).safeParse(validUntil)
const bound = validity.success ? Date.parse(validity.data) : NaN const bound = validity.success ? Date.parse(validity.data) : NaN
if (!Number.isFinite(bound) || bound <= now) return null if (!Number.isFinite(bound) || bound <= now) return null
return new Date(Math.min(ordinaryExpiry, bound)) return new Date(Math.min(ordinaryExpiry, bound))
+47 -1
View File
@@ -21,7 +21,7 @@ test("failed/malformed/redirected and stalled protocol probes are bounded and ne
try { try {
for (const state of ["malformed", "redirect", "large", "announced", "stalled"]) { for (const state of ["malformed", "redirect", "large", "announced", "stalled"]) {
mode = state mode = state
const started = performance.now(), result = await runMcpProtocolProbe(endpoint, "private-readiness-fixture", 300) const started = performance.now(), result = await runMcpProtocolProbe(endpoint, "private-readiness-fixture", { timeoutMs: 300 })
assert.equal(result.status, "failed", state) assert.equal(result.status, "failed", state)
assert.ok(performance.now() - started < 3000, state) assert.ok(performance.now() - started < 3000, state)
assert.ok(!JSON.stringify(result).includes("private-readiness-fixture")) assert.ok(!JSON.stringify(result).includes("private-readiness-fixture"))
@@ -29,3 +29,49 @@ test("failed/malformed/redirected and stalled protocol probes are bounded and ne
assert.equal(forwarded, 0); assert.ok(requests >= 5) assert.equal(forwarded, 0); assert.ok(requests >= 5)
} finally { server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve())) } } finally { server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve())) }
}) })
test("capabilities readiness refuses widened scope and checks the tool result", async () => {
const capability = { id: "capabilities.read", toolName: "mm_capabilities_get" }
const tool = { name: capability.toolName, inputSchema: { type: "object" } }
let mode = "extra-tool", calls = 0
const server = createServer(async (req, res) => {
if (req.method !== "POST") { res.writeHead(405).end(); return }
const chunks: Buffer[] = []
for await (const chunk of req) chunks.push(chunk)
const { id, method, params } = JSON.parse(Buffer.concat(chunks).toString())
if (id === undefined) { res.writeHead(202).end(); return }
let result: unknown
if (method === "initialize") result = { protocolVersion: params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "fixture", version: "1.0.0" } }
if (method === "tools/list") result = { tools: mode === "extra-tool" ? [tool, { ...tool, name: "mm_servers_list" }] : [tool] }
if (method === "tools/call") {
calls++
assert.equal(params.name, capability.toolName)
if (mode === "timeout") { res.writeHead(200, { "content-type": "application/json" }).write("{"); return }
const operations = mode === "extra-operation" ? [capability, { id: "servers.list" }] : [capability]
result = { content: [], isError: mode === "tool-error", structuredContent: { result: { schemaVersion: 1, operations } } }
}
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ jsonrpc: "2.0", id, result }))
})
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve))
const endpoint = new URL(`http://127.0.0.1:${(server.address() as { port: number }).port}/mcp`)
const probe = () => runMcpProtocolProbe(endpoint, "private-readiness-fixture", { timeoutMs: 400, readCapabilities: true })
try {
const widened = await probe()
assert.equal(widened.reason, "SCOPE_NOT_ENFORCED")
assert.equal(widened.capabilitiesRead, "skipped")
assert.equal(calls, 0)
for (const state of ["tool-error", "extra-operation", "timeout"]) {
mode = state
const result = await probe()
assert.equal(result.status, "failed", state)
assert.equal(result.capabilitiesRead, "failed", state)
assert.equal(result.reason, state === "timeout" ? "TIMEOUT" : "CAPABILITIES_READ_FAILED", state)
}
mode = "valid"
const valid = await probe()
assert.equal(valid.status, "passed")
assert.equal(valid.capabilitiesRead, "passed")
assert.equal(valid.clientCredentialsVerified, false)
assert.equal(calls, 4)
} finally { server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve())) }
})
+28 -12
View File
@@ -9,15 +9,19 @@ export interface ProtocolProbe {
status: "passed" | "failed" | "skipped"; status: "passed" | "failed" | "skipped";
elapsedMs: number; elapsedMs: number;
reason?: string; reason?: string;
credentialType: "temporary-no-operations"; credentialType: "temporary-no-operations" | "temporary-capabilities-read";
capabilitiesRead: "passed" | "failed" | "skipped";
clientCredentialsVerified: false; clientCredentialsVerified: false;
} }
/** A temporary access credential may negotiate metadata, but its empty grant const capabilitiesOperation = "capabilities.read"
* permits no tools. The ordinary connection credentials are never recovered. */ const capabilitiesTool = "mm_capabilities_get"
export async function withMcpProbeCredential<T>(connectionId: string, actor: AuthUser, work: (token: string) => Promise<T>): Promise<T> {
/** The probe may read its own narrowed capabilities when the connection grants
* that operation. The ordinary connection credentials are never recovered. */
export async function withMcpProbeCredential<T>(connectionId: string, actor: AuthUser, work: (token: string, readCapabilities: boolean) => Promise<T>): Promise<T> {
const credentialId = randomUUID(), grantId = randomUUID(), token = randomToken("access") const credentialId = randomUUID(), grantId = randomUUID(), token = randomToken("access")
await transaction(async client => { const readCapabilities = await transaction(async client => {
await client.query("SELECT id FROM mcp_connections WHERE id=$1 FOR UPDATE", [connectionId]) await client.query("SELECT id FROM mcp_connections WHERE id=$1 FOR UPDATE", [connectionId])
const connection = await getConnection(connectionId, client) const connection = await getConnection(connectionId, client)
assertConnectionVisible(connection, actor) assertConnectionVisible(connection, actor)
@@ -27,14 +31,16 @@ export async function withMcpProbeCredential<T>(connectionId: string, actor: Aut
const expired = (await client.query("DELETE FROM mcp_credentials WHERE connection_id=$1 AND client_id='mcp-readiness-probe' AND expires_at<=now() RETURNING id", [connectionId])).rows.map(row => row.id) const expired = (await client.query("DELETE FROM mcp_credentials WHERE connection_id=$1 AND client_id='mcp-readiness-probe' AND expires_at<=now() RETURNING id", [connectionId])).rows.map(row => row.id)
if (expired.length) await client.query("DELETE FROM mcp_grants WHERE connection_id=$1 AND payload->>'credentialId'=ANY($2::text[])", [connectionId, expired]) if (expired.length) await client.query("DELETE FROM mcp_grants WHERE connection_id=$1 AND payload->>'credentialId'=ANY($2::text[])", [connectionId, expired])
const expiresAt = new Date(Math.min(Date.now() + 20000, Date.parse(connection.policy.expiresAt), connection.owner_valid_until.getTime())).toISOString() const expiresAt = new Date(Math.min(Date.now() + 20000, Date.parse(connection.policy.expiresAt), connection.owner_valid_until.getTime())).toISOString()
const grant = mcpTemporaryGrantSchema.parse({ id: grantId, startsAt: new Date(Date.now() - 1000).toISOString(), expiresAt, scope: { serverIds: [], operationIds: [], allowGlobal: false } }) const allowed = connection.policy.scope.operationIds.includes(capabilitiesOperation)
const grant = mcpTemporaryGrantSchema.parse({ id: grantId, startsAt: new Date(Date.now() - 1000).toISOString(), expiresAt, scope: { serverIds: [], operationIds: allowed ? [capabilitiesOperation] : [], allowGlobal: false } })
await client.query("INSERT INTO mcp_credentials(id,connection_id,digest,kind,expires_at,audience,client_id) VALUES($1,$2,$3,'access',$4,$5,'mcp-readiness-probe')", [credentialId, connectionId, tokenDigest(token), expiresAt, canonicalMcpUrl().href]) await client.query("INSERT INTO mcp_credentials(id,connection_id,digest,kind,expires_at,audience,client_id) VALUES($1,$2,$3,'access',$4,$5,'mcp-readiness-probe')", [credentialId, connectionId, tokenDigest(token), expiresAt, canonicalMcpUrl().href])
await client.query("INSERT INTO mcp_grants(id,connection_id,payload) VALUES($1,$2,$3::jsonb)", [grantId, connectionId, JSON.stringify({ ...grant, credentialId })]) await client.query("INSERT INTO mcp_grants(id,connection_id,payload) VALUES($1,$2,$3::jsonb)", [grantId, connectionId, JSON.stringify({ ...grant, credentialId })])
await validatePrincipal({ connectionId, credentialId, credentialExpiresAt: expiresAt }, client) await validatePrincipal({ connectionId, credentialId, credentialExpiresAt: expiresAt }, client)
await audit(client, actor.id, "connection.handshake.start", connectionId, { credentialId, expiresAt }) await audit(client, actor.id, "connection.handshake.start", connectionId, { credentialId, expiresAt })
return allowed
}) })
let completed = false let completed = false
try { const result = await work(token); completed = true; return result } try { const result = await work(token, readCapabilities); completed = true; return result }
finally { finally {
await transaction(async client => { await transaction(async client => {
await client.query("DELETE FROM mcp_grants WHERE id=$1 AND connection_id=$2", [grantId, connectionId]) await client.query("DELETE FROM mcp_grants WHERE id=$1 AND connection_id=$2", [grantId, connectionId])
@@ -45,11 +51,12 @@ export async function withMcpProbeCredential<T>(connectionId: string, actor: Aut
} }
/** One deadline covers negotiation, metadata and HTTP response bodies. */ /** One deadline covers negotiation, metadata and HTTP response bodies. */
export async function runMcpProtocolProbe(endpoint: URL, token: string, timeoutMs = 8000): Promise<ProtocolProbe> { export async function runMcpProtocolProbe(endpoint: URL, token: string, { timeoutMs = 8000, readCapabilities = false } = {}): Promise<ProtocolProbe> {
const started = performance.now(), controller = new AbortController() const started = performance.now(), controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs) const timer = setTimeout(() => controller.abort(), timeoutMs)
const client = new Client({ name: "MikrotikManager readiness", version: "1.0.0" }) const client = new Client({ name: "MikrotikManager readiness", version: "1.0.0" })
const result = (status: ProtocolProbe["status"], reason?: string): ProtocolProbe => ({ status, reason, elapsedMs: Math.round(performance.now() - started), credentialType: "temporary-no-operations", clientCredentialsVerified: false }) let capabilitiesRead: ProtocolProbe["capabilitiesRead"] = "skipped"
const result = (status: ProtocolProbe["status"], reason?: string): ProtocolProbe => ({ status, reason, elapsedMs: Math.round(performance.now() - started), credentialType: readCapabilities ? "temporary-capabilities-read" : "temporary-no-operations", capabilitiesRead, clientCredentialsVerified: false })
try { try {
const transport = new StreamableHTTPClientTransport(endpoint, { requestInit: { headers: { Authorization: `Bearer ${token}` }, redirect: "error" }, redirectPolicy: "follow", const transport = new StreamableHTTPClientTransport(endpoint, { requestInit: { headers: { Authorization: `Bearer ${token}` }, redirect: "error" }, redirectPolicy: "follow",
fetch: async (url, init) => { fetch: async (url, init) => {
@@ -67,13 +74,22 @@ export async function runMcpProtocolProbe(endpoint: URL, token: string, timeoutM
} }) } })
await client.connect(transport, { timeout: timeoutMs }) await client.connect(transport, { timeout: timeoutMs })
const tools = await client.listTools({}, { timeout: timeoutMs }) const tools = await client.listTools({}, { timeout: timeoutMs })
if (tools.tools.length !== 0 || tools.nextCursor) return result("failed", "SCOPE_NOT_ENFORCED") const expectedTools = readCapabilities ? [capabilitiesTool] : []
if (tools.nextCursor || tools.tools.length !== expectedTools.length || tools.tools.some((tool, index) => tool.name !== expectedTools[index])) return result("failed", "SCOPE_NOT_ENFORCED")
if (readCapabilities) {
capabilitiesRead = "failed"
const call = await client.callTool({ name: capabilitiesTool, arguments: {} }, { timeout: timeoutMs })
const output = (call.structuredContent as { result?: { schemaVersion?: unknown; operations?: unknown } } | undefined)?.result
const operations = output?.operations
if (call.isError || output?.schemaVersion !== 1 || !Array.isArray(operations) || operations.length !== 1 || operations[0]?.id !== capabilitiesOperation || operations[0]?.toolName !== capabilitiesTool) return result("failed", "CAPABILITIES_READ_FAILED")
capabilitiesRead = "passed"
}
return result("passed") return result("passed")
} catch { return result("failed", controller.signal.aborted ? "TIMEOUT" : "TRANSPORT_FAILED") } } catch { return result("failed", controller.signal.aborted ? "TIMEOUT" : capabilitiesRead === "failed" ? "CAPABILITIES_READ_FAILED" : "TRANSPORT_FAILED") }
finally { clearTimeout(timer); controller.abort(); await client.close().catch(() => {}) } finally { clearTimeout(timer); controller.abort(); await client.close().catch(() => {}) }
} }
export async function checkMcpProtocol(connectionId: string, actor: AuthUser) { export async function checkMcpProtocol(connectionId: string, actor: AuthUser) {
const endpoint = canonicalMcpUrl() const endpoint = canonicalMcpUrl()
return withMcpProbeCredential(connectionId, actor, token => runMcpProtocolProbe(endpoint, token)) return withMcpProbeCredential(connectionId, actor, (token, readCapabilities) => runMcpProtocolProbe(endpoint, token, { readCapabilities }))
} }
+6 -4
View File
@@ -6,13 +6,14 @@ import { decryptSecret, fingerprint, McpError } from "./security.js"
import { effectiveMcpConnection } from "./store.js" import { effectiveMcpConnection } from "./store.js"
import { findMcpService } from "./service-registry.js" import { findMcpService } from "./service-registry.js"
import { findMcpOperation } from "./registry.js" import { findMcpOperation } from "./registry.js"
import { readRosSnapshot, resourceById, routerClient, validateRosStep, splitOspfPassiveUpdate, ROS_SNAPSHOT_VERSION, type RosSnapshot, type RosStep } from "./resources.js" import { readRosSnapshot, resourceById, routerClient, validateRosStep, splitOspfPassiveUpdate, rosSnapshotVersion, type RosSnapshot, type RosStep } from "./resources.js"
import { verifyRosStepSnapshots } from "./ros-step-verification.js" import { verifyRosStepSnapshots } from "./ros-step-verification.js"
import type { add } from "./actions.js" import type { add } from "./actions.js"
import type { ActionContext, ChangeEvidence, ChangeStep } from "./action-types.js" import type { ActionContext, ChangeEvidence, ChangeStep } from "./action-types.js"
import { loadScopedRecoverySource } from "./routing-community-scoped-recovery-source.js" import { loadScopedRecoverySource } from "./routing-community-scoped-recovery-source.js"
import { prepareScopedRecovery, registerScopedRecoveryService } from "./routing-community-scoped-recovery.js" import { prepareScopedRecovery, registerScopedRecoveryService } from "./routing-community-scoped-recovery.js"
import { reconcileVrfProductionStep } from "../../services/vrf-production-execution.js" import { reconcileVrfProductionStep } from "../../services/vrf-production-execution.js"
import { needsBridgeVlanRecovery, prepareBridgeVlanRecovery } from "./bridge-vlan-recovery.js"
async function source(operationId: string, context: ActionContext, allowServices = false) { async function source(operationId: string, context: ActionContext, allowServices = false) {
const { rows: [row] } = await pool.query("SELECT o.*,p.operation_id AS source_action,p.operation_version AS source_version,p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id WHERE o.id=$1 AND o.connection_id=$2", [operationId, context.connection.id]) const { rows: [row] } = await pool.query("SELECT o.*,p.operation_id AS source_action,p.operation_version AS source_version,p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id WHERE o.id=$1 AND o.connection_id=$2", [operationId, context.connection.id])
@@ -86,7 +87,7 @@ export function registerRecoveryActions(register: typeof add) {
}, },
}) })
register("operations.recover", "mm_operation_recovery_plan", "Подготовить отдельный обратный план сетевой операции", "network", "mm:network:write", "write", "servers", z.object({ operationId: z.string().uuid() }).strict(), { register("operations.recover", "mm_operation_recovery_plan", "Подготовить отдельный обратный план сетевой операции", "network", "mm:network:write", "write", "servers", z.object({ operationId: z.string().uuid() }).strict(), {
version: 7, version: 8,
target: async (input, context) => { target: async (input, context) => {
const { evidence } = await source(String(input.operationId), context, true) const { evidence } = await source(String(input.operationId), context, true)
if (evidence.steps.some(step => step.kind === "ros" && step.step.resource === "vrf")) throw new McpError("UNSUPPORTED_CAPABILITY", "VRF не имеет проверенной обратной ревизии; используйте read-only сверку и отдельный approved план", 409) if (evidence.steps.some(step => step.kind === "ros" && step.step.resource === "vrf")) throw new McpError("UNSUPPORTED_CAPABILITY", "VRF не имеет проверенной обратной ревизии; используйте read-only сверку и отдельный approved план", 409)
@@ -106,6 +107,7 @@ export function registerRecoveryActions(register: typeof add) {
const checkpoints = (await pool.query("SELECT step_index,before_sealed,after_sealed FROM mcp_operation_checkpoints WHERE operation_id=$1 ORDER BY step_index DESC", [row.id])).rows const checkpoints = (await pool.query("SELECT step_index,before_sealed,after_sealed FROM mcp_operation_checkpoints WHERE operation_id=$1 ORDER BY step_index DESC", [row.id])).rows
const intended = (await pool.query("SELECT step_index,state,result FROM mcp_operation_steps WHERE operation_id=$1 ORDER BY step_index DESC", [row.id])).rows const intended = (await pool.query("SELECT step_index,state,result FROM mcp_operation_steps WHERE operation_id=$1 ORDER BY step_index DESC", [row.id])).rows
if (!intended.length || intended.some(step => step.state !== "verified") || intended.length !== checkpoints.length || checkpoints.some(checkpoint => !checkpoint.after_sealed)) throw new McpError("STATE_CONFLICT", "Нет полного набора подтверждённых снимков; требуется ручная сверка", 409) if (!intended.length || intended.some(step => step.state !== "verified") || intended.length !== checkpoints.length || checkpoints.some(checkpoint => !checkpoint.after_sealed)) throw new McpError("STATE_CONFLICT", "Нет полного набора подтверждённых снимков; требуется ручная сверка", 409)
if (needsBridgeVlanRecovery(row.source_action, evidence)) return prepareBridgeVlanRecovery({ operationId: row.id, evidence, checkpoints, intended }, context)
const snapshots = new Map<string, RosSnapshot>(), steps: ChangeStep[] = [], objects: ChangeEvidence["objects"] = [...evidence.objects] const snapshots = new Map<string, RosSnapshot>(), steps: ChangeStep[] = [], objects: ChangeEvidence["objects"] = [...evidence.objects]
for (const checkpoint of checkpoints) { for (const checkpoint of checkpoints) {
const original = evidence.steps[checkpoint.step_index] const original = evidence.steps[checkpoint.step_index]
@@ -113,7 +115,7 @@ export function registerRecoveryActions(register: typeof add) {
const step = original.step, resource = resourceById(step.resource) const step = original.step, resource = resourceById(step.resource)
if (!context.connection.policy.scope.serverIds.includes(step.serverId)) throw new McpError("SERVER_DENIED", "Зависимость вне допуска") if (!context.connection.policy.scope.serverIds.includes(step.serverId)) throw new McpError("SERVER_DENIED", "Зависимость вне допуска")
const before = JSON.parse(await decryptSecret(checkpoint.before_sealed)) as RosSnapshot, after = JSON.parse(await decryptSecret(checkpoint.after_sealed)) as RosSnapshot const before = JSON.parse(await decryptSecret(checkpoint.before_sealed)) as RosSnapshot, after = JSON.parse(await decryptSecret(checkpoint.after_sealed)) as RosSnapshot
if (before.projectionVersion !== ROS_SNAPSHOT_VERSION || after.projectionVersion !== ROS_SNAPSHOT_VERSION || !before.sourceProof || before.sourceProof !== after.sourceProof || before.fingerprint !== fingerprint(before.rows) || after.fingerprint !== fingerprint(after.rows) || !verifyRosStepSnapshots(step, intended.find(item => item.step_index === checkpoint.step_index)?.result, before, after)) throw new McpError("STATE_CONFLICT", "Исходный переход каталога или источник не подтверждён; обратный план запрещён", 409) if (before.projectionVersion !== rosSnapshotVersion(step.resource) || after.projectionVersion !== rosSnapshotVersion(step.resource) || !before.sourceProof || before.sourceProof !== after.sourceProof || before.fingerprint !== fingerprint(before.rows) || after.fingerprint !== fingerprint(after.rows) || !verifyRosStepSnapshots(step, intended.find(item => item.step_index === checkpoint.step_index)?.result, before, after)) throw new McpError("STATE_CONFLICT", "Исходный переход каталога или источник не подтверждён; обратный план запрещён", 409)
const key = `${step.serverId}:${step.resource}` const key = `${step.serverId}:${step.resource}`
if (!snapshots.has(key)) { if (!snapshots.has(key)) {
const current = await readRosSnapshot(step.serverId, step.resource, undefined, after.sourceProof) const current = await readRosSnapshot(step.serverId, step.resource, undefined, after.sourceProof)
@@ -143,7 +145,7 @@ export function registerRecoveryActions(register: typeof add) {
const key = `${dependency.serverId}:${dependency.resource}` const key = `${dependency.serverId}:${dependency.resource}`
if (snapshots.has(key)) continue if (snapshots.has(key)) continue
const current = await readRosSnapshot(dependency.serverId, dependency.resource, undefined, dependency.sourceProof) const current = await readRosSnapshot(dependency.serverId, dependency.resource, undefined, dependency.sourceProof)
if (current.fingerprint !== dependency.fingerprint || current.sourceProof !== dependency.sourceProof || dependency.projectionVersion !== ROS_SNAPSHOT_VERSION) throw new McpError("STATE_CONFLICT", "Зависимость или источник исходного плана изменились", 409) if (current.fingerprint !== dependency.fingerprint || current.sourceProof !== dependency.sourceProof || dependency.projectionVersion !== rosSnapshotVersion(dependency.resource)) throw new McpError("STATE_CONFLICT", "Зависимость или источник исходного плана изменились", 409)
snapshots.set(key, current) snapshots.set(key, current)
} }
objects.push(...[...new Set([...snapshots.values()].map(snapshot => snapshot.serverId))].map(serverId => ({ kind: "server", id: String(serverId), serverId }))) objects.push(...[...new Set([...snapshots.values()].map(snapshot => snapshot.serverId))].map(serverId => ({ kind: "server", id: String(serverId), serverId })))
+22 -6
View File
@@ -5,14 +5,19 @@ import { eq } from "drizzle-orm"
import { db } from "../../db/index.js" import { db } from "../../db/index.js"
import { servers } from "../../db/schema.js" import { servers } from "../../db/schema.js"
import { MikrotikClient } from "../../services/mikrotik.js" import { MikrotikClient } from "../../services/mikrotik.js"
import { readRouterOsSnapshotGet } from "../../services/routeros-snapshot-read.js"
import { McpError, fingerprint, privateSecretFingerprint } from "./security.js" import { McpError, fingerprint, privateSecretFingerprint } from "./security.js"
import { readOspfEffectiveDisabled } from "../../services/ospf-effective-disabled.js" import { readOspfEffectiveDisabled } from "../../services/ospf-effective-disabled.js"
import { greNativeFieldsSchema } from "../../services/gre-mutation-input.js" import { greNativeFieldsSchema } from "../../services/gre-mutation-input.js"
import { canonicalVrfMembers } from "../../services/vrf-members.js" import { canonicalVrfMembers } from "../../services/vrf-members.js"
import { canonicalBridgeVlanValue } from "../../services/bridge-vlan-values.js"
const text = z.string().max(1000) const text = z.string().max(1000)
const name = z.string().trim().min(1).max(100) const name = z.string().trim().min(1).max(100)
const bool = z.boolean() const bool = z.boolean()
const vlanIds = z.string().min(1).max(16384).refine(value => {
try { canonicalBridgeVlanValue("vlan-ids", value); return true } catch { return false }
}, "Требуется непересекающийся список VLAN ID/диапазонов 1..4094")
const integer = (min: number, max: number) => z.number().int().min(min).max(max) const integer = (min: number, max: number) => z.number().int().min(min).max(max)
const common = { name: name.optional(), comment: text.optional(), disabled: bool.optional() } const common = { name: name.optional(), comment: text.optional(), disabled: bool.optional() }
const ipv4 = name.refine(value => isIP(value) === 4, "Требуется IPv4 router/area ID") const ipv4 = name.refine(value => isIP(value) === 4, "Требуется IPv4 router/area ID")
@@ -31,13 +36,15 @@ const route = {
export interface RosResourceSpec { export interface RosResourceSpec {
id: string; path: string; title: string; permission: string; fields: z.ZodObject; id: string; path: string; title: string; permission: string; fields: z.ZodObject;
create: boolean; remove: boolean; requiredOnCreate?: string[]; readOnly?: boolean create: boolean; remove: boolean; requiredOnCreate?: string[]; readOnly?: boolean
/** Native configuration sealed for drift checks, never accepted as writes. */
observeFields?: readonly string[]
} }
export const rosResources: RosResourceSpec[] = [ export const rosResources: RosResourceSpec[] = [
{ id: "routing_filter", path: "/routing/filter/rule", title: "Фильтры маршрутизации", permission: "mm:filters", fields: z.object({ chain: text.optional(), rule: text.optional(), comment: text.optional(), disabled: bool.optional() }).strict(), create: false, remove: false, readOnly: true }, { id: "routing_filter", path: "/routing/filter/rule", title: "Фильтры маршрутизации", permission: "mm:filters", fields: z.object({ chain: text.optional(), rule: text.optional(), comment: text.optional(), disabled: bool.optional() }).strict(), create: false, remove: false, readOnly: true },
{ id: "ethernet", path: "/interface/ethernet", title: "Ethernet", permission: "mm:network", fields: z.object({ ...common, mtu: integer(68, 65535).optional(), "auto-negotiation": bool.optional() }).strict(), create: false, remove: false }, { id: "ethernet", path: "/interface/ethernet", title: "Ethernet", permission: "mm:network", fields: z.object({ ...common, mtu: integer(68, 65535).optional(), "auto-negotiation": bool.optional() }).strict(), create: false, remove: false },
{ id: "bridge", path: "/interface/bridge", title: "Bridge", permission: "mm:network", fields: z.object({ ...common, mtu: integer(68, 65535).optional(), "vlan-filtering": bool.optional(), pvid: integer(1, 4094).optional(), "protocol-mode": z.enum(["none", "stp", "rstp", "mstp"]).optional() }).strict(), create: true, remove: true, requiredOnCreate: ["name"] }, { id: "bridge", path: "/interface/bridge", title: "Bridge", permission: "mm:network", fields: z.object({ ...common, mtu: integer(68, 65535).optional(), "vlan-filtering": bool.optional(), pvid: integer(1, 4094).optional(), "frame-types": z.enum(["admit-all", "admit-only-vlan-tagged", "admit-only-untagged-and-priority-tagged"]).optional(), "ingress-filtering": bool.optional(), "protocol-mode": z.enum(["none", "stp", "rstp", "mstp"]).optional() }).strict(), observeFields: ["mvrp"], create: true, remove: true, requiredOnCreate: ["name"] },
{ id: "bridge_port", path: "/interface/bridge/port", title: "Порты bridge", permission: "mm:network", fields: z.object({ bridge: name.optional(), interface: name.optional(), pvid: integer(1, 4094).optional(), "ingress-filtering": bool.optional(), "frame-types": z.enum(["admit-all", "admit-only-vlan-tagged", "admit-only-untagged-and-priority-tagged"]).optional(), disabled: bool.optional(), comment: text.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["bridge", "interface"] }, { id: "bridge_port", path: "/interface/bridge/port", title: "Порты bridge", permission: "mm:network", fields: z.object({ bridge: name.optional(), interface: name.optional(), pvid: integer(1, 4094).optional(), "ingress-filtering": bool.optional(), "frame-types": z.enum(["admit-all", "admit-only-vlan-tagged", "admit-only-untagged-and-priority-tagged"]).optional(), disabled: bool.optional(), comment: text.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["bridge", "interface"] },
{ id: "bridge_vlan", path: "/interface/bridge/vlan", title: "Bridge VLAN membership", permission: "mm:network", fields: z.object({ bridge: name.optional(), "vlan-ids": name.optional(), tagged: text.optional(), untagged: text.optional(), disabled: bool.optional(), comment: text.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["bridge", "vlan-ids"] }, { id: "bridge_vlan", path: "/interface/bridge/vlan", title: "Bridge VLAN membership", permission: "mm:network", fields: z.object({ bridge: name.optional(), "vlan-ids": vlanIds.optional(), tagged: text.optional(), untagged: text.optional(), disabled: bool.optional(), comment: text.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["bridge", "vlan-ids"] },
{ id: "vlan", path: "/interface/vlan", title: "VLAN", permission: "mm:network", fields: z.object({ ...common, interface: name.optional(), "vlan-id": integer(1, 4094).optional(), mtu: integer(68, 65535).optional(), "use-service-tag": bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["name", "interface", "vlan-id"] }, { id: "vlan", path: "/interface/vlan", title: "VLAN", permission: "mm:network", fields: z.object({ ...common, interface: name.optional(), "vlan-id": integer(1, 4094).optional(), mtu: integer(68, 65535).optional(), "use-service-tag": bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["name", "interface", "vlan-id"] },
{ id: "ipv4_address", path: "/ip/address", title: "IPv4-адреса", permission: "mm:network", fields: z.object({ address: name.optional(), interface: name.optional(), network: name.optional(), comment: text.optional(), disabled: bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["address", "interface"] }, { id: "ipv4_address", path: "/ip/address", title: "IPv4-адреса", permission: "mm:network", fields: z.object({ address: name.optional(), interface: name.optional(), network: name.optional(), comment: text.optional(), disabled: bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["address", "interface"] },
{ id: "ipv6_address", path: "/ipv6/address", title: "IPv6-адреса", permission: "mm:network", fields: z.object({ address: name.optional(), interface: name.optional(), advertise: bool.optional(), comment: text.optional(), disabled: bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["address", "interface"] }, { id: "ipv6_address", path: "/ipv6/address", title: "IPv6-адреса", permission: "mm:network", fields: z.object({ address: name.optional(), interface: name.optional(), advertise: bool.optional(), comment: text.optional(), disabled: bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["address", "interface"] },
@@ -70,6 +77,7 @@ export const rosResources: RosResourceSpec[] = [
export type ConfigurationRow = Record<string, unknown> export type ConfigurationRow = Record<string, unknown>
export interface RosStep { serverId: number; resource: string; action: "create" | "update" | "delete"; id?: string; fields: Record<string, string>; placeBeforeId?: string; expectedCost?: number; expectedServerProof?: string } export interface RosStep { serverId: number; resource: string; action: "create" | "update" | "delete"; id?: string; fields: Record<string, string>; placeBeforeId?: string; expectedCost?: number; expectedServerProof?: string }
export const ROS_SNAPSHOT_VERSION = 3 export const ROS_SNAPSHOT_VERSION = 3
export const rosSnapshotVersion = (resource: string): number => ["bridge", "bridge_port", "bridge_vlan"].includes(resource) ? 4 : ROS_SNAPSHOT_VERSION
export interface RosSnapshot { serverId: number; resource: string; rows: ConfigurationRow[]; fingerprint: string; projectionVersion?: number; sourceProof?: string } export interface RosSnapshot { serverId: number; resource: string; rows: ConfigurationRow[]; fingerprint: string; projectionVersion?: number; sourceProof?: string }
export function resourceById(id: string): RosResourceSpec { export function resourceById(id: string): RosResourceSpec {
const resource = rosResources.find(item => item.id === id) const resource = rosResources.find(item => item.id === id)
@@ -93,13 +101,21 @@ export function configurationRows(resource: RosResourceSpec, payload: unknown):
// RouterOS creates/removes IPv6 link-local rows with interface runtime state. // RouterOS creates/removes IPv6 link-local rows with interface runtime state.
// They cannot be configured independently and must not invalidate a sealed // They cannot be configured independently and must not invalidate a sealed
// inverse plan when its own GRE is disabled. Explicit addresses remain sealed. // inverse plan when its own GRE is disabled. Explicit addresses remain sealed.
const configuration = rows.filter(row => !(resource.id === "ipv6_address" && [true, "true"].includes((row as ConfigurationRow).dynamic as boolean | string) && /^fe80:/i.test(String((row as ConfigurationRow).address)))) const configuration = rows.filter(row => !(resource.id === "ipv6_address" && [true, "true"].includes((row as ConfigurationRow).dynamic as boolean | string) && /^fe80:/i.test(String((row as ConfigurationRow).address)))
// These rows are generated from bridge/port PVID, not independently
// configurable VLAN entries. Their inputs remain sealed in bridge/ports.
&& !(resource.id === "bridge_vlan" && [true, "true", "yes"].includes((row as ConfigurationRow).dynamic as boolean | string)))
const selectedRows = configuration.map(row => { const selectedRows = configuration.map(row => {
const selected = Object.fromEntries(Object.entries(row as ConfigurationRow).filter(([key]) => [".id", "dynamic", "builtin", "default", ...fields].includes(key))) const selected = Object.fromEntries(Object.entries(row as ConfigurationRow).filter(([key]) => [".id", "dynamic", "builtin", "default", ...fields, ...(resource.observeFields ?? [])].includes(key)))
// RouterOS 7.24.5 returns an explicit empty fib flag for non-FIB tables. // RouterOS 7.24.5 returns an explicit empty fib flag for non-FIB tables.
// Normalize only this observed field; missing values must remain unknown. // Normalize only this observed field; missing values must remain unknown.
if (resource.id === "routing_table" && selected.fib === "") selected.fib = "false" if (resource.id === "routing_table" && selected.fib === "") selected.fib = "false"
if (resource.id === "vrf" && selected.interfaces !== undefined) selected.interfaces = canonicalVrfMembers(selected.interfaces) if (resource.id === "vrf" && selected.interfaces !== undefined) selected.interfaces = canonicalVrfMembers(selected.interfaces)
if (["bridge_port", "bridge_vlan"].includes(resource.id) && selected.comment === undefined) selected.comment = ""
if (resource.id === "bridge_vlan") {
try { for (const field of ["vlan-ids", "tagged", "untagged"]) selected[field] = canonicalBridgeVlanValue(field, selected[field]) }
catch { throw new McpError("STATE_CONFLICT", "Недостоверная конфигурация VLAN membership", 409) }
}
return selected return selected
}) })
// Reordering an ordered catalog changes configuration even with unchanged rows. // Reordering an ordered catalog changes configuration even with unchanged rows.
@@ -118,10 +134,10 @@ export async function readRosSnapshot(serverId: number, resourceId: string, befo
const payload = resourceId === "ospf_template" ? await client.getOspfInterfaceTemplates(checkSource) : ["ospf_instance", "ospf_area", "bgp_instance", "bgp_connection"].includes(resourceId) ? await readOspfEffectiveDisabled(await client.get<unknown>(resource.path), async id => { const payload = resourceId === "ospf_template" ? await client.getOspfInterfaceTemplates(checkSource) : ["ospf_instance", "ospf_area", "bgp_instance", "bgp_connection"].includes(resourceId) ? await readOspfEffectiveDisabled(await client.get<unknown>(resource.path), async id => {
await checkSource() await checkSource()
return client.post(resource.path + "/get", { ".id": id, "value-name": "disabled" }) return client.post(resource.path + "/get", { ".id": id, "value-name": "disabled" })
}) : await client.get<unknown>(resource.path) }) : await readRouterOsSnapshotGet<unknown>({ client, path: resource.path, assertCurrent: checkSource })
await checkSource() await checkSource()
const rows = configurationRows(resource, payload) const rows = configurationRows(resource, payload)
return { serverId, resource: resourceId, rows, fingerprint: fingerprint(rows), projectionVersion: ROS_SNAPSHOT_VERSION, sourceProof } return { serverId, resource: resourceId, rows, fingerprint: fingerprint(rows), projectionVersion: rosSnapshotVersion(resourceId), sourceProof }
} }
export async function rosSnapshotSourceProof(server: Awaited<ReturnType<typeof routerClient>>["server"]): Promise<string> { export async function rosSnapshotSourceProof(server: Awaited<ReturnType<typeof routerClient>>["server"]): Promise<string> {
return privateSecretFingerprint(fingerprint({ id: server.id, enabled: server.enabled, host: server.host, port: server.port, useSsl: server.useSsl, verifySsl: server.verifySsl, username: server.username, password: server.password, epoch: server.routingEndpointEpoch }), "mcp-ros-snapshot-source-v3") return privateSecretFingerprint(fingerprint({ id: server.id, enabled: server.enabled, host: server.host, port: server.port, useSsl: server.useSsl, verifySsl: server.verifySsl, username: server.username, password: server.password, epoch: server.routingEndpointEpoch }), "mcp-ros-snapshot-source-v3")
+2 -2
View File
@@ -1,11 +1,11 @@
import { fingerprint, McpError } from "./security.js" import { fingerprint, McpError } from "./security.js"
import { readRosSnapshot, ROS_SNAPSHOT_VERSION, type RosSnapshot, type RosStep } from "./resources.js" import { readRosSnapshot, rosSnapshotVersion, type RosSnapshot, type RosStep } from "./resources.js"
import { verifyRosStepSnapshots } from "./ros-step-verification.js" import { verifyRosStepSnapshots } from "./ros-step-verification.js"
import { acknowledgedCreateId } from "./native-create-identity.js" import { acknowledgedCreateId } from "./native-create-identity.js"
const key = (snapshot: Pick<RosSnapshot, "serverId" | "resource">) => `${snapshot.serverId}:${snapshot.resource}` const key = (snapshot: Pick<RosSnapshot, "serverId" | "resource">) => `${snapshot.serverId}:${snapshot.resource}`
function valid(snapshot: RosSnapshot): boolean { function valid(snapshot: RosSnapshot): boolean {
if (snapshot.projectionVersion !== ROS_SNAPSHOT_VERSION || typeof snapshot.sourceProof !== "string" || !/^[a-f0-9]{64}$/.test(snapshot.sourceProof) || !Array.isArray(snapshot.rows) || snapshot.fingerprint !== fingerprint(snapshot.rows)) return false if (snapshot.projectionVersion !== rosSnapshotVersion(snapshot.resource) || typeof snapshot.sourceProof !== "string" || !/^[a-f0-9]{64}$/.test(snapshot.sourceProof) || !Array.isArray(snapshot.rows) || snapshot.fingerprint !== fingerprint(snapshot.rows)) return false
if (["ip_settings", "ipv6_settings"].includes(snapshot.resource)) return snapshot.rows.length === 1 && !!snapshot.rows[0] && typeof snapshot.rows[0] === "object" && !Array.isArray(snapshot.rows[0]) if (["ip_settings", "ipv6_settings"].includes(snapshot.resource)) return snapshot.rows.length === 1 && !!snapshot.rows[0] && typeof snapshot.rows[0] === "object" && !Array.isArray(snapshot.rows[0])
const ids = snapshot.rows.map(acknowledgedCreateId) const ids = snapshot.rows.map(acknowledgedCreateId)
return ids.every(id => id !== undefined) && new Set(ids).size === ids.length return ids.every(id => id !== undefined) && new Set(ids).size === ids.length
@@ -1,5 +1,6 @@
import { isDeepStrictEqual } from "node:util" import { isDeepStrictEqual } from "node:util"
import { acknowledgedCreateId } from "./native-create-identity.js" import { acknowledgedCreateId } from "./native-create-identity.js"
import { canonicalBridgeVlanValue } from "../../services/bridge-vlan-values.js"
import { isOrderedRosResource, resourceById, type ConfigurationRow, type RosResourceSpec, type RosSnapshot, type RosStep } from "./resources.js" import { isOrderedRosResource, resourceById, type ConfigurationRow, type RosResourceSpec, type RosSnapshot, type RosStep } from "./resources.js"
function booleanValue(value: unknown): unknown { function booleanValue(value: unknown): unknown {
@@ -23,6 +24,9 @@ function requested(resource: RosResourceSpec, fields: RosStep["fields"], row: Co
const schema = resource.fields.shape[key] const schema = resource.fields.shape[key]
if (!schema || typeof value !== "string" || !Object.hasOwn(row, key) || row[key] === undefined || row[key] === null) return false if (!schema || typeof value !== "string" || !Object.hasOwn(row, key) || row[key] === undefined || row[key] === null) return false
if (schema.safeParse(true).success) return ["true", "false"].includes(value) && booleanValue(row[key]) === (value === "true") if (schema.safeParse(true).success) return ["true", "false"].includes(value) && booleanValue(row[key]) === (value === "true")
if (resource.id === "bridge_vlan" && ["vlan-ids", "tagged", "untagged"].includes(key)) {
try { return canonicalBridgeVlanValue(key, row[key]) === canonicalBridgeVlanValue(key, value) } catch { return false }
}
return String(row[key]) === value return String(row[key]) === value
}) })
} }
@@ -1,3 +1,5 @@
import { isConfirmedRoutingFibTable } from "../../services/routing-table-fib.js"
import { prepareOriginalEffectSelection } from "./routing-original-effects.js"
import type { RouteAiProfile } from "@mmapp/contracts/mcp-routing" import type { RouteAiProfile } from "@mmapp/contracts/mcp-routing"
import { pool } from "../../db/index.js" import { pool } from "../../db/index.js"
import { nativeBgpRows, bgpTemplateReferences, type NativeBgpRow } from "../../services/bgp-export-model.js" import { nativeBgpRows, bgpTemplateReferences, type NativeBgpRow } from "../../services/bgp-export-model.js"
@@ -96,7 +98,7 @@ async function readContext(profile: RouteAiProfile, options: RouteContextOptions
} }
const sourceId = profile.serverId, tables = await get(sourceId, "/routing/table"), vrfs = await get(sourceId, "/ip/vrf") const sourceId = profile.serverId, tables = await get(sourceId, "/routing/table"), vrfs = await get(sourceId, "/ip/vrf")
const table = unique(tables, profile.table) const table = unique(tables, profile.table)
if (![true, "true", "yes"].includes(table.fib as string | boolean) || [true, "true", "yes"].includes(table.disabled as string | boolean) || [true, "true", "yes"].includes(table.invalid as string | boolean)) throw new McpError("STATE_UNAVAILABLE", "FIB таблицы назначения не подтверждён", 409) if (!isConfirmedRoutingFibTable(table)) throw new McpError("STATE_UNAVAILABLE", "FIB таблицы назначения не подтверждён", 409)
await enabled(sourceId, "/ip/vrf", unique(vrfs, profile.probeVrf)) await enabled(sourceId, "/ip/vrf", unique(vrfs, profile.probeVrf))
// Keep ordered VRF membership (first matching row wins), never equate an // Keep ordered VRF membership (first matching row wins), never equate an
// arbitrary policy table with the diagnostic VRF's same-name table. // arbitrary policy table with the diagnostic VRF's same-name table.
@@ -140,7 +142,7 @@ async function readContext(profile: RouteAiProfile, options: RouteContextOptions
for (const property of ["vrf", "routing-table"]) if (!values[property] && instance) values[property] = bgpFilterName(await post(sourceId, "/routing/bgp/instance/get", { ".id": instance[".id"], "value-name": property })) for (const property of ["vrf", "routing-table"]) if (!values[property] && instance) values[property] = bgpFilterName(await post(sourceId, "/routing/bgp/instance/get", { ".id": instance[".id"], "value-name": property }))
if (!values.vrf || !values["routing-table"] || !values.afi || values["input.filter"] === null) throw new McpError("STATE_UNAVAILABLE", "Не подтверждены effective BGP table/VRF/AFI/input binding", 409) if (!values.vrf || !values["routing-table"] || !values.afi || values["input.filter"] === null) throw new McpError("STATE_UNAVAILABLE", "Не подтверждены effective BGP table/VRF/AFI/input binding", 409)
const boundTable = unique(tables, values["routing-table"]) const boundTable = unique(tables, values["routing-table"])
if (![true, "true", "yes"].includes(boundTable.fib as string | boolean) || [true, "true", "yes"].includes(boundTable.disabled as string | boolean) || [true, "true", "yes"].includes(boundTable.invalid as string | boolean)) throw new McpError("STATE_UNAVAILABLE", "FIB BGP таблицы не подтверждён", 409) if (!isConfirmedRoutingFibTable(boundTable)) throw new McpError("STATE_UNAVAILABLE", "FIB BGP таблицы не подтверждён", 409)
await enabled(sourceId, "/ip/vrf", unique(vrfs, values.vrf)) await enabled(sourceId, "/ip/vrf", unique(vrfs, values.vrf))
bindings.push({ connectionId: connection[".id"], instance: instance ? String(instance.name) : null, vrf: values.vrf, table: values["routing-table"]!, afi: values.afi, inputChain: values["input.filter"]!, outputChain: values["output.filter-chain"] ?? "" }) bindings.push({ connectionId: connection[".id"], instance: instance ? String(instance.name) : null, vrf: values.vrf, table: values["routing-table"]!, afi: values.afi, inputChain: values["input.filter"]!, outputChain: values["output.filter-chain"] ?? "" })
} }
@@ -177,7 +179,7 @@ async function readContext(profile: RouteAiProfile, options: RouteContextOptions
let gateway: ReturnType<typeof routeGatewayContext> let gateway: ReturnType<typeof routeGatewayContext>
try { gateway = routeGatewayContext(candidate.gateway) } catch { throw new McpError("STATE_UNAVAILABLE", "Gateway кандидата не подтверждён", 409) } try { gateway = routeGatewayContext(candidate.gateway) } catch { throw new McpError("STATE_UNAVAILABLE", "Gateway кандидата не подтверждён", 409) }
const resolver = unique(tables, gateway.resolverTable) const resolver = unique(tables, gateway.resolverTable)
if (![true, "true", "yes"].includes(resolver.fib as string | boolean) || [true, "true", "yes"].includes(resolver.disabled as string | boolean) || [true, "true", "yes"].includes(resolver.invalid as string | boolean)) throw new McpError("STATE_UNAVAILABLE", "FIB resolver таблицы не подтверждён", 409) if (!isConfirmedRoutingFibTable(resolver)) throw new McpError("STATE_UNAVAILABLE", "FIB resolver таблицы не подтверждён", 409)
if (unavailableIds.includes(candidate.jumpHostId) || unavailableIds.includes(candidate.exitNodeId)) throw new McpError("DEVICE_UNAVAILABLE", "Источник кандидата отсутствует или отключён", 409) if (unavailableIds.includes(candidate.jumpHostId) || unavailableIds.includes(candidate.exitNodeId)) throw new McpError("DEVICE_UNAVAILABLE", "Источник кандидата отсутствует или отключён", 409)
} catch (error) { unavailableCandidate(candidate.id, error) } } catch (error) { unavailableCandidate(candidate.id, error) }
} }
@@ -250,6 +252,14 @@ async function readContext(profile: RouteAiProfile, options: RouteContextOptions
nativeRuleId: selected && current ? filters.find(row => row.chain === selected.inputChain)![".id"] : null, ownedCompilerRecognized: !!current, nativeRuleId: selected && current ? filters.find(row => row.chain === selected.inputChain)![".id"] : null, ownedCompilerRecognized: !!current,
sharedConnectionIds, bindings, disabledConnectionIds: disabledBindings.map(binding => binding.connectionId), externalReferences, executionAvailable: reason === null, executionReason: reason, scopedExecutionAvailable: scopedReason === null, scopedExecutionReason: scopedReason, targetRuleProof, dependencyProof, sourceProofs: sources.expected } sharedConnectionIds, bindings, disabledConnectionIds: disabledBindings.map(binding => binding.connectionId), externalReferences, executionAvailable: reason === null, executionReason: reason, scopedExecutionAvailable: scopedReason === null, scopedExecutionReason: scopedReason, targetRuleProof, dependencyProof, sourceProofs: sources.expected }
return { before, clients, fence, probeEndpoints, ownedTarget, return { before, clients, fence, probeEndpoints, ownedTarget,
captureOriginalEffectSelection: async (candidateId: string) => {
try {
const routes = await get(sourceId, "/routing/route"), sessions = await get(sourceId, "/routing/bgp/session")
await fence()
return prepareOriginalEffectSelection(profile, candidateId, selected, { routes, connections, sessions,
candidate: candidateRows.get(candidateId), probe: probeEndpoints.get(candidateId) })
} catch { await fence(); return { status: "unknown" as const, reason: "prepared-effect-catalogue-unavailable" } }
},
signEvidence: (value: unknown) => sign(pathProofTag("community") + fingerprint({ dependencyProof, targetRuleProof, value })), signEvidence: (value: unknown) => sign(pathProofTag("community") + fingerprint({ dependencyProof, targetRuleProof, value })),
verifyEvidence: (value: unknown, proof: unknown) => typeof proof === "string" && constantEqual(sign(pathProofTag("community") + fingerprint({ dependencyProof, targetRuleProof, value })), proof), verifyEvidence: (value: unknown, proof: unknown) => typeof proof === "string" && constantEqual(sign(pathProofTag("community") + fingerprint({ dependencyProof, targetRuleProof, value })), proof),
} }
@@ -1,5 +1,8 @@
import assert from "node:assert/strict" import assert from "node:assert/strict"
import { readFile } from "node:fs/promises"
import { test } from "node:test" import { test } from "node:test"
import { runInNewContext } from "node:vm"
import ts from "typescript"
import type { ActionContext } from "./action-types.js" import type { ActionContext } from "./action-types.js"
import { loadReservedScopedRecoverySource } from "./routing-community-scoped-recovery-source.js" import { loadReservedScopedRecoverySource } from "./routing-community-scoped-recovery-source.js"
import { executeReservedScopedRecovery, scopedRecoveryIntentFromCheckpoint, validateReservedInverseVerified } from "./routing-community-scoped-recovery.js" import { executeReservedScopedRecovery, scopedRecoveryIntentFromCheckpoint, validateReservedInverseVerified } from "./routing-community-scoped-recovery.js"
@@ -16,6 +19,67 @@ test("a serialized allocation cannot enter the shared inverse engine", async ()
await assert.rejects(executeReservedScopedRecovery(forged, {} as Parameters<typeof executeReservedScopedRecovery>[1]), /capability/) await assert.rejects(executeReservedScopedRecovery(forged, {} as Parameters<typeof executeReservedScopedRecovery>[1]), /capability/)
}) })
test("confirmed inverse spend quarantines the exact slot on post-spend refusal without replay", async () => {
// Execute the actual boundary body with only its I/O dependencies substituted.
// This does not mint a production capability or claim native/SQL acceptance.
const source = ts.createSourceFile("routing-community-scoped-recovery.ts",
await readFile(new URL("./routing-community-scoped-recovery.ts", import.meta.url), "utf8"), ts.ScriptTarget.ES2022)
const declaration = source.statements.find(node => ts.isFunctionDeclaration(node) && node.name?.text === "executeReservedScopedRecovery")
assert(declaration)
const compiled = ts.transpileModule(declaration.getText(source), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText
const allocation = Object.freeze({ kind: "routing-reserved-inverse-allocation" })
const execution = Object.freeze({ kind: "routing-reserved-inverse-execution" })
const principal = { connectionId: "owned", credentialId: "original" }
const original = new Error("original authority failure"), result = Object.freeze({ outcome: "recovered" })
const scenarios = {
spend: ["spend"],
inspect: ["spend", "inspect", "quarantine"],
authority: ["spend", "inspect", "authority", "quarantine"],
source: ["spend", "inspect", "authority", "source", "quarantine"],
proof: ["spend", "inspect", "authority", "source", "proof", "quarantine"],
engine: ["spend", "inspect", "authority", "source", "proof", "engine", "quarantine"],
success: ["spend", "inspect", "authority", "source", "proof", "engine"],
} as const
for (const [scenario, expected] of Object.entries(scenarios)) {
const events: string[] = [], exports: Record<string, unknown> = {}, capabilities = new WeakMap()
let request: object | undefined
const at = (phase: string) => { events.push(phase); if (phase === scenario) throw original }
runInNewContext(compiled, {
exports, reservedExecutions: capabilities, fingerprint: JSON.stringify,
createRoutingReservationStore: () => ({
async beginInverseAttempt(value: unknown) {
assert.equal(value, allocation); at("spend"); return { reservationId: "spent-slot", execution }
},
async inspectInverseExecution(value: unknown) {
assert.equal(value, execution); at("inspect")
return { principal, sourceOperationId: "source", deadline: "2026-10-29T01:00:00.000Z" }
},
async markUnknown(id: string, lane: string) {
assert.equal(id, "spent-slot"); assert.equal(lane, "inverse"); at("quarantine")
throw new Error("quarantine persistence unavailable")
},
}),
effectiveMcpConnection: async () => { at("authority"); return { owner_id: "owner", policy: {}, owner_permissions: [] } },
loadReservedScopedRecoverySource: async () => {
at("source"); return { profile: { serverId: 1 }, input: {}, configuration: { packet: {} } }
},
accessGeneration: async () => "generation",
deadline: () => Date.parse("2026-10-29T01:00:00.000Z"),
sealedProof: async () => { at("proof"); return "proof" },
executeScopedRecovery: async (value: object) => {
request = value; assert.equal(capabilities.get(value), execution); at("engine"); return result
},
})
const execute = exports.executeReservedScopedRecovery as
(allocation: unknown, expected: { sourceOperationId: string; principal: typeof principal }) => Promise<unknown>
const outcome = execute(allocation, { sourceOperationId: "source", principal })
if (scenario === "success") assert.equal(await outcome, result)
else await assert.rejects(outcome, error => error === original)
assert.deepEqual(events, expected, scenario)
if (request) assert.equal(capabilities.has(request), false)
}
})
test("configuration equality or generic VERIFIED cannot replace reserved signed receipts", async () => { test("configuration equality or generic VERIFIED cannot replace reserved signed receipts", async () => {
const expected = {} as Parameters<typeof validateReservedInverseVerified>[1] const expected = {} as Parameters<typeof validateReservedInverseVerified>[1]
for (const row of [{}, { state: "verified", phase: "RECOVERY_VERIFIED", configurationVerified: true }, { inverse_intent_sealed: "x".repeat(524289) }]) { for (const row of [{}, { state: "verified", phase: "RECOVERY_VERIFIED", configurationVerified: true }, { inverse_intent_sealed: "x".repeat(524289) }]) {
File diff suppressed because it is too large. Load diff
@@ -1,3 +1,4 @@
import { originalEffectReportSchema, type OriginalEffectReport } from "./routing-original-effects.js"
import { z } from "zod" import { z } from "zod"
import type { PoolClient } from "pg" import type { PoolClient } from "pg"
import { pool } from "../../db/index.js" import { pool } from "../../db/index.js"
@@ -19,7 +20,7 @@ const checkpointSchema = z.union([manualCheckpointSchema, manualCheckpointSchema
export type ScopedConfigurationPacket = z.infer<typeof checkpointSchema> export type ScopedConfigurationPacket = z.infer<typeof checkpointSchema>
type Checkpoint = ScopedConfigurationPacket type Checkpoint = ScopedConfigurationPacket
const networkSchema = z.object({ receiptId: z.string().uuid(), requestSeal: proof, configurationCheckpointProof: proof, const networkSchema = z.object({ receiptId: z.string().uuid(), requestSeal: proof, configurationCheckpointProof: proof,
check: z.object({ id: z.string().uuid(), candidateId: z.string(), status: z.enum(["passed", "failed"]), observedAt: z.string().datetime() }).strict(), networkSeal: proof }).strict() check: z.object({ id: z.string().uuid(), candidateId: z.string(), status: z.enum(["passed", "failed"]), observedAt: z.string().datetime() }).strict(), originalEffects: originalEffectReportSchema.optional(), networkSeal: proof }).strict()
const intended = (input: Record<string, unknown>) => JSON.stringify(redact({ kind: "service", service: "routing.community.scoped", input })) const intended = (input: Record<string, unknown>) => JSON.stringify(redact({ kind: "service", service: "routing.community.scoped", input }))
const connection = (input: Record<string, unknown>) => z.object({ connectionId: z.string().uuid() }).parse(input.principal).connectionId const connection = (input: Record<string, unknown>) => z.object({ connectionId: z.string().uuid() }).parse(input.principal).connectionId
const seal = (value: Checkpoint) => privateSecretFingerprint(fingerprint(value), `route-ai-scoped-config-checkpoint-v${value.revision}`) const seal = (value: Checkpoint) => privateSecretFingerprint(fingerprint(value), `route-ai-scoped-config-checkpoint-v${value.revision}`)
@@ -72,12 +73,13 @@ export async function loadScopedConfigurationCheckpoint(input: Record<string, un
return value ? { checkpointProof: value.checkpointProof, observedAt: value.observedAt } : null return value ? { checkpointProof: value.checkpointProof, observedAt: value.observedAt } : null
} }
export async function saveScopedNetworkCheckpoint(input: Record<string, unknown>, requestSeal: string, check: { id: string; candidateId: string; status: "passed" | "failed"; observedAt: string }) { export async function saveScopedNetworkCheckpoint(input: Record<string, unknown>, requestSeal: string, check: { id: string; candidateId: string; status: "passed" | "failed"; observedAt: string }, originalEffects?: OriginalEffectReport) {
await assertPersistentScopedProfileAuthority(input) await assertPersistentScopedProfileAuthority(input)
const configuration = await loadScopedConfigurationCheckpoint(input, requestSeal) const configuration = await loadScopedConfigurationCheckpoint(input, requestSeal)
if (!configuration) throw new McpError("STATE_CONFLICT", "Нет подтверждённого CONFIG_APPLIED", 409) if (!configuration) throw new McpError("STATE_CONFLICT", "Нет подтверждённого CONFIG_APPLIED", 409)
const packet = { receiptId: input.receiptId, requestSeal, configurationCheckpointProof: configuration.checkpointProof, const packet = { receiptId: input.receiptId, requestSeal, configurationCheckpointProof: configuration.checkpointProof,
check: { id: check.id, candidateId: check.candidateId, status: check.status, observedAt: check.observedAt } } check: { id: check.id, candidateId: check.candidateId, status: check.status, observedAt: check.observedAt },
...(originalEffects === undefined ? {} : { originalEffects: originalEffectReportSchema.parse(originalEffects) }) }
const networkSeal = await privateSecretFingerprint(fingerprint(packet), "route-ai-scoped-network-checkpoint-v1") const networkSeal = await privateSecretFingerprint(fingerprint(packet), "route-ai-scoped-network-checkpoint-v1")
await assertPersistentScopedProfileAuthority(input) await assertPersistentScopedProfileAuthority(input)
await audit(pool, String(input.ownerId), "routing.scoped.network-check", connection(input), { ...packet, networkSeal }) await audit(pool, String(input.ownerId), "routing.scoped.network-check", connection(input), { ...packet, networkSeal })
@@ -92,5 +94,5 @@ export async function loadScopedNetworkCheckpoint(input: Record<string, unknown>
if (!parsed.success) return null if (!parsed.success) return null
const { networkSeal, ...packet } = parsed.data const { networkSeal, ...packet } = parsed.data
if (packet.configurationCheckpointProof !== configuration.checkpointProof || packet.check.candidateId !== input.candidateId || Date.parse(packet.check.observedAt) < Date.parse(configuration.observedAt) || Date.parse(packet.check.observedAt) > Date.now() || networkSeal !== await privateSecretFingerprint(fingerprint(packet), "route-ai-scoped-network-checkpoint-v1")) return null if (packet.configurationCheckpointProof !== configuration.checkpointProof || packet.check.candidateId !== input.candidateId || Date.parse(packet.check.observedAt) < Date.parse(configuration.observedAt) || Date.parse(packet.check.observedAt) > Date.now() || networkSeal !== await privateSecretFingerprint(fingerprint(packet), "route-ai-scoped-network-checkpoint-v1")) return null
return { status: packet.check.status, networkSeal } return { status: packet.check.status, networkSeal, ...(packet.originalEffects === undefined ? {} : { originalEffects: packet.originalEffects }) }
} }
@@ -257,6 +257,7 @@ async function checkpoint(value: Session, intent: Intent, phase: Checkpoint["pha
export async function executeReservedScopedRecovery(allocation: RoutingReservedInverseAllocation, expected: { sourceOperationId: string; principal: McpPrincipal }) { export async function executeReservedScopedRecovery(allocation: RoutingReservedInverseAllocation, expected: { sourceOperationId: string; principal: McpPrincipal }) {
const store = createRoutingReservationStore(), spent = await store.beginInverseAttempt(allocation) const store = createRoutingReservationStore(), spent = await store.beginInverseAttempt(allocation)
const execution = spent.execution const execution = spent.execution
try {
const binding = await store.inspectInverseExecution(execution) const binding = await store.inspectInverseExecution(execution)
if (binding.sourceOperationId !== expected.sourceOperationId || fingerprint(binding.principal) !== fingerprint(expected.principal)) throw fail("Reserved runner identity differs") if (binding.sourceOperationId !== expected.sourceOperationId || fingerprint(binding.principal) !== fingerprint(expected.principal)) throw fail("Reserved runner identity differs")
const current = await effectiveMcpConnection(binding.principal) const current = await effectiveMcpConnection(binding.principal)
@@ -273,8 +274,13 @@ export async function executeReservedScopedRecovery(allocation: RoutingReservedI
input.requestProof = await sealedProof(input, "request") input.requestProof = await sealedProof(input, "request")
reservedExecutions.set(input, execution) reservedExecutions.set(input, execution)
try { return await executeScopedRecovery(input) } try { return await executeScopedRecovery(input) }
catch (error) { await store.markUnknown(binding.reservationId, "inverse").catch(() => {}); throw error }
finally { reservedExecutions.delete(input) } finally { reservedExecutions.delete(input) }
} catch (error) {
// Acknowledged spend owns this exact slot even when its authority can no
// longer be inspected. A refused or uncertain spend never enters here.
await store.markUnknown(spent.reservationId, "inverse").catch(() => {})
throw error
}
} }
/** Fixed-domain receipt verifier used by the reservation store. It recognizes /** Fixed-domain receipt verifier used by the reservation store. It recognizes
@@ -1,5 +1,18 @@
import type { PoolClient } from "pg"
import { COMMUNITY_PATH_MAX_AGE_MS } from "./path-proof-purpose.js"
import { fingerprint, McpError, privateSecretFingerprint } from "./security.js" import { fingerprint, McpError, privateSecretFingerprint } from "./security.js"
export async function readScopedPathCheckpoint(db: Pick<PoolClient, "query">, id: string, connectionId: string, phase: "admission" | "configuration" = "admission") {
const { rows: [row] } = await db.query("SELECT id,connection_id,profile_hash,candidate_id,status,created_at,evidence FROM mcp_path_checks WHERE id=$1 AND connection_id=$2", [id, connectionId])
const now = Date.now(), createdAt = row?.created_at.getTime(), observedAt = Date.parse(row?.evidence?.observedAt)
const validUntil = Math.min(createdAt, observedAt) + COMMUNITY_PATH_MAX_AGE_MS
if (!row || row.status !== "passed" || createdAt > now || phase === "admission" &&
(!Number.isFinite(validUntil) || observedAt > now || validUntil <= now)) {
throw new McpError("MEASUREMENT_UNKNOWN", "Контрольное подтверждение пути устарело", 409)
}
return { proof: await privateSecretFingerprint(fingerprint(row), "route-ai-scoped-path-checkpoint-v1"), validUntil }
}
/** One request domain for forward execution and trusted recovery resolution. */ /** One request domain for forward execution and trusted recovery resolution. */
export async function assertScopedCommunityRequest(input: Record<string, unknown>) { export async function assertScopedCommunityRequest(input: Record<string, unknown>) {
const { requestProof: _proof, ...request } = input const { requestProof: _proof, ...request } = input
@@ -1,3 +1,7 @@
import { createRoutingNetworkSessionForCurrentExecution } from "./routing-network-execution-session.js"
import { readRoutingNetworkScopedCommunityContext } from "./routing-network-scoped-context.js"
import { resolveRoutingNetworkForwardRoundConfigurationContext, closeRoutingNetworkForwardRoundConfigurationContext } from "./routing-network-configuration-bridge.js"
import { originalEffectReportSchema, originalEffectSelectionSchema, unknownOriginalEffects, originalEffectBaselineMatches, inspectOriginalEffectRounds, type OriginalEffectReport } from "./routing-original-effects.js"
import { randomUUID } from "node:crypto" import { randomUUID } from "node:crypto"
import { z } from "zod" import { z } from "zod"
import { routeAiProfileSchema } from "@mmapp/contracts/mcp-routing" import { routeAiProfileSchema } from "@mmapp/contracts/mcp-routing"
@@ -8,7 +12,7 @@ import { withApplicationMutationLock, withConfigMutationLock } from "../../servi
import { effectiveMcpConnection, audit } from "./store.js" import { effectiveMcpConnection, audit } from "./store.js"
import { accessGeneration, decryptSecret, encryptSecret, fingerprint, McpError, privateSecretFingerprint, redact } from "./security.js" import { accessGeneration, decryptSecret, encryptSecret, fingerprint, McpError, privateSecretFingerprint, redact } from "./security.js"
import { saveScopedConfigurationCheckpoint, loadScopedConfigurationCheckpoint, saveScopedNetworkCheckpoint, loadScopedNetworkCheckpoint } from "./routing-community-scoped-checkpoint.js" import { saveScopedConfigurationCheckpoint, loadScopedConfigurationCheckpoint, saveScopedNetworkCheckpoint, loadScopedNetworkCheckpoint } from "./routing-community-scoped-checkpoint.js"
import { assertScopedCommunityRequest as sealedInput } from "./routing-community-scoped-request.js" import { assertScopedCommunityRequest as sealedInput, readScopedPathCheckpoint } from "./routing-community-scoped-request.js"
import { prepareManagementTransportBinding } from "./management-transport.js" import { prepareManagementTransportBinding } from "./management-transport.js"
import { readRouteCommunityContext } from "./routing-community-context.js" import { readRouteCommunityContext } from "./routing-community-context.js"
import { candidateMatchesGateway } from "./routing-community-context-model.js" import { candidateMatchesGateway } from "./routing-community-context-model.js"
@@ -25,15 +29,14 @@ import { createRoutingReservationStore } from "./routing-reservation-store.js"
const principalSchema = z.object({ connectionId: z.string().uuid(), credentialId: z.string().uuid(), credentialExpiresAt: z.string() }).strict() const principalSchema = z.object({ connectionId: z.string().uuid(), credentialId: z.string().uuid(), credentialExpiresAt: z.string() }).strict()
const fail = (message: string, code = "STATE_CONFLICT") => new McpError(code, message, 409) const fail = (message: string, code = "STATE_CONFLICT") => new McpError(code, message, 409)
const effectReadFailure = (stage: "original-effect-rounds-unavailable" | "effect-publication-revalidation-unavailable", error: unknown) => {
const code = error instanceof McpError && ["READ_CANCELLED", "PLAN_EXPIRED", "LIMIT_EXCEEDED", "STATE_CONFLICT", "RESET_REQUIRED", "STATE_UNAVAILABLE"].includes(error.code)
? error.code : "STATE_UNAVAILABLE"
return unknownOriginalEffects(stage + ":" + code)
}
export const scopedProfileIds = (profile: z.infer<typeof routeAiProfileSchema>) => [...new Set([profile.serverId, ...profile.candidates.flatMap(candidate => [candidate.jumpHostId, candidate.exitNodeId])])].sort((a, b) => a - b) export const scopedProfileIds = (profile: z.infer<typeof routeAiProfileSchema>) => [...new Set([profile.serverId, ...profile.candidates.flatMap(candidate => [candidate.jumpHostId, candidate.exitNodeId])])].sort((a, b) => a - b)
const publicRule = (rule: { community: string; gateway: string; outIface: string }) => ({ community: rule.community, action: "route", gateway: rule.gateway, gatewayTunnelId: rule.outIface }) const publicRule = (rule: { community: string; gateway: string; outIface: string }) => ({ community: rule.community, action: "route", gateway: rule.gateway, gatewayTunnelId: rule.outIface })
type ObservationPhase = "admission" | "configuration" type ObservationPhase = "admission" | "configuration"
async function pathCheckpoint(id: string, connectionId: string, phase: ObservationPhase = "admission") {
const row = (await pool.query("SELECT id,connection_id,profile_hash,candidate_id,status,created_at,evidence FROM mcp_path_checks WHERE id=$1 AND connection_id=$2", [id, connectionId])).rows[0]
if (!row || row.status !== "passed" || row.created_at.getTime() > Date.now() || phase === "admission" && Date.now() - row.created_at.getTime() > 90000) throw fail("Контрольное подтверждение пути устарело", "MEASUREMENT_UNKNOWN")
return privateSecretFingerprint(fingerprint(row), "route-ai-scoped-path-checkpoint-v1")
}
async function access(input: Record<string, unknown>, signal?: AbortSignal, verified = false, phase: ObservationPhase = "admission") { async function access(input: Record<string, unknown>, signal?: AbortSignal, verified = false, phase: ObservationPhase = "admission") {
if (signal?.aborted) throw fail("Операция отменена", "READ_CANCELLED") if (signal?.aborted) throw fail("Операция отменена", "READ_CANCELLED")
if (verified) await assertVerifiedConfigurationAccess() if (verified) await assertVerifiedConfigurationAccess()
@@ -49,7 +52,7 @@ async function access(input: Record<string, unknown>, signal?: AbortSignal, veri
const ranked = rankRoutingCandidates(profile, await readRoutingObservations(ids)) const ranked = rankRoutingCandidates(profile, await readRoutingObservations(ids))
if (!ranked.find(row => row.candidate.id === input.candidateId)?.eligible) throw fail("Нет свежего SLA выбранного пути", "MEASUREMENT_UNKNOWN") if (!ranked.find(row => row.candidate.id === input.candidateId)?.eligible) throw fail("Нет свежего SLA выбранного пути", "MEASUREMENT_UNKNOWN")
} }
if (input.pathCheckId !== undefined && await pathCheckpoint(String(input.pathCheckId), principal.connectionId, phase) !== input.pathCheckpointProof) throw fail("Подтверждение пути изменилось", "MEASUREMENT_UNKNOWN") if (input.pathCheckId !== undefined && (await readScopedPathCheckpoint(pool, String(input.pathCheckId), principal.connectionId, phase)).proof !== input.pathCheckpointProof) throw fail("Подтверждение пути изменилось", "MEASUREMENT_UNKNOWN")
return { principal, current, profile, ids } return { principal, current, profile, ids }
} }
@@ -80,7 +83,7 @@ async function receipt(input: Record<string, unknown>, action: string) {
await assertPersistentScopedProfileAuthority(input) await assertPersistentScopedProfileAuthority(input)
await audit(pool, String(input.ownerId), action, principalSchema.parse(input.principal).connectionId, { receiptId: input.receiptId, requestSeal, revision: input.proofRevision }) await audit(pool, String(input.ownerId), action, principalSchema.parse(input.principal).connectionId, { receiptId: input.receiptId, requestSeal, revision: input.proofRevision })
} }
const matches = (input: Record<string, unknown>, context: Awaited<ReturnType<typeof readRouteCommunityContext>>) => scopedConfigurationMatches({ dependencyProof: input.dependencyProof, originalTargetProof: input.originalTargetProof, desiredTargetProof: input.desiredTargetProof, candidateId: input.candidateId }, context.before) const matches = (input: Record<string, unknown>, context: { before: Parameters<typeof scopedConfigurationMatches>[1] }) => scopedConfigurationMatches({ dependencyProof: input.dependencyProof, originalTargetProof: input.originalTargetProof, desiredTargetProof: input.desiredTargetProof, candidateId: input.candidateId }, context.before)
async function original(input: Record<string, unknown>, verified: boolean) { async function original(input: Record<string, unknown>, verified: boolean) {
await sealedInput(input) await sealedInput(input)
const value = await observe(input, undefined, verified) const value = await observe(input, undefined, verified)
@@ -108,7 +111,8 @@ export async function prepareScopedCommunitySwitch(input: Record<string, unknown
await access(request, action.signal) await access(request, action.signal)
} }
const pathCheckId = await requireRecentPath(profile, String(input.candidateId), action.connection.id, { authorize: async () => { await access(request, action.signal) }, signal: action.signal }) const pathCheckId = await requireRecentPath(profile, String(input.candidateId), action.connection.id, { authorize: async () => { await access(request, action.signal) }, signal: action.signal })
const withPath = { ...request, pathCheckId, pathCheckpointProof: await pathCheckpoint(pathCheckId, action.connection.id) } const checkpoint = await readScopedPathCheckpoint(pool, pathCheckId, action.connection.id)
const withPath = { ...request, pathCheckId, pathCheckpointProof: checkpoint.proof }
const value = await observe(withPath, action.signal), owned = value.context.ownedTarget! const value = await observe(withPath, action.signal), owned = value.context.ownedTarget!
if (!value.candidate.controlProbe) throw fail("Scoped переключение требует свежий control probe", "MEASUREMENT_UNKNOWN") if (!value.candidate.controlProbe) throw fail("Scoped переключение требует свежий control probe", "MEASUREMENT_UNKNOWN")
await requireRecentPath(value.profile, value.candidate.id, action.connection.id, { authorize: value.authorize, signal: action.signal }) await requireRecentPath(value.profile, value.candidate.id, action.connection.id, { authorize: value.authorize, signal: action.signal })
@@ -128,7 +132,10 @@ export async function prepareScopedCommunitySwitch(input: Record<string, unknown
try { managementBindingCiphertext = await encryptSecret(JSON.stringify(await prepareManagementTransportBinding(profile.serverId, { authorize: value.authorize, signal: action.signal, deadline: Math.min(Date.now() + 120000, Date.parse(action.principal.credentialExpiresAt), action.connection.owner_valid_until.getTime(), Date.parse(action.connection.policy.expiresAt)) }))) } try { managementBindingCiphertext = await encryptSecret(JSON.stringify(await prepareManagementTransportBinding(profile.serverId, { authorize: value.authorize, signal: action.signal, deadline: Math.min(Date.now() + 120000, Date.parse(action.principal.credentialExpiresAt), action.connection.owner_valid_until.getTime(), Date.parse(action.connection.policy.expiresAt)) }))) }
catch (error) { if (!(error instanceof McpError) || error.code !== "APPROVAL_REQUIRED") throw error } catch (error) { if (!(error instanceof McpError) || error.code !== "APPROVAL_REQUIRED") throw error }
} }
const sealed: Record<string, unknown> = { ...withPath, nativeId: owned.nativeId, outIface, contextSources: value.context.before.sourceProofs, dependencyProof: value.context.before.dependencyProof, const originalEffectSelection = await value.context.captureOriginalEffectSelection(String(input.candidateId))
const sealed: Record<string, unknown> = { ...withPath, originalEffectSelection,
...(!persistent && !profile.autoRecovery && originalEffectSelection.status === "prepared" ? { manualEffectPolicy: "observational-v1" } : {}),
nativeId: owned.nativeId, outIface, contextSources: value.context.before.sourceProofs, dependencyProof: value.context.before.dependencyProof,
originalTargetProof: value.context.before.targetRuleProof, desiredTargetProof: owned.desiredTargetProof(compiled.compiledBody), receiptId: randomUUID(), proofRevision: persistent ? 3 : 2, originalTargetProof: value.context.before.targetRuleProof, desiredTargetProof: owned.desiredTargetProof(compiled.compiledBody), receiptId: randomUUID(), proofRevision: persistent ? 3 : 2,
originalCandidateId: value.context.before.currentCandidateId, originalRuleCiphertext: await encryptSecret(String(owned.rawRow.rule)), desiredRuleCiphertext: await encryptSecret(compiled.compiledBody), managementBindingCiphertext, originalCandidateId: value.context.before.currentCandidateId, originalRuleCiphertext: await encryptSecret(String(owned.rawRow.rule)), desiredRuleCiphertext: await encryptSecret(compiled.compiledBody), managementBindingCiphertext,
rules: [publicRule(compiled.proposed)], beforeRules: [publicRule(compiled.before)] } rules: [publicRule(compiled.proposed)], beforeRules: [publicRule(compiled.before)] }
@@ -137,9 +144,12 @@ export async function prepareScopedCommunitySwitch(input: Record<string, unknown
sealed.requestProof = await privateSecretFingerprint(fingerprint(sealed), `route-ai-scoped-request-v${sealed.proofRevision}`) sealed.requestProof = await privateSecretFingerprint(fingerprint(sealed), `route-ai-scoped-request-v${sealed.proofRevision}`)
await value.context.fence() await value.context.fence()
await access(sealed, action.signal) await access(sealed, action.signal)
const authorityValidUntil = persistent ? routingProfileAuthorityBindingSchema.parse(sealed.persistentProfileAuthority).packet.validUntil : undefined
const validUntil = authorityValidUntil && !profile.autoRecovery
? new Date(Math.min(Date.parse(authorityValidUntil), checkpoint.validUntil)).toISOString() : authorityValidUntil
return { target: { scope: "servers", serverIds: scopedProfileIds(value.profile) }, objects: [{ kind: "routing_filter", serverId: value.profile.serverId, id: owned.nativeId }, ...scopedProfileIds(value.profile).map(serverId => ({ kind: "server" as const, serverId, id: String(serverId) }))], snapshots: [], before: sealed.beforeRules, return { target: { scope: "servers", serverIds: scopedProfileIds(value.profile) }, objects: [{ kind: "routing_filter", serverId: value.profile.serverId, id: owned.nativeId }, ...scopedProfileIds(value.profile).map(serverId => ({ kind: "server" as const, serverId, id: String(serverId) }))], snapshots: [], before: sealed.beforeRules,
steps: [{ kind: "service", service: "routing.community.scoped", input: sealed }], requiresHuman: true, mutationCost: 2, verificationScope: "configuration", steps: [{ kind: "service", service: "routing.community.scoped", input: sealed }], requiresHuman: true, mutationCost: 2, verificationScope: "configuration",
...(persistent ? { validUntil: routingProfileAuthorityBindingSchema.parse(sealed.persistentProfileAuthority).packet.validUntil } : {}), ...(validUntil ? { validUntil } : {}),
warnings: ["Одна запись owned фильтра; SQL метаданные фильтров сохраняются.", "SLA и свежая preflight-проба обязательны до отправки PATCH. После записи подтверждение конфигурации сохраняется независимо от новых SLA-измерений; контрольная проба и её результат проверяются отдельно.", "Подтверждается конфигурация и control probe; сквозной и обратный трафик требуют отдельной приёмки.", managementBindingCiphertext ? "Заранее закреплён независимый транспорт для отдельного обратного плана; автоматический inverse не включён." : "Независимый transport не закреплён; отдельный обратный план потребует доступного primary.", "Неизвестный результат не повторяется; сверка только читает состояние."] } warnings: ["Одна запись owned фильтра; SQL метаданные фильтров сохраняются.", "SLA и свежая preflight-проба обязательны до отправки PATCH. После записи подтверждение конфигурации сохраняется независимо от новых SLA-измерений; контрольная проба и её результат проверяются отдельно.", "Подтверждается конфигурация и control probe; сквозной и обратный трафик требуют отдельной приёмки.", managementBindingCiphertext ? "Заранее закреплён независимый транспорт для отдельного обратного плана; автоматический inverse не включён." : "Независимый transport не закреплён; отдельный обратный план потребует доступного primary.", "Неизвестный результат не повторяется; сверка только читает состояние."] }
} }
@@ -154,9 +164,35 @@ export function registerScopedCommunityService() {
} }
await original(input, true) await original(input, true)
if (await receiptExists(input, "routing.scoped.attempt")) throw fail("Повтор PATCH запрещён", "MUTATION_OUTCOME_UNKNOWN") if (await receiptExists(input, "routing.scoped.attempt")) throw fail("Повтор PATCH запрещён", "MUTATION_OUTCOME_UNKNOWN")
await receipt(input, "routing.scoped.attempt") const requiresEffectBaseline = input.proofRevision === 3 && !routeAiProfileSchema.parse(input.profile).autoRecovery
let attemptRecorded = !requiresEffectBaseline
if (attemptRecorded) await receipt(input, "routing.scoped.attempt")
const before = await original(input, true), owned = before.context.ownedTarget! const before = await original(input, true), owned = before.context.ownedTarget!
let effectSession: Awaited<ReturnType<typeof createRoutingNetworkSessionForCurrentExecution>> | undefined
let effectBaseline: Parameters<typeof originalEffectBaselineMatches>[1] | undefined
let originalEffects: OriginalEffectReport | undefined
const effectSelection = originalEffectSelectionSchema.safeParse(input.originalEffectSelection)
if (effectSelection.success) originalEffects = unknownOriginalEffects(effectSelection.data.status === "unknown" ? effectSelection.data.reason : "effect-acquisition-unavailable")
try { try {
// The persistent controller needs a baseline it can advance after the
// write. Manual plans still treat the effect vector as observational.
if (context?.phase === "forward" && (input.proofRevision === 3 || input.proofRevision === 2 && input.manualEffectPolicy === "observational-v1") && effectSelection.success && effectSelection.data.status === "prepared") {
try {
const selected = effectSelection.data
effectSession = await createRoutingNetworkSessionForCurrentExecution({ servers: scopedProfileIds(before.profile),
probes: [{ key: selected.probe.key, serverId: selected.probe.serverId, destination: selected.probe.destination,
source: selected.probe.source, interface: selected.probe.interface, probeVrf: selected.probe.probeVrf }] })
effectBaseline = await effectSession.readSnapshot("before")
if (!originalEffectBaselineMatches(selected, effectBaseline)) {
effectSession.close(); effectSession = undefined; originalEffects = unknownOriginalEffects("prepared-effect-baseline-drift")
}
} catch { effectSession?.close(); effectSession = undefined }
}
if (requiresEffectBaseline) {
if (!effectSession) throw fail("Исходное состояние изменяемого префикса не подтверждено", "MEASUREMENT_UNKNOWN")
await receipt(input, "routing.scoped.attempt")
attemptRecorded = true
}
await executeGuardedConfigurationWrite(before.profile.serverId, "PATCH", "/routing/filter/rule/" + owned.nativeId, async () => { await executeGuardedConfigurationWrite(before.profile.serverId, "PATCH", "/routing/filter/rule/" + owned.nativeId, async () => {
// This runs AFTER the durable configuration.intent, immediately before transport. // This runs AFTER the durable configuration.intent, immediately before transport.
const fresh = await original(input, true); await before.context.fence() const fresh = await original(input, true); await before.context.fence()
@@ -170,13 +206,48 @@ export function registerScopedCommunityService() {
await saveScopedConfigurationCheckpoint(input, await sealedInput(input)) await saveScopedConfigurationCheckpoint(input, await sealedInput(input))
const check = await checkCandidatePath(after.profile, after.candidate.id, principalSchema.parse(input.principal).connectionId, { authorize: after.authorize }) const check = await checkCandidatePath(after.profile, after.candidate.id, principalSchema.parse(input.principal).connectionId, { authorize: after.authorize })
if (check.status === "unknown") throw fail("Control probe неизвестен", "MUTATION_OUTCOME_UNKNOWN") if (check.status === "unknown") throw fail("Control probe неизвестен", "MUTATION_OUTCOME_UNKNOWN")
// A probe can be long: prove the whole configuration again before a durable VERIFIED receipt. if (effectSession && effectBaseline && effectSelection.success && effectSelection.data.status === "prepared") {
let roundContext: Awaited<ReturnType<typeof resolveRoutingNetworkForwardRoundConfigurationContext>> | undefined
try {
const principal = principalSchema.parse(input.principal), action = { principal, connection: await effectiveMcpConnection(principal) }
roundContext = await resolveRoutingNetworkForwardRoundConfigurationContext(effectSession.sharedContext, action)
const first = await effectSession.readConfigurationBoundForwardSnapshot(roundContext)
const second = await effectSession.readConfigurationBoundForwardSnapshot(roundContext)
originalEffects = await inspectOriginalEffectRounds(effectSelection.data, [first, second], effectBaseline)
} catch (error) { originalEffects = effectReadFailure("original-effect-rounds-unavailable", error) }
finally { if (roundContext) closeRoutingNetworkForwardRoundConfigurationContext(roundContext) }
}
// Publication revalidation uses the existing native owner's fixed
// context reader and therefore its SAME counters, without a snapshot.
let sharedPublication = false
if (effectSession && !effectSession.readBudget().closed) {
try {
const published = await readRoutingNetworkScopedCommunityContext(effectSession.sharedContext)
if (!matches(input, published).configurationMatchesDesired) throw fail("Состояние изменилось после effect-check")
await published.fence()
sharedPublication = true
} catch (error) { originalEffects = effectReadFailure("effect-publication-revalidation-unavailable", error) }
}
if (!sharedPublication) {
// This is the OLD manual final read, relocated after optional work,
// never duplicated or counted as a completed effect-owner read.
// Closed/exhausted effect owners cannot waive this fresh guard.
const final = await observe(input, undefined, true, undefined, "configuration") const final = await observe(input, undefined, true, undefined, "configuration")
if (!matches(input, final.context).configurationMatchesDesired) throw fail("Состояние изменилось после probe") if (!matches(input, final.context).configurationMatchesDesired) throw fail("Состояние изменилось после probe")
await final.context.fence() await final.context.fence()
await saveScopedNetworkCheckpoint(input, await sealedInput(input), { ...check, status: check.status }) }
if (effectSession && originalEffects) {
const { deadline, calls, bytes, rows } = effectSession.readBudget()
const budget = originalEffectReportSchema.shape.budget.safeParse({ deadline, calls, bytes, rows })
if (budget.success) originalEffects = { ...originalEffects, budget: budget.data }
}
await saveScopedNetworkCheckpoint(input, await sealedInput(input), { ...check, status: check.status }, originalEffects)
return verifiedResult(input) return verifiedResult(input)
} catch { throw fail("Результат переключения не подтверждён; повтор запрещён, требуется сверка", "MUTATION_OUTCOME_UNKNOWN") } } catch (error) {
if (!attemptRecorded) throw error
throw fail("Результат переключения не подтверждён; повтор запрещён, требуется сверка", "MUTATION_OUTCOME_UNKNOWN")
}
finally { effectSession?.close() }
})), })),
verify: async (input, result) => { verify: async (input, result) => {
const expected = await verifiedResult(input) const expected = await verifiedResult(input)
@@ -200,6 +271,7 @@ async function verifiedResult(input: Record<string, unknown>) {
const network = await loadScopedNetworkCheckpoint(input, await sealedInput(input)) const network = await loadScopedNetworkCheckpoint(input, await sealedInput(input))
const result = { outcome: network?.status === "passed" ? "applied" : "configuration-applied-network-unverified", nativeWrites: 1, forwardingVerified: false, configurationVerified: true, receiptId: input.receiptId, proofRevision: input.proofRevision, const result = { outcome: network?.status === "passed" ? "applied" : "configuration-applied-network-unverified", nativeWrites: 1, forwardingVerified: false, configurationVerified: true, receiptId: input.receiptId, proofRevision: input.proofRevision,
configurationCheckpointAvailable: !!checkpoint, configurationCheckpointProof: checkpoint?.checkpointProof ?? null, controlProbeVerified: network?.status === "passed", postProbeStatus: network?.status ?? "unknown", networkCheckpointProof: network?.networkSeal ?? null, configurationCheckpointAvailable: !!checkpoint, configurationCheckpointProof: checkpoint?.checkpointProof ?? null, controlProbeVerified: network?.status === "passed", postProbeStatus: network?.status ?? "unknown", networkCheckpointProof: network?.networkSeal ?? null,
...(network?.originalEffects === undefined ? {} : { originalEffects: network.originalEffects }),
nativeVerificationSeal: await privateSecretFingerprint(fingerprint({ request: await sealedInput(input), dependency: input.dependencyProof, desired: input.desiredTargetProof, candidate: input.candidateId, configuration: checkpoint?.checkpointProof ?? null, network: network?.networkSeal ?? null }), `route-ai-scoped-verified-v${input.proofRevision}`) } nativeVerificationSeal: await privateSecretFingerprint(fingerprint({ request: await sealedInput(input), dependency: input.dependencyProof, desired: input.desiredTargetProof, candidate: input.candidateId, configuration: checkpoint?.checkpointProof ?? null, network: network?.networkSeal ?? null }), `route-ai-scoped-verified-v${input.proofRevision}`) }
await assertPersistentScopedProfileAuthority(input) await assertPersistentScopedProfileAuthority(input)
return result return result
@@ -4,6 +4,7 @@ import { findMcpOperation } from "./registry.js"
import { assertScopedCommunityRequest } from "./routing-community-scoped-request.js" import { assertScopedCommunityRequest } from "./routing-community-scoped-request.js"
import { assertPersistentScopedProfileAuthority, routingProfileAuthorityBindingSchema } from "./routing-profile-authority.js" import { assertPersistentScopedProfileAuthority, routingProfileAuthorityBindingSchema } from "./routing-profile-authority.js"
import { loadScopedConfigurationPacket, loadScopedNetworkCheckpoint } from "./routing-community-scoped-checkpoint.js" import { loadScopedConfigurationPacket, loadScopedNetworkCheckpoint } from "./routing-community-scoped-checkpoint.js"
import { originalEffectSelectionSchema } from "./routing-original-effects.js"
import { z } from "zod" import { z } from "zod"
import { routeAiProfileSchema, type RouteAiProfile } from "@mmapp/contracts/mcp-routing" import { routeAiProfileSchema, type RouteAiProfile } from "@mmapp/contracts/mcp-routing"
import { hasPermission } from "../../lib/permissions.js" import { hasPermission } from "../../lib/permissions.js"
@@ -166,6 +167,13 @@ export async function advanceRoutingControllerBaseline(rawBaseline: unknown, pro
const configuration = await loadScopedConfigurationPacket(input, requestSeal, { operationId, stepIndex: 0 }) const configuration = await loadScopedConfigurationPacket(input, requestSeal, { operationId, stepIndex: 0 })
const network = await loadScopedNetworkCheckpoint(input, requestSeal) const network = await loadScopedNetworkCheckpoint(input, requestSeal)
if (!configuration || configuration.packet.revision !== 2 || !network || network.status !== "passed") throw fail() if (!configuration || configuration.packet.revision !== 2 || !network || network.status !== "passed") throw fail()
// A genuinely prepared effect check may pause controller advancement without
// erasing the separate successful manual configuration/control-probe receipt.
if (Object.hasOwn(input, "originalEffectSelection")) {
const effects = originalEffectSelectionSchema.safeParse(input.originalEffectSelection)
if (!effects.success || effects.data.status !== "prepared" || network.originalEffects?.status !== "observed-expected" ||
network.originalEffects.selectionIdentity !== fingerprint(effects.data) || network.originalEffects.switchEffectObserved !== true || !network.originalEffects.transition?.changedPrefixes.length) throw fail()
}
const packet = configuration.packet, result = row.result const packet = configuration.packet, result = row.result
if (packet.operationId !== operationId || packet.stepIndex !== 0 || packet.nativeId !== original.nativeId || if (packet.operationId !== operationId || packet.stepIndex !== 0 || packet.nativeId !== original.nativeId ||
packet.originalCandidateId !== original.candidateId || packet.originalTargetProof !== original.targetProof || packet.originalCandidateId !== original.candidateId || packet.originalTargetProof !== original.targetProof ||
@@ -4,7 +4,7 @@ import { readFile } from "node:fs/promises"
import { randomUUID } from "node:crypto" import { randomUUID } from "node:crypto"
import ts from "typescript" import ts from "typescript"
import { z } from "zod" import { z } from "zod"
import { routeAiProfileSchema } from "@mmapp/contracts/mcp-routing" import { routeAiControllerBlockReason, routeAiProfileSchema, usesScopedRouteAiContext } from "@mmapp/contracts/mcp-routing"
import { fingerprint, McpError } from "./security.js" import { fingerprint, McpError } from "./security.js"
type Handler = { prepare?: (input: Record<string, unknown>, context: unknown) => Promise<Record<string, unknown>>; execute?: (input: Record<string, unknown>) => Promise<unknown> } type Handler = { prepare?: (input: Record<string, unknown>, context: unknown) => Promise<Record<string, unknown>>; execute?: (input: Record<string, unknown>) => Promise<unknown> }
@@ -32,10 +32,10 @@ async function fixture() {
const profileModule = await load("./routing-profiles.ts", { const profileModule = await load("./routing-profiles.ts", {
"./routing-controller-overlap.js": overlap, "./security.js": { fingerprint, McpError, accessGeneration: async () => "g" }, "./routing-controller-overlap.js": overlap, "./security.js": { fingerprint, McpError, accessGeneration: async () => "g" },
"./routing-controller-baseline.js": { prepareRoutingControllerBaseline: async () => { baselinePreparations++; return { packet: {}, proof: "inert-baseline" } }, assertRoutingControllerBaseline: async () => {}, routingControllerBaselinePrincipal: async () => principal, advanceRoutingControllerBaseline: async () => {} }, "./routing-controller-baseline.js": { prepareRoutingControllerBaseline: async () => { baselinePreparations++; return { packet: {}, proof: "inert-baseline" } }, assertRoutingControllerBaseline: async () => {}, routingControllerBaselinePrincipal: async () => principal, advanceRoutingControllerBaseline: async () => {} },
"./routing-community-public.js": { publicRoutingControllerState: () => ({}) }, "node:crypto": { randomUUID }, zod: { z }, "@mmapp/contracts/mcp-routing": { routeAiProfileSchema }, "../../db/index.js": { pool }, "./routing-community-public.js": { publicRoutingControllerState: () => ({}) }, "node:crypto": { randomUUID }, zod: { z }, "@mmapp/contracts/mcp-routing": { routeAiControllerBlockReason, routeAiProfileSchema, usesScopedRouteAiContext }, "../../db/index.js": { pool },
"./service-registry.js": { registerMcpService: (name: string, handler: Handler) => services.set(name, handler) }, "./service-registry.js": { registerMcpService: (name: string, handler: Handler) => services.set(name, handler) },
"./store.js": { audit: async () => {}, validatePrincipal: async () => connection }, "./routing-model.js": { decideRoutingSwitch: () => ({}) }, "./store.js": { audit: async () => {}, validatePrincipal: async () => connection }, "./routing-model.js": { decideRoutingSwitch: () => ({}) },
"./routing-community-context-model.js": { routeContextExecutionReason: () => null }, "./path-proof-purpose.js": { COMMUNITY_PATH_MAX_AGE_MS: 90000 },
"./routing-controller-fence.js": { saveControllerProfileCas: async () => { casWrites++; return { id: "saved", version: 1 } } }, "./routing-controller-fence.js": { saveControllerProfileCas: async () => { casWrites++; return { id: "saved", version: 1 } } },
"./routing-reservation-store.js": { createRoutingReservationStore: () => { throw new Error("Overlap-only fixture must not read or mutate routing charges") } }, "./routing-reservation-store.js": { createRoutingReservationStore: () => { throw new Error("Overlap-only fixture must not read or mutate routing charges") } },
}) })
+23 -9
View File
@@ -1,3 +1,5 @@
import { readRoutingTopology } from "../../services/routing-topology-observation.js"
import { createRoutingSourceContext, readRoutingEndpoints } from "./routing-observation-source.js"
import { z } from "zod" import { z } from "zod"
import { dedupeVrfAliasSnapshots } from "../../services/vrf-production-action-binding.js" import { dedupeVrfAliasSnapshots } from "../../services/vrf-production-action-binding.js"
import { pool } from "../../db/index.js" import { pool } from "../../db/index.js"
@@ -177,14 +179,26 @@ export function registerRoutingControls(register: typeof add) {
}, },
}) })
register("routing.topology.read", "mm_routing_topology_get", "Наблюдаемая конфигурация маршрутизации", "network", "mm:network:read", "read", "servers", z.object({ serverId }).strict(), { register("routing.topology.read", "mm_routing_topology_get", "Наблюдаемая конфигурация маршрутизации", "network", "mm:network:read", "read", "servers", z.object({ serverId }).strict(), {
read: async input => { read: async (input, context) => {
const { client } = await routerClient(Number(input.serverId)) const sid = Number(input.serverId)
const paths = { interfaces: "/interface", ipv4: "/ip/route", ipv6: "/ipv6/route", tables: "/routing/table", rules: "/routing/rule", vrfs: "/ip/vrf", ospfNeighbors: "/routing/ospf/neighbor", bgpSessions: "/routing/bgp/session" } if (!context.connection.policy.scope.serverIds.includes(sid)) throw new McpError("SERVER_DENIED", "Источник топологии вне разрешённой области", 403)
const entries = await Promise.all(Object.entries(paths).map(async ([key, path]) => { const signal = AbortSignal.any([AbortSignal.timeout(30000), ...(context.signal ? [context.signal] : [])])
try { const values = await client.get<unknown>(path); const rows = Array.isArray(values) ? values : [values]; return [key, { items: rows.slice(0, 500), truncated: rows.length > 500, observed: true }] } const authorize = async () => {
catch { return [key, { items: [], observed: false, reason: "Устройство не подтвердило возможность чтения" }] } if (signal.aborted) throw new McpError("READ_CANCELLED", "Чтение топологии отменено или превысило срок", 409)
})) const current = await effectiveMcpConnection(context.principal)
return { serverId: input.serverId, observedAt: new Date().toISOString(), resources: Object.fromEntries(entries), observedPath: false } if (!hasPermission(current.owner_permissions, "mm:network:read") || !hasPermission(current.policy.permissions, "mm:network:read")) throw new McpError("PERMISSION_DENIED", "Право чтения сети отозвано", 403)
if (fingerprint(current.policy) !== fingerprint(context.connection.policy) || fingerprint(current.owner_permissions) !== fingerprint(context.connection.owner_permissions)) throw new McpError("RESET_REQUIRED", "Допуск изменился; повторите чтение топологии", 409)
}
await authorize()
const source = await createRoutingSourceContext(await readRoutingEndpoints([sid]))
if (source.expected.length !== 1) throw new McpError("DEVICE_UNAVAILABLE", "Источник топологии отсутствует или выключен", 409)
const beforeRead = async () => {
await authorize(); await source.assertCurrent()
if (signal.aborted) throw new McpError("READ_CANCELLED", "Чтение топологии отменено или превысило срок", 409)
}
await beforeRead()
const { client } = await routerClient(sid)
return { serverId: sid, ...await readRoutingTopology(client, beforeRead, signal) }
}, },
}) })
register("routing.ecmp.update", "mm_ecmp_plan", "Равная стоимость маршрутов ECMP и hash policy", "network", "mm:network:write", "write", "servers", z.object({ serverId, family, routeIds: z.array(objectId).min(2).max(8).refine(ids => new Set(ids).size === ids.length), distance: z.number().int().min(1).max(255).default(1), hashPolicy: z.enum(["l3", "l4", "l3-inner"]).optional() }).strict(), { register("routing.ecmp.update", "mm_ecmp_plan", "Равная стоимость маршрутов ECMP и hash policy", "network", "mm:network:write", "write", "servers", z.object({ serverId, family, routeIds: z.array(objectId).min(2).max(8).refine(ids => new Set(ids).size === ids.length), distance: z.number().int().min(1).max(255).default(1), hashPolicy: z.enum(["l3", "l4", "l3-inner"]).optional() }).strict(), {
@@ -229,7 +243,7 @@ export function registerRoutingControls(register: typeof add) {
}, },
}) })
register("routing.vrf.update", "mm_vrf_plan", "Проверенный состав существующего VRF", "network", "mm:network:write", "write", "servers", z.object({ serverId, id: objectId, interfaces: z.array(text).min(1).max(100).refine(values => new Set(values).size === values.length) }).strict(), { register("routing.vrf.update", "mm_vrf_plan", "Проверенный состав существующего VRF", "network", "mm:network:write", "write", "servers", z.object({ serverId, id: objectId, interfaces: z.array(text).min(1).max(100).refine(values => new Set(values).size === values.length) }).strict(), {
version: 3, version: 4,
prepare: async (input, context) => { prepare: async (input, context) => {
const sid = Number(input.serverId), { client } = await routerClient(sid), snapshot = await readRosSnapshot(sid, "vrf"), current = snapshot.rows.find(row => row[".id"] === input.id) const sid = Number(input.serverId), { client } = await routerClient(sid), snapshot = await readRosSnapshot(sid, "vrf"), current = snapshot.rows.find(row => row[".id"] === input.id)
if (!current || current.name === "main" || !hasManagedCommentPrefix(String(current.comment ?? ""))) throw new McpError("PROTECTED_OBJECT", "Изменять состав можно только у существующего VRF приложения") if (!current || current.name === "main" || !hasManagedCommentPrefix(String(current.comment ?? ""))) throw new McpError("PROTECTED_OBJECT", "Изменять состав можно только у существующего VRF приложения")
@@ -37,7 +37,7 @@ function fixture(service='routing.community') {
const principal={connectionId:id(3), credentialId:id(4), credentialExpiresAt:'2026-11-02T13:00:00.000Z'}; const principal={connectionId:id(3), credentialId:id(4), credentialExpiresAt:'2026-11-02T13:00:00.000Z'};
const authority={packet:{profileId:id(5),profileVersion:7,connectionId:id(3),credentialId:id(4),ownerId:'owner'}, proof:'a'.repeat(64)}; const authority={packet:{profileId:id(5),profileVersion:7,connectionId:id(3),credentialId:id(4),ownerId:'owner'}, proof:'a'.repeat(64)};
const metadata={operationId:id(1),planId:id(2),operationName:'routing.community.switch',operationVersion:7,ownerId:'owner',phase:'forward',stepIndex:0,serviceName:service,principal,policyVersion:2,generation:'generation',originalExpiresAt:now.getTime()+60000,sealedEvidenceProof:sha('evidence'),sealedPayloadProof:sha('payload'),serviceInput:{profile:{autoRecovery:false},persistentProfileAuthority:authority}}; const metadata={operationId:id(1),planId:id(2),operationName:'routing.community.switch',operationVersion:7,ownerId:'owner',phase:'forward',stepIndex:0,serviceName:service,principal,policyVersion:2,generation:'generation',originalExpiresAt:now.getTime()+60000,sealedEvidenceProof:sha('evidence'),sealedPayloadProof:sha('payload'),serviceInput:{profile:{autoRecovery:false},persistentProfileAuthority:authority}};
const source={operation_state:'running',plan_state:'executing',policy_version:2,generation:'generation',approved_by:'human',observed_at:now,evidence_sealed:'evidence',payload_sealed:'payload',step_state:'intent',intended_state:redact({kind:'service',service,input:metadata.serviceInput})}; const source={operation_state:'running',plan_state:'executing',policy_version:2,generation:'generation',approved_by:service==='routing.community.scoped'?'owner':'human',observed_at:now,evidence_sealed:'evidence',payload_sealed:'payload',step_state:'intent',intended_state:redact({kind:'service',service,input:metadata.serviceInput})};
const binding={revision:1,operationId:id(1),planId:id(2),stepIndex:0,profileId:id(5),profileVersion:7,connectionId:id(3),ownerId:'owner',principal,service,operationVersion:7,authorityProof:authority.proof,evidenceProof:fingerprint('evidence'),payloadProof:fingerprint('payload'),scope:[1],createdAt:now.toISOString(),chargeUnits:1}; const binding={revision:1,operationId:id(1),planId:id(2),stepIndex:0,profileId:id(5),profileVersion:7,connectionId:id(3),ownerId:'owner',principal,service,operationVersion:7,authorityProof:authority.proof,evidenceProof:fingerprint('evidence'),payloadProof:fingerprint('payload'),scope:[1],createdAt:now.toISOString(),chargeUnits:1};
const row={source_operation_id:id(1),source_plan_id:id(2),source_step_index:0,profile_id:id(5),connection_id:id(3),charge_units:1,binding_sealed:null,binding_proof:null,created_at:now}; const row={source_operation_id:id(1),source_plan_id:id(2),source_step_index:0,profile_id:id(5),connection_id:id(3),charge_units:1,binding_sealed:null,binding_proof:null,created_at:now};
let chargeRows=[row], sourceRows=[source], reservedRows=[], decryptCalls=0, nativeWrites=0, keyUnavailable=false; let chargeRows=[row], sourceRows=[source], reservedRows=[], decryptCalls=0, nativeWrites=0, keyUnavailable=false;
@@ -59,6 +59,7 @@ function fixture(service='routing.community') {
run:async()=>{await verifier(db,metadata,'owner');nativeWrites++;},writes:()=>nativeWrites,decryptCalls:()=>decryptCalls}; run:async()=>{await verifier(db,metadata,'owner');nativeWrites++;},writes:()=>nativeWrites,decryptCalls:()=>decryptCalls};
} }
for(const service of ['routing.community','routing.community.scoped'])test(`exact ${service} running charge passes without mutation`,async()=>{const f=fixture(service);await f.run();assert.equal(f.writes(),1);assert.equal(f.queries.length,3);assert.ok(f.queries.every(({sql})=>sql.startsWith('SELECT ')));assert.ok(f.queries[0].sql.includes('LIMIT 2'));}); for(const service of ['routing.community','routing.community.scoped'])test(`exact ${service} running charge passes without mutation`,async()=>{const f=fixture(service);await f.run();assert.equal(f.writes(),1);assert.equal(f.queries.length,3);assert.ok(f.queries.every(({sql})=>sql.startsWith('SELECT ')));assert.ok(f.queries[0].sql.includes('LIMIT 2'));});
test('scoped transport refuses delegated approval before reading the charge',async()=>{const f=fixture('routing.community.scoped');f.source.approved_by='delegated-admin';await assert.rejects(f.run);assert.equal(f.writes(),0);assert.equal(f.queries.length,1);});
const mutations=[ const mutations=[
['missing charge',f=>f.noCharge()],['duplicate charge',f=>f.duplicateCharge()],['reserved overlap',f=>f.reserved()],['ambiguous source',f=>f.ambiguousSource()], ['missing charge',f=>f.noCharge()],['duplicate charge',f=>f.duplicateCharge()],['reserved overlap',f=>f.reserved()],['ambiguous source',f=>f.ambiguousSource()],
['pending operation',f=>f.source.operation_state='pending'],['applied operation',f=>f.source.operation_state='applied'],['terminal step',f=>f.source.step_state='verified'],['draft plan',f=>f.source.plan_state='draft'],['missing approval',f=>f.source.approved_by=null],['policy drift',f=>f.source.policy_version++],['generation drift',f=>f.source.generation='other'], ['pending operation',f=>f.source.operation_state='pending'],['applied operation',f=>f.source.operation_state='applied'],['terminal step',f=>f.source.step_state='verified'],['draft plan',f=>f.source.plan_state='draft'],['missing approval',f=>f.source.approved_by=null],['policy drift',f=>f.source.policy_version++],['generation drift',f=>f.source.generation='other'],
@@ -57,7 +57,7 @@ function cursor(rows:unknown[],onFetch?:()=>void) {
test("historical cursor authenticates more than two pages of genuine nonpersistent classifications",async()=>{ test("historical cursor authenticates more than two pages of genuine nonpersistent classifications",async()=>{
const dir=await mkdtemp(join(tmpdir(),"mm-manual-history-unit-")),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir const dir=await mkdtemp(join(tmpdir(),"mm-manual-history-unit-")),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir
try { try {
const rows=[];for(let n=0;n<25;n++)rows.push((await fixture(false,n%2?6:7)).row) const rows=[];for(let n=0;n<25;n++)rows.push((await fixture(false,6+n%3)).row)
const f=cursor(rows) const f=cursor(rows)
await assertLegacyManualHistoryWithinOwner(f.db,randomUUID(),at,()=>{}) await assertLegacyManualHistoryWithinOwner(f.db,randomUUID(),at,()=>{})
assert.equal(f.calls.filter(sql=>sql.startsWith("FETCH")).length,3) assert.equal(f.calls.filter(sql=>sql.startsWith("FETCH")).length,3)
@@ -76,8 +76,10 @@ test("historical authentication, active persistent refusal, future rejection and
await assertLegacyManualHistoryWithinOwner(cursor([f.row]).db,f.profileId,"2026-11-01T13:01:00.001Z",()=>{}) await assertLegacyManualHistoryWithinOwner(cursor([f.row]).db,f.profileId,"2026-11-01T13:01:00.001Z",()=>{})
const expiredTamper={...f.row,source_evidence:null} const expiredTamper={...f.row,source_evidence:null}
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([expiredTamper]).db,f.profileId,"2026-11-01T14:00:00.000Z",()=>{})) await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([expiredTamper]).db,f.profileId,"2026-11-01T14:00:00.000Z",()=>{}))
const current={...f.row,recovery_version:7} for (const version of [7,8]) {
const current={...f.row,recovery_version:version}
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([current]).db,f.profileId,"2026-11-01T14:00:00.000Z",()=>{}),/lacks durable charge/) await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([current]).db,f.profileId,"2026-11-01T14:00:00.000Z",()=>{}),/lacks durable charge/)
}
const stripped={...f.input};delete stripped.persistentProfileAuthority const stripped={...f.input};delete stripped.persistentProfileAuthority
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([{...f.row,source_evidence:await encryptSecret(JSON.stringify({steps:[{kind:"service",service:"routing.community.scoped",input:stripped}]}))}]).db,f.profileId,at,()=>{})) await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([{...f.row,source_evidence:await encryptSecret(JSON.stringify({steps:[{kind:"service",service:"routing.community.scoped",input:stripped}]}))}]).db,f.profileId,at,()=>{}))
const future={...f.intent,createdAt:"2026-11-01T12:00:00.001Z"} const future={...f.intent,createdAt:"2026-11-01T12:00:00.001Z"}
@@ -112,14 +114,14 @@ test("state transfer cap MODEL refuses oversized or NULL history as a whole, wit
}finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})} }finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})}
}) })
async function retentionFixture() { async function retentionFixture(version = 7) {
const f=await fixture(true,7), authority=f.input.persistentProfileAuthority as {packet:{profileVersion:number;scope:number[]};proof:string} const f=await fixture(true,version), authority=f.input.persistentProfileAuthority as {packet:{profileVersion:number;scope:number[]};proof:string}
const payload=await encryptSecret(JSON.stringify({MODEL:true})) const payload=await encryptSecret(JSON.stringify({MODEL:true}))
const row={...f.row,recovery_payload:payload,authority_profile_version:1} const row={...f.row,recovery_payload:payload,authority_profile_version:1}
const binding=manualInverseReceiptSchema.shape.binding.parse({revision:1,recoveryOperationId:f.intent.operationId,recoveryPlanId:row.recovery_plan_id, const binding=manualInverseReceiptSchema.shape.binding.parse({revision:1,recoveryOperationId:f.intent.operationId,recoveryPlanId:row.recovery_plan_id,
sourceOperationId:f.intent.sourceOperationId,sourcePlanId:row.source_plan_id,sourceStepIndex:0,profileId:f.profileId,profileVersion:1, sourceOperationId:f.intent.sourceOperationId,sourcePlanId:row.source_plan_id,sourceStepIndex:0,profileId:f.profileId,profileVersion:1,
connectionId:f.intent.connectionId,ownerId:f.intent.ownerId,originalPrincipal:f.input.principal,recoveryPrincipal:f.input.principal, connectionId:f.intent.connectionId,ownerId:f.intent.ownerId,originalPrincipal:f.input.principal,recoveryPrincipal:f.input.principal,
operationVersion:7,sourceVersion:7,authorityProof:authority.proof,sourceProof:f.intent.sourceProof,sourceRequestProof:f.input.requestProof, operationVersion:version,sourceVersion:7,authorityProof:authority.proof,sourceProof:f.intent.sourceProof,sourceRequestProof:f.input.requestProof,
recoveryRequestProof:f.intent.requestSeal,sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence), recoveryRequestProof:f.intent.requestSeal,sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence),
recoveryPayloadProof:fingerprint(payload),scope:authority.packet.scope,createdAt:at,chargeUnits:1}) recoveryPayloadProof:fingerprint(payload),scope:authority.packet.scope,createdAt:at,chargeUnits:1})
const receipt={binding,proof:await sign(binding,"routing-manual-inverse-charge-v1")} const receipt={binding,proof:await sign(binding,"routing-manual-inverse-charge-v1")}
@@ -153,7 +155,8 @@ function retentionOwner(f:Awaited<ReturnType<typeof retentionFixture>>,present=t
test('retention MODEL intent2 authenticates SQL clock after parent GC; unknown noReplay anchor survives',async()=>{ test('retention MODEL intent2 authenticates SQL clock after parent GC; unknown noReplay anchor survives',async()=>{
const dir=await mkdtemp(join(tmpdir(),'mm-intent2-retention-')),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir const dir=await mkdtemp(join(tmpdir(),'mm-intent2-retention-')),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir
try { try {
const f=await retentionFixture(),owner=retentionOwner(f),before=fingerprint(f.row) for (const version of [7,8]) {
const f=await retentionFixture(version),owner=retentionOwner(f),before=fingerprint(f.row)
// SQL aging and the transaction owner below are MODELS, never genuine SQL. // SQL aging and the transaction owner below are MODELS, never genuine SQL.
await assertLegacyManualHistoryWithinOwner(owner.db,f.profileId,'2026-11-03T12:00:00.000Z',()=>{}) await assertLegacyManualHistoryWithinOwner(owner.db,f.profileId,'2026-11-03T12:00:00.000Z',()=>{})
assert.deepEqual(await retainManualInverseChargesWithinOwner(owner.db,1),{deleted:1}) assert.deepEqual(await retainManualInverseChargesWithinOwner(owner.db,1),{deleted:1})
@@ -164,6 +167,7 @@ test('retention MODEL intent2 authenticates SQL clock after parent GC; unknown n
assert(candidate.indexOf("state IN('pending','running')")<candidate.indexOf('LIMIT 100')) assert(candidate.indexOf("state IN('pending','running')")<candidate.indexOf('LIMIT 100'))
assert(candidate.includes('r.authority_profile_version IS NOT NULL')) assert(candidate.includes('r.authority_profile_version IS NOT NULL'))
assert.equal(f.intent.revision,2);assert.equal(await decryptSecretExisting(f.row.intent_sealed),JSON.stringify(f.intent)) assert.equal(f.intent.revision,2);assert.equal(await decryptSecretExisting(f.row.intent_sealed),JSON.stringify(f.intent))
}
}finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})} }finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})}
}) })
@@ -182,6 +186,11 @@ test('retention MODEL refuses recent/future/malformed copy, present corrupt pare
const t=retentionOwner(tamper);await assert.rejects(retainManualInverseChargesWithinOwner(t.db,1),/MAC/);assert.equal(t.deleted.length,0) const t=retentionOwner(tamper);await assert.rejects(retainManualInverseChargesWithinOwner(t.db,1),/MAC/);assert.equal(t.deleted.length,0)
const legacy=await fixture(true,7) const legacy=await fixture(true,7)
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([legacy.row]).db,legacy.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/lacks durable/) await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([legacy.row]).db,legacy.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/lacks durable/)
const wrongVersion=await retentionFixture(7);wrongVersion.row.recovery_version=8
const mismatched=retentionOwner(wrongVersion)
await assert.rejects(assertLegacyManualHistoryWithinOwner(mismatched.db,wrongVersion.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/signed allocation/)
await assert.rejects(retainManualInverseChargesWithinOwner(mismatched.db,1),/hint/)
assert.equal(mismatched.deleted.length,0)
// Revision2 payload proof cannot be replaced by an authentic stamp from a different payload. // Revision2 payload proof cannot be replaced by an authentic stamp from a different payload.
const payload=await retentionFixture();payload.row.recovery_payload=await encryptSecret('{}') const payload=await retentionFixture();payload.row.recovery_payload=await encryptSecret('{}')
await assert.rejects(assertLegacyManualHistoryWithinOwner(retentionOwner(payload,false).db,payload.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/payload/) await assert.rejects(assertLegacyManualHistoryWithinOwner(retentionOwner(payload,false).db,payload.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/payload/)
@@ -40,7 +40,7 @@ export async function classifyManualInverseHistory(row:HistoricalRow,observedAt:
intent.receiptId!==row.recovery_receipt_id || intent.validUntil!==iso(row.deadline) || intent.receiptId!==row.recovery_receipt_id || intent.validUntil!==iso(row.deadline) ||
row.source_connection_id!==intent.connectionId || row.recovery_connection_id!==intent.connectionId || row.source_connection_id!==intent.connectionId || row.recovery_connection_id!==intent.connectionId ||
row.source_action!=="routing.community.switch" || row.source_version!==7 || row.source_action!=="routing.community.switch" || row.source_version!==7 ||
row.recovery_action!=="operations.recover" || ![6,7].includes(row.recovery_version??0) || row.recovery_action!=="operations.recover" || ![6,7,8].includes(row.recovery_version??0) ||
!["partial","applied"].includes(row.source_state??"")) throw fail("Historical manual intent/source identity differs") !["partial","applied"].includes(row.source_state??"")) throw fail("Historical manual intent/source identity differs")
const source=await sealedJson(row.source_evidence) as ChangeEvidence, recovery=await sealedJson(row.recovery_evidence) as ChangeEvidence const source=await sealedJson(row.source_evidence) as ChangeEvidence, recovery=await sealedJson(row.recovery_evidence) as ChangeEvidence
const step=source.steps?.[0], inverse=recovery.steps?.[0] const step=source.steps?.[0], inverse=recovery.steps?.[0]
@@ -141,7 +141,7 @@ export async function assertLegacyManualHistoryWithinOwner(db:PoolClient,profile
// Authenticate any durable inverse row before treating this old manual // Authenticate any durable inverse row before treating this old manual
// identity as represented by the normal profile ledger. // identity as represented by the normal profile ledger.
const {authenticateManualInverseHistoryWithinOwner}=await import("./routing-manual-inverse-store.js") const {authenticateManualInverseHistoryWithinOwner}=await import("./routing-manual-inverse-store.js")
if(await authenticateManualInverseHistoryWithinOwner(db,classified.intent,classified.authority,observedAt,{sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence),recoveryPlanId:row.recovery_plan_id,sourcePlanId:row.source_plan_id,sourceRequestProof:classified.sourceRequestProof,sourcePrincipal:classified.sourcePrincipal,recoveryPrincipal:classified.recoveryPrincipal})) continue if(await authenticateManualInverseHistoryWithinOwner(db,classified.intent,classified.authority,observedAt,{sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence),recoveryPlanId:row.recovery_plan_id,recoveryVersion:row.recovery_version,sourcePlanId:row.source_plan_id,sourceRequestProof:classified.sourceRequestProof,sourcePrincipal:classified.sourcePrincipal,recoveryPrincipal:classified.recoveryPrincipal})) continue
if(row.recovery_version!==6) throw fail("Current persistent manual inverse lacks durable charge") if(row.recovery_version!==6) throw fail("Current persistent manual inverse lacks durable charge")
if(classified.authority.packet.profileId!==profileId) continue if(classified.authority.packet.profileId!==profileId) continue
// Historical v6 sealed evidence.validUntil bounded admission; source and // Historical v6 sealed evidence.validUntil bounded admission; source and
@@ -7,6 +7,7 @@ import { effectiveMcpConnection } from "./store.js"
import { decryptSecretExisting, encryptSecret, fingerprint, McpError, privateFingerprinterExisting, privateSecretFingerprint } from "./security.js" import { decryptSecretExisting, encryptSecret, fingerprint, McpError, privateFingerprinterExisting, privateSecretFingerprint } from "./security.js"
import { routingProfileAuthorityBindingSchema } from "./routing-profile-authority.js" import { routingProfileAuthorityBindingSchema } from "./routing-profile-authority.js"
import type { RoutingSwitchLedgerEntry } from "./routing-switch-ledger-contract.js" import type { RoutingSwitchLedgerEntry } from "./routing-switch-ledger-contract.js"
import { findMcpOperation } from "./registry.js"
const fail = (message: string) => new McpError("STATE_CONFLICT", message, 409) const fail = (message: string) => new McpError("STATE_CONFLICT", message, 409)
const uuid = z.string().uuid(), proof = z.string().regex(/^[a-f0-9]{64}$/), stamp = z.string().datetime() const uuid = z.string().uuid(), proof = z.string().regex(/^[a-f0-9]{64}$/), stamp = z.string().datetime()
@@ -14,7 +15,7 @@ const principal = z.object({ connectionId: uuid, credentialId: uuid, credentialE
export const manualInverseBindingSchema = z.object({ revision: z.literal(1), recoveryOperationId: uuid, recoveryPlanId: uuid, export const manualInverseBindingSchema = z.object({ revision: z.literal(1), recoveryOperationId: uuid, recoveryPlanId: uuid,
sourceOperationId: uuid, sourcePlanId: uuid, sourceStepIndex: z.literal(0), profileId: uuid, profileVersion: z.number().int().positive(), sourceOperationId: uuid, sourcePlanId: uuid, sourceStepIndex: z.literal(0), profileId: uuid, profileVersion: z.number().int().positive(),
connectionId: uuid, ownerId: z.string().min(1).max(1000), originalPrincipal: principal, recoveryPrincipal: principal, connectionId: uuid, ownerId: z.string().min(1).max(1000), originalPrincipal: principal, recoveryPrincipal: principal,
operationVersion: z.literal(7), sourceVersion: z.literal(7), authorityProof: proof, sourceProof: proof, operationVersion: z.union([z.literal(7), z.literal(8)]), sourceVersion: z.literal(7), authorityProof: proof, sourceProof: proof,
sourceRequestProof: proof, recoveryRequestProof: proof, sourceEvidenceProof: proof, sourceRequestProof: proof, recoveryRequestProof: proof, sourceEvidenceProof: proof,
recoveryEvidenceProof: proof, recoveryPayloadProof: proof, recoveryEvidenceProof: proof, recoveryPayloadProof: proof,
scope: z.array(z.number().int().positive().safe()).min(1).max(1000), createdAt: stamp, chargeUnits: z.literal(1) }).strict() scope: z.array(z.number().int().positive().safe()).min(1).max(1000), createdAt: stamp, chargeUnits: z.literal(1) }).strict()
@@ -74,7 +75,8 @@ export async function selectManualInverseAdmissionWithinOwner(db: PoolClient, pl
if (plan.operation_id!=="operations.recover") return undefined if (plan.operation_id!=="operations.recover") return undefined
const step=evidence.steps[0] const step=evidence.steps[0]
if (evidence.steps.length!==1 || step.kind!=="service" || step.service!=="routing.community.scoped.recover") return undefined if (evidence.steps.length!==1 || step.kind!=="service" || step.service!=="routing.community.scoped.recover") return undefined
if (plan.operation_version!==7 || step.input.mode!=="manual") throw fail("Manual recovery action version/mode differs") if (plan.operation_version!==findMcpOperation(plan.operation_id)?.version) throw new McpError("PLAN_EXPIRED", "Версия операции изменилась; подготовьте новый план", 409)
if (step.input.mode!=="manual") throw fail("Manual recovery mode differs")
const { loadScopedManualRecoveryAdmissionWithinOwner } = await import("./routing-community-scoped-recovery.js") const { loadScopedManualRecoveryAdmissionWithinOwner } = await import("./routing-community-scoped-recovery.js")
const loaded = await loadScopedManualRecoveryAdmissionWithinOwner(step.input, actor, db) const loaded = await loadScopedManualRecoveryAdmissionWithinOwner(step.input, actor, db)
const { source } = loaded const { source } = loaded
@@ -92,7 +94,7 @@ function expectedBinding(selected: NonNullable<Awaited<ReturnType<typeof selectM
return manualInverseBindingSchema.parse({revision:1,recoveryOperationId:operationId,recoveryPlanId:plan.id, return manualInverseBindingSchema.parse({revision:1,recoveryOperationId:operationId,recoveryPlanId:plan.id,
sourceOperationId:selected.source.row.id,sourcePlanId:selected.source.row.source_plan_id,sourceStepIndex:0,profileId:authority.profileId,profileVersion:authority.profileVersion, sourceOperationId:selected.source.row.id,sourcePlanId:selected.source.row.source_plan_id,sourceStepIndex:0,profileId:authority.profileId,profileVersion:authority.profileVersion,
connectionId:actor.connectionId,ownerId:authority.ownerId,originalPrincipal:principal.parse(selected.input.principal),recoveryPrincipal:actor, connectionId:actor.connectionId,ownerId:authority.ownerId,originalPrincipal:principal.parse(selected.input.principal),recoveryPrincipal:actor,
operationVersion:7,sourceVersion:7,authorityProof:selected.authority.proof,sourceProof:selected.source.sourceProof, operationVersion:plan.operation_version,sourceVersion:selected.source.row.source_version,authorityProof:selected.authority.proof,sourceProof:selected.source.sourceProof,
sourceRequestProof:selected.input.requestProof,recoveryRequestProof:selected.recoveryInput.requestProof, sourceRequestProof:selected.input.requestProof,recoveryRequestProof:selected.recoveryInput.requestProof,
sourceEvidenceProof:selected.sourceEvidenceProof,recoveryEvidenceProof:fingerprint(plan.evidence.sealed), sourceEvidenceProof:selected.sourceEvidenceProof,recoveryEvidenceProof:fingerprint(plan.evidence.sealed),
recoveryPayloadProof:fingerprint(plan.payload.sealed),scope:authority.scope,createdAt:observedAt,chargeUnits:1}) recoveryPayloadProof:fingerprint(plan.payload.sealed),scope:authority.scope,createdAt:observedAt,chargeUnits:1})
@@ -134,7 +136,7 @@ export async function admitManualInverseWithinOwner(db: PoolClient, operationId:
/** A committed charge is required before the existing manual native intent. /** A committed charge is required before the existing manual native intent.
* This authenticates identity only; existing live/native authority still runs. */ * This authenticates identity only; existing live/native authority still runs. */
export async function assertManualInverseChargeWithinOwner(db:PoolClient,operationId:string,input:Record<string,unknown>,actor:McpPrincipal) { export async function assertManualInverseChargeWithinOwner(db:PoolClient,operationId:string,input:Record<string,unknown>,actor:McpPrincipal) {
const rows=(await db.query<StoredPlan>("SELECT p.* FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id WHERE o.id=$1 AND o.connection_id=$2 AND o.state='running' AND p.state='executing' AND p.operation_id='operations.recover' AND p.operation_version=7",[operationId,actor.connectionId])).rows const rows=(await db.query<StoredPlan>("SELECT p.* FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id WHERE o.id=$1 AND o.connection_id=$2 AND o.state='running' AND p.state='executing' AND p.operation_id='operations.recover' AND p.operation_version=$3",[operationId,actor.connectionId,findMcpOperation("operations.recover")?.version])).rows
if(rows.length!==1) throw fail("Manual native claim requires current running action version") if(rows.length!==1) throw fail("Manual native claim requires current running action version")
const plan=rows[0], evidence=JSON.parse(await decryptSecretExisting(plan.evidence.sealed)) as ChangeEvidence const plan=rows[0], evidence=JSON.parse(await decryptSecretExisting(plan.evidence.sealed)) as ChangeEvidence
const step=evidence.steps[0] const step=evidence.steps[0]
@@ -154,7 +156,7 @@ export async function assertManualInverseChargeWithinOwner(db:PoolClient,operati
export async function authenticateManualInverseHistoryWithinOwner(db:PoolClient, export async function authenticateManualInverseHistoryWithinOwner(db:PoolClient,
intent:{revision?:number;manualInverseCharge?:ManualInverseReceipt;operationId:string;sourceOperationId:string;sourceReceiptId:string;sourceProof:string;requestSeal:string;connectionId:string;ownerId:string}, intent:{revision?:number;manualInverseCharge?:ManualInverseReceipt;operationId:string;sourceOperationId:string;sourceReceiptId:string;sourceProof:string;requestSeal:string;connectionId:string;ownerId:string},
authority:z.infer<typeof routingProfileAuthorityBindingSchema>,observedAt:string, authority:z.infer<typeof routingProfileAuthorityBindingSchema>,observedAt:string,
identity:{sourceEvidenceProof:string;recoveryEvidenceProof:string;recoveryPlanId:string|null;sourcePlanId:string|null;sourceRequestProof:unknown;sourcePrincipal:unknown;recoveryPrincipal:unknown}) { identity:{sourceEvidenceProof:string;recoveryEvidenceProof:string;recoveryPlanId:string|null;recoveryVersion:number|null;sourcePlanId:string|null;sourceRequestProof:unknown;sourcePrincipal:unknown;recoveryPrincipal:unknown}) {
const rows=(await db.query<ChargeRow>(`SELECT ${columns} FROM mcp_routing_manual_inverse_charges WHERE recovery_operation_id=$1 OR source_operation_id=$2 LIMIT 3`,[intent.operationId,intent.sourceOperationId])).rows const rows=(await db.query<ChargeRow>(`SELECT ${columns} FROM mcp_routing_manual_inverse_charges WHERE recovery_operation_id=$1 OR source_operation_id=$2 LIMIT 3`,[intent.operationId,intent.sourceOperationId])).rows
const copy = intent.revision === 2 ? await authenticateCopy(intent.manualInverseCharge, observedAt) : undefined const copy = intent.revision === 2 ? await authenticateCopy(intent.manualInverseCharge, observedAt) : undefined
if (!rows.length && !copy) return false if (!rows.length && !copy) return false
@@ -166,7 +168,8 @@ export async function authenticateManualInverseHistoryWithinOwner(db:PoolClient,
value.profileId!==authority.packet.profileId || value.profileVersion!==authority.packet.profileVersion || value.authorityProof!==authority.proof || value.profileId!==authority.packet.profileId || value.profileVersion!==authority.packet.profileVersion || value.authorityProof!==authority.proof ||
value.connectionId!==intent.connectionId || value.ownerId!==intent.ownerId || value.sourceProof!==intent.sourceProof || value.connectionId!==intent.connectionId || value.ownerId!==intent.ownerId || value.sourceProof!==intent.sourceProof ||
value.recoveryRequestProof!==intent.requestSeal || value.sourceEvidenceProof!==identity.sourceEvidenceProof || value.recoveryRequestProof!==intent.requestSeal || value.sourceEvidenceProof!==identity.sourceEvidenceProof ||
value.recoveryEvidenceProof!==identity.recoveryEvidenceProof || value.recoveryPlanId!==identity.recoveryPlanId || value.sourcePlanId!==identity.sourcePlanId)throw fail("Historical inverse is not represented by its signed allocation") value.recoveryEvidenceProof!==identity.recoveryEvidenceProof || value.recoveryPlanId!==identity.recoveryPlanId ||
value.operationVersion!==identity.recoveryVersion || value.sourcePlanId!==identity.sourcePlanId)throw fail("Historical inverse is not represented by its signed allocation")
// Only revision2 needs this extra one-row bounded projection; preserve the // Only revision2 needs this extra one-row bounded projection; preserve the
// existing cursor page payload ceiling and legacy protocol untouched. // existing cursor page payload ceiling and legacy protocol untouched.
const payload = copy ? (await db.query<{sealed:string|null}>("SELECT CASE WHEN octet_length(payload::text)<=1048576 THEN payload->>'sealed' END AS sealed FROM mcp_plans WHERE id=$1 LIMIT 2",[identity.recoveryPlanId])).rows : [] const payload = copy ? (await db.query<{sealed:string|null}>("SELECT CASE WHEN octet_length(payload::text)<=1048576 THEN payload->>'sealed' END AS sealed FROM mcp_plans WHERE id=$1 LIMIT 2",[identity.recoveryPlanId])).rows : []
@@ -201,11 +204,11 @@ export async function retainManualInverseChargesWithinOwner(db: PoolClient, rete
" WHERE r.mode='manual' AND r.source_operation_id=$1 AND r.source_step_index=0 AND r.recovery_operation_id=$2",[candidate.source_operation_id,candidate.recovery_operation_id])).rows " WHERE r.mode='manual' AND r.source_operation_id=$1 AND r.source_step_index=0 AND r.recovery_operation_id=$2",[candidate.source_operation_id,candidate.recovery_operation_id])).rows
if(rows.length!==1)throw fail("Retention intent/source pair disappeared") if(rows.length!==1)throw fail("Retention intent/source pair disappeared")
const row=rows[0],classified=await classifyManualInverseHistory(row,observedAt) const row=rows[0],classified=await classifyManualInverseHistory(row,observedAt)
if(classified.kind!=="persistent" || classified.intent.revision!==2 || row.recovery_version!==7 || if(classified.kind!=="persistent" || classified.intent.revision!==2 || row.recovery_version!==classified.intent.manualInverseCharge.binding.operationVersion ||
row.authority_profile_version!==candidate.profile_version || row.authority_profile_version!==classified.intent.manualInverseCharge.binding.profileVersion) row.authority_profile_version!==candidate.profile_version || row.authority_profile_version!==classified.intent.manualInverseCharge.binding.profileVersion)
throw fail("Retention hint is not an authenticated persistent intent2") throw fail("Retention hint is not an authenticated persistent intent2")
if(!await authenticateManualInverseHistoryWithinOwner(db,classified.intent,classified.authority,observedAt,{ if(!await authenticateManualInverseHistoryWithinOwner(db,classified.intent,classified.authority,observedAt,{
sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence),recoveryPlanId:row.recovery_plan_id,sourcePlanId:row.source_plan_id,sourceRequestProof:classified.sourceRequestProof,sourcePrincipal:classified.sourcePrincipal,recoveryPrincipal:classified.recoveryPrincipal})) sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence),recoveryPlanId:row.recovery_plan_id,recoveryVersion:row.recovery_version,sourcePlanId:row.source_plan_id,sourceRequestProof:classified.sourceRequestProof,sourcePrincipal:classified.sourcePrincipal,recoveryPrincipal:classified.recoveryPrincipal}))
throw fail("Retention parent is not authenticated") throw fail("Retention parent is not authenticated")
const result=await db.query("DELETE FROM mcp_routing_manual_inverse_charges WHERE recovery_operation_id=$1 AND source_operation_id=$2",[candidate.recovery_operation_id,candidate.source_operation_id]) const result=await db.query("DELETE FROM mcp_routing_manual_inverse_charges WHERE recovery_operation_id=$1 AND source_operation_id=$2",[candidate.recovery_operation_id,candidate.source_operation_id])
if(result.rowCount!==1)throw fail("Retention parent delete was not confirmed") if(result.rowCount!==1)throw fail("Retention parent delete was not confirmed")
+116 -7
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict" import assert from "node:assert/strict"
import { test } from "node:test" import { test } from "node:test"
import { routeAiProfileSchema, type RouteObservation } from "@mmapp/contracts/mcp-routing" import { routeAiProfileSchema, type RouteObservation } from "@mmapp/contracts/mcp-routing"
import { rankRoutingCandidates, decideRoutingSwitch, type ControllerState, type PathHealth } from "./routing-model.js" import { rankRoutingCandidates, decideRoutingSwitch, selectFailoverReserveProbes, type ControllerState, type PathHealth } from "./routing-model.js"
const now = Date.parse("2026-10-17T10:00:00Z"), at = new Date(now).toISOString() const now = Date.parse("2026-10-17T10:00:00Z"), at = new Date(now).toISOString()
const profile = routeAiProfileSchema.parse({ name: "Test", serverId: 1, community: "65000:10", family: "ipv4", candidates: [ const profile = routeAiProfileSchema.parse({ name: "Test", serverId: 1, community: "65000:10", family: "ipv4", candidates: [
@@ -43,14 +43,63 @@ test("new unknown attempts block older successful data independently for each me
test("failover requires three distinct failures and a freshly verified eligible standby", () => { test("failover requires three distinct failures and a freshly verified eligible standby", () => {
const ranking = rankRoutingCandidates(profile, [...a, sample("b1", 1, 4, "wan-b"), sample("b2", 4, 3, "exit-b")], now) const ranking = rankRoutingCandidates(profile, [...a, sample("b1", 1, 4, "wan-b"), sample("b2", 4, 3, "exit-b")], now)
let state: ControllerState = {} let state: ControllerState = {}
const health = (id: string, status: PathHealth["status"] = "failed"): PathHealth[] => [{ id, candidateId: "a", status, observedAt: at }, { id: "reserve", candidateId: "b", status: "passed", observedAt: at }] const health = (id: string, time = now, status: PathHealth["status"] = "failed"): PathHealth[] => {
const observedAt = new Date(time).toISOString()
return [{ id, candidateId: "a", status, observedAt }, { id: "reserve", candidateId: "b", status: "passed", observedAt }]
}
for (let i = 0; i < 5; i++) { const next = decideRoutingSwitch(profile, state, ranking, "a", now, health("one")); state = next.state; assert.equal(next.switchTo, null) } for (let i = 0; i < 5; i++) { const next = decideRoutingSwitch(profile, state, ranking, "a", now, health("one")); state = next.state; assert.equal(next.switchTo, null) }
assert.equal(state.failureCount, 1) assert.equal(state.failureCount, 1)
const second = decideRoutingSwitch(profile, state, ranking, "a", now, health("two")); assert.equal(second.switchTo, null) const secondAt = now + 60000, thirdAt = now + 120000
assert.equal(decideRoutingSwitch(profile, second.state, ranking, "a", now, health("three")).switchTo, "b") const second = decideRoutingSwitch(profile, state, ranking, "a", secondAt, health("two", secondAt)); assert.equal(second.switchTo, null)
assert.equal(decideRoutingSwitch(profile, second.state, ranking, "a", now, health("three", "unknown")).switchTo, null) assert.equal(decideRoutingSwitch(profile, second.state, ranking, "a", thirdAt, health("three", thirdAt)).switchTo, "b")
assert.equal(decideRoutingSwitch(profile, { ...second.state, switches: [at, at, at] }, ranking, "a", now, health("three")).switchTo, null) const unknown = decideRoutingSwitch(profile, second.state, ranking, "a", thirdAt, health("three", thirdAt, "unknown"))
assert.equal(decideRoutingSwitch(profile, second.state, ranking, "a", now + 100000, health("three")).switchTo, null) assert.equal(unknown.switchTo, null)
assert.equal(unknown.state.failureObservedAt, undefined)
assert.equal(decideRoutingSwitch(profile, { ...second.state, switches: [at, at, at] }, ranking, "a", thirdAt, health("three", thirdAt)).switchTo, null)
assert.equal(decideRoutingSwitch(profile, second.state, ranking, "a", thirdAt + 100000, health("three", thirdAt)).switchTo, null)
})
test("failover starts a fresh series after downtime or legacy state and ignores reordered failures", () => {
const ranking = rankRoutingCandidates(profile, [...a, sample("b1", 1, 4, "wan-b"), sample("b2", 4, 3, "exit-b")], now)
const last = now + 60000
const persisted: ControllerState = JSON.parse(JSON.stringify({ failureCount: 2, failureKey: "two",
failureObservedAt: new Date(last).toISOString(), failingCandidateId: "a" }))
const check = (state: ControllerState, id: string, time: number, clock = time) => {
const observedAt = new Date(time).toISOString()
return decideRoutingSwitch(profile, state, ranking, "a", clock,
[{ id, candidateId: "a", status: "failed", observedAt }, { id: "reserve", candidateId: "b", status: "passed", observedAt }])
}
for (const result of [check(persisted, "two", last + 60000), check(persisted, "older", last - 1000, last),
check(persisted, "clock-rollback", last - 1000), check(persisted, "equal-time", last)]) {
assert.equal(result.switchTo, null)
assert.equal(result.state.failureCount, 2)
assert.equal(result.state.failureObservedAt, persisted.failureObservedAt)
}
for (const [state, time] of [[persisted, now + 4 * 3600000], [{ ...persisted, failureObservedAt: undefined }, last + 60000]] as const) {
const result = check(state, "next", time)
assert.equal(result.switchTo, null)
assert.equal(result.state.failureCount, 1)
assert.equal(result.state.failureObservedAt, new Date(time).toISOString())
}
})
test("bounded failover probing keeps the highest eligible reserve and eventually visits every other eligible probe", () => {
const controlProbe = { targetInterface: "exit-a", expectedHops: [{ serverId: 3, interface: "exit-a" }] }
const configured = routeAiProfileSchema.parse({ ...profile, excludedCandidateIds: ["05"],
candidates: Array.from({ length: 100 }, (_, index) => ({ ...profile.candidates[0], id: String(index).padStart(2, "0"),
controlProbe: index === 4 ? undefined : controlProbe })) })
const ranking = rankRoutingCandidates(configured, a, now)
const expected = ranking.filter(row => row.eligible && row.candidate.controlProbe && !["00", "01"].includes(row.candidate.id)).map(row => row.candidate.id)
let cursor: string | undefined
for (const alternate of expected) {
const probes = selectFailoverReserveProbes(ranking, "00", cursor)
assert.deepEqual(probes.candidateIds, ["01", alternate])
assert.equal(probes.cursor, alternate)
cursor = probes.cursor
}
assert.deepEqual(selectFailoverReserveProbes(ranking, "00", cursor).candidateIds, ["01", "02"])
assert.deepEqual(selectFailoverReserveProbes(ranking, "00", "05").candidateIds, ["01", "02"])
const pinned = rankRoutingCandidates({ ...configured, pinnedCandidateId: "09" }, a, now)
assert.deepEqual(selectFailoverReserveProbes(pinned, "00"), { candidateIds: ["09"], cursor: undefined })
assert.deepEqual(selectFailoverReserveProbes(pinned, "09"), { candidateIds: [], cursor: undefined })
}) })
test("routing combines RTT, independent losses and bottleneck speed, never claims an observed path", () => { test("routing combines RTT, independent losses and bottleneck speed, never claims an observed path", () => {
const ranked = rankRoutingCandidates(profile, a, now).find(row => row.candidate.id === "a")! const ranked = rankRoutingCandidates(profile, a, now).find(row => row.candidate.id === "a")!
@@ -92,3 +141,63 @@ test("controller does not count polling the same sample as consecutive confirmat
assert.equal(decideRoutingSwitch(profile, { ...state, switches: [at, at, at] }, ranks, "a", now).switchTo, null) assert.equal(decideRoutingSwitch(profile, { ...state, switches: [at, at, at] }, ranks, "a", now).switchTo, null)
assert.equal(decideRoutingSwitch(profile, state, ranks, null, now).switchTo, null) assert.equal(decideRoutingSwitch(profile, state, ranks, null, now).switchTo, null)
}) })
test("hysteresis retains pending confirmation without counting sub-threshold observations or releasing cooldown", () => {
const ranked = rankRoutingCandidates(profile, [...a, sample("b1", 1, 4, "wan-b"), sample("b2", 4, 3, "exit-b")], now)
const compare = (state: ControllerState, gain: number, observation: string, time = now) => {
const ranks = ["b", "a"].map(id => ({ ...ranked.find(row => row.candidate.id === id)!,
score: id === "a" ? 50 : 50 + gain, observationIds: [id + observation] }))
return decideRoutingSwitch(profile, state, ranks, "a", time)
}
const initial = compare({}, 15, "1").state
assert.equal(initial.consecutive, 1)
for (const [gain, observation] of [[6, "2"], [5, "3"]] as const) {
const result = compare(initial, gain, observation)
assert.equal(result.switchTo, null)
assert.deepEqual(result.state, initial)
}
const reset = compare(initial, 4, "4")
assert.equal(reset.state.pendingCandidateId, undefined)
assert.equal(reset.state.consecutive, 0)
const second = compare(initial, 15, "5")
assert.equal(second.switchTo, null)
const third = compare({ ...second.state, lastSwitchAt: at }, 15, "6")
assert.equal(third.state.consecutive, 3)
assert.equal(third.switchTo, null)
const afterCooldown = now + profile.cooldownSec * 1000
assert.equal(compare(third.state, 6, "7", afterCooldown).switchTo, null)
assert.equal(compare(third.state, 15, "8", afterCooldown).switchTo, "b")
})
test("a different pending candidate cannot borrow confirmations from a shared observation key", () => {
const ranks = rankRoutingCandidates(profile, [...a, sample("b1", 1, 4, "wan-b"), sample("b2", 4, 3, "exit-b")], now)
.map(row => ({ ...row, score: row.candidate.id === "a" ? 50 : 75 }))
.sort((left, right) => right.score - left.score)
const observationKey = ranks.flatMap(row => row.observationIds).sort().join(":")
const persisted: ControllerState = { pendingCandidateId: "previous-candidate", consecutive: 3, observationKey }
const selected = decideRoutingSwitch(profile, persisted, ranks, "a", now)
assert.equal(selected.switchTo, null)
assert.equal(selected.state.pendingCandidateId, "b")
assert.equal(selected.state.consecutive, 0)
assert.equal(selected.state.observationKey, observationKey)
const replay = decideRoutingSwitch(profile, selected.state, ranks, "a", now)
assert.equal(replay.switchTo, null)
assert.equal(replay.state.consecutive, 0)
const freshRanks = [{ ...ranks[0], observationIds: ["new-b-1", "new-b-2"] }, ranks[1]]
const fresh = decideRoutingSwitch(profile, replay.state, freshRanks, "a", now)
assert.equal(fresh.state.consecutive, 1)
assert.equal(fresh.switchTo, null)
const belowFloor = [{ ...freshRanks[0], score: 54 }, freshRanks[1]]
const reset = decideRoutingSwitch(profile, fresh.state, belowFloor, "a", now)
assert.equal(reset.state.pendingCandidateId, undefined)
assert.equal(reset.state.consecutive, 0)
const replayAfterReset = decideRoutingSwitch(profile, reset.state, freshRanks, "a", now)
assert.equal(replayAfterReset.switchTo, null)
assert.equal(replayAfterReset.state.consecutive, 0)
})
+31 -9
View File
@@ -51,36 +51,58 @@ export function rankRoutingCandidates(profile: RouteAiProfile, observations: Rou
} }
export interface PathHealth { id: string; candidateId: string; status: "passed" | "failed" | "unknown"; observedAt: string } export interface PathHealth { id: string; candidateId: string; status: "passed" | "failed" | "unknown"; observedAt: string }
export interface ControllerState { pendingCandidateId?: string; consecutive?: number; observationKey?: string; lastSwitchAt?: string; switches?: string[]; operationId?: string; pausedReason?: string; failureCount?: number; failureKey?: string; failingCandidateId?: string } export interface ControllerState { pendingCandidateId?: string; consecutive?: number; observationKey?: string; lastSwitchAt?: string; switches?: string[]; operationId?: string; pausedReason?: string; failureCount?: number; failureKey?: string; failureObservedAt?: string; failingCandidateId?: string }
export function selectFailoverReserveProbes(ranking: ReturnType<typeof rankRoutingCandidates>, currentCandidateId: string, previous?: string) {
const standbys = ranking.filter(({ candidate, eligible }) => eligible && candidate.id !== currentCandidateId && candidate.controlProbe)
.map(({ candidate }) => candidate.id)
const [preferred, ...remaining] = standbys
if (remaining.length === 0) return { candidateIds: standbys, cursor: undefined }
const index = remaining.indexOf(previous ?? "")
const cursor = remaining[(index + 1) % remaining.length]
return { candidateIds: [preferred, cursor], cursor }
}
const pathHealthFreshnessSec = 90
export function decideRoutingSwitch(profile: RouteAiProfile, state: ControllerState, ranking: ReturnType<typeof rankRoutingCandidates>, currentCandidateId: string | null, now = Date.now(), health: PathHealth[] = [], chargedSwitchesPerHour?: number) { export function decideRoutingSwitch(profile: RouteAiProfile, state: ControllerState, ranking: ReturnType<typeof rankRoutingCandidates>, currentCandidateId: string | null, now = Date.now(), health: PathHealth[] = [], chargedSwitchesPerHour?: number) {
const next: ControllerState = { ...state, switches: (state.switches ?? []).filter(at => now - Date.parse(at) < 3600000) } const next: ControllerState = { ...state, switches: (state.switches ?? []).filter(at => now - Date.parse(at) < 3600000) }
if (chargedSwitchesPerHour !== undefined && (!Number.isSafeInteger(chargedSwitchesPerHour) || chargedSwitchesPerHour < next.switches!.length || chargedSwitchesPerHour > 4096)) throw Error("Invalid common routing charge count") if (chargedSwitchesPerHour !== undefined && (!Number.isSafeInteger(chargedSwitchesPerHour) || chargedSwitchesPerHour < next.switches!.length || chargedSwitchesPerHour > 4096)) throw Error("Invalid common routing charge count")
const switchCount = chargedSwitchesPerHour ?? next.switches!.length const switchCount = chargedSwitchesPerHour ?? next.switches!.length
const best = ranking.find(candidate => candidate.eligible), current = ranking.find(candidate => candidate.candidate.id === currentCandidateId) const best = ranking.find(candidate => candidate.eligible), current = ranking.find(candidate => candidate.candidate.id === currentCandidateId)
const currentHealth = health.find(value => value.candidateId === currentCandidateId && fresh(value.observedAt, 90, now)) const currentHealth = health.find(value => value.candidateId === currentCandidateId && fresh(value.observedAt, pathHealthFreshnessSec, now))
if (currentHealth?.status === "failed" && currentCandidateId && !state.pausedReason) { if (currentHealth?.status === "failed" && currentCandidateId && !state.pausedReason) {
if (state.failureKey !== currentHealth.id) { const observed = Date.parse(currentHealth.observedAt), previous = Date.parse(state.failureObservedAt ?? "")
next.failureCount = state.failingCandidateId === currentCandidateId ? (state.failureCount ?? 0) + 1 : 1 const sameCandidate = state.failingCandidateId === currentCandidateId
next.failureKey = currentHealth.id; next.failingCandidateId = currentCandidateId if (state.failureKey === currentHealth.id) {
if (!sameCandidate || observed !== previous) return { state: next, switchTo: null, reason: "Ожидание новой проверки отказа" }
} else {
if (observed <= previous) return { state: next, switchTo: null, reason: "Ожидание новой проверки отказа" }
const continues = sameCandidate && fresh(state.failureObservedAt ?? null, pathHealthFreshnessSec, observed)
next.failureCount = continues ? (state.failureCount ?? 0) + 1 : 1
next.failureKey = currentHealth.id; next.failureObservedAt = currentHealth.observedAt; next.failingCandidateId = currentCandidateId
} }
const reserve = ranking.find(value => value.eligible && value.candidate.id !== currentCandidateId && health.some(check => check.candidateId === value.candidate.id && check.status === "passed" && fresh(check.observedAt, 90, now))) const reserve = ranking.find(value => value.eligible && value.candidate.id !== currentCandidateId && health.some(check => check.candidateId === value.candidate.id && check.status === "passed" && fresh(check.observedAt, pathHealthFreshnessSec, now)))
if ((next.failureCount ?? 0) < 3 || !reserve) return { state: next, switchTo: null, reason: "Failover ожидает три отдельных отказа и подтверждённый резерв" } if ((next.failureCount ?? 0) < 3 || !reserve) return { state: next, switchTo: null, reason: "Failover ожидает три отдельных отказа и подтверждённый резерв" }
if (switchCount >= profile.maxSwitchesPerHour) return { state: { ...next, pausedReason: "Лимит переключений" }, switchTo: null, reason: "Исчерпан часовой бюджет, включая failover" } if (switchCount >= profile.maxSwitchesPerHour) return { state: { ...next, pausedReason: "Лимит переключений" }, switchTo: null, reason: "Исчерпан часовой бюджет, включая failover" }
if (next.lastSwitchAt && now - Date.parse(next.lastSwitchAt) < profile.cooldownSec * 1000) return { state: next, switchTo: null, reason: "Failover ожидает окончания cooldown" } if (next.lastSwitchAt && now - Date.parse(next.lastSwitchAt) < profile.cooldownSec * 1000) return { state: next, switchTo: null, reason: "Failover ожидает окончания cooldown" }
return { state: { ...next, failureCount: 0 }, switchTo: reserve.candidate.id, reason: "Failover: три подтверждённых отказа и проверенный резерв" } return { state: { ...next, failureCount: 0 }, switchTo: reserve.candidate.id, reason: "Failover: три подтверждённых отказа и проверенный резерв" }
} }
next.failureCount = 0; next.failureKey = undefined; next.failingCandidateId = undefined next.failureCount = 0; next.failureKey = undefined; next.failureObservedAt = undefined; next.failingCandidateId = undefined
if (state.pausedReason || !currentCandidateId || !best || best.candidate.id === currentCandidateId) return { state: { ...next, pendingCandidateId: undefined, consecutive: 0 }, switchTo: null, reason: "Нет подтверждённого улучшения или текущего пути" } if (state.pausedReason || !currentCandidateId || !best || best.candidate.id === currentCandidateId) return { state: { ...next, pendingCandidateId: undefined, consecutive: 0 }, switchTo: null, reason: "Нет подтверждённого улучшения или текущего пути" }
if (!current?.eligible || current.score === null) return { state: next, switchTo: null, reason: "Состояние текущего пути неизвестно; failover требует отдельных подтверждённых проверок отказа" } if (!current?.eligible || current.score === null) return { state: next, switchTo: null, reason: "Состояние текущего пути неизвестно; failover требует отдельных подтверждённых проверок отказа" }
const gain = best.score! - current.score const gain = best.score! - current.score
if (gain < Math.max(0, profile.switchThreshold - profile.hysteresis)) return { state: { ...next, consecutive: 0, pendingCandidateId: undefined }, switchTo: null, reason: "Сработал гистерезис" } if (gain < Math.max(0, profile.switchThreshold - profile.hysteresis)) return { state: { ...next, consecutive: 0, pendingCandidateId: undefined }, switchTo: null, reason: "Сработал гистерезис" }
if (gain < profile.switchThreshold && next.pendingCandidateId !== best.candidate.id) return { state: next, switchTo: null, reason: "Порог улучшения не достигнут" } if (gain < profile.switchThreshold) return { state: next, switchTo: null, reason: "Порог улучшения не достигнут" }
const observationKey = [...best.observationIds, ...current.observationIds].sort().join(":") const observationKey = [...best.observationIds, ...current.observationIds].sort().join(":")
if (next.pendingCandidateId !== undefined && next.pendingCandidateId !== best.candidate.id) {
next.pendingCandidateId = best.candidate.id
next.consecutive = 0
}
if (observationKey !== next.observationKey) { if (observationKey !== next.observationKey) {
next.consecutive = next.pendingCandidateId === best.candidate.id ? (next.consecutive ?? 0) + 1 : 1 next.consecutive = next.pendingCandidateId === best.candidate.id ? (next.consecutive ?? 0) + 1 : 1
next.pendingCandidateId = best.candidate.id; next.observationKey = observationKey next.pendingCandidateId = best.candidate.id; next.observationKey = observationKey
} }
if ((next.consecutive ?? 0) < profile.confirmationSamples) return { state: next, switchTo: null, reason: "Ожидание новых измерений" } if (next.pendingCandidateId !== best.candidate.id || (next.consecutive ?? 0) < profile.confirmationSamples) return { state: next, switchTo: null, reason: "Ожидание новых измерений" }
if (next.lastSwitchAt && now - Date.parse(next.lastSwitchAt) < profile.cooldownSec * 1000) return { state: next, switchTo: null, reason: "Cooldown" } if (next.lastSwitchAt && now - Date.parse(next.lastSwitchAt) < profile.cooldownSec * 1000) return { state: next, switchTo: null, reason: "Cooldown" }
if (switchCount >= profile.maxSwitchesPerHour) return { state: { ...next, pausedReason: "Лимит переключений" }, switchTo: null, reason: "Исчерпан часовой бюджет переключений" } if (switchCount >= profile.maxSwitchesPerHour) return { state: { ...next, pausedReason: "Лимит переключений" }, switchTo: null, reason: "Исчерпан часовой бюджет переключений" }
return { state: next, switchTo: best.candidate.id, reason: "Улучшение подтверждено свежими наблюдениями" } return { state: next, switchTo: best.candidate.id, reason: "Улучшение подтверждено свежими наблюдениями" }
@@ -0,0 +1,28 @@
import { isIP } from "node:net"
export function parseRoutingEndpoint(value: unknown) {
if (typeof value !== "string" || !value.length || value.length > 256) throw Error("missing-or-unsupported-field")
const match = /^([^%@/,\s]+)(?:\/(\d+))?(?:%([^%@/,\s]+))?(?:@([^%@/,\s]+))?$/.exec(value)
if (!match) throw Error("unsupported-endpoint")
const [, address, prefix, interfaceName, vrf] = match
const family = isIP(address)
if (!family) throw Error("unsupported-address")
if (prefix !== undefined && prefix !== String(family === 4 ? 32 : 128)) throw Error("unsupported-peer-range")
return { address, interface: interfaceName, vrf }
}
export function matchesBgpSessionContext(
row: Record<string, unknown>,
expected: { table: string; vrf: string },
local: ReturnType<typeof parseRoutingEndpoint>,
remote: ReturnType<typeof parseRoutingEndpoint>,
) {
if (!Object.hasOwn(row, "routing-table") || row["routing-table"] !== expected.table) return false
if (Object.hasOwn(row, "vrf")) {
return row.vrf === expected.vrf && (local.vrf === undefined || local.vrf === expected.vrf) &&
(remote.vrf === undefined || remote.vrf === expected.vrf)
}
// RouterOS may report the transport VRF only in the two runtime addresses.
// The installation table alone cannot identify it.
return local.vrf === expected.vrf && remote.vrf === expected.vrf
}
@@ -14,7 +14,7 @@ async function fixture() {
let nativeReads = 0, profileChecks = 0, sourceReads = 0, configChecks = 0 let nativeReads = 0, profileChecks = 0, sourceReads = 0, configChecks = 0
let nativeBody = "desired", nativeDependency = "dependency", nativeTarget = "desired-target", nativeCandidate = "b", nativeId = "*1", generation = "generation" let nativeBody = "desired", nativeDependency = "dependency", nativeTarget = "desired-target", nativeCandidate = "b", nativeId = "*1", generation = "generation"
let afterNative: (() => void) | undefined, afterCheckpoint: (() => void) | undefined, afterProfile: (() => void) | undefined let afterNative: (() => void) | undefined, afterCheckpoint: (() => void) | undefined, afterProfile: (() => void) | undefined
let sharedReads = 0, sharedClosed = false let sharedReads = 0, sharedClosed = false, manualOwner = false
class ClockDate extends Date { static now() { return now } } class ClockDate extends Date { static now() { return now } }
class BridgeError extends Error { constructor(readonly code: string, message: string) { super(message) } } class BridgeError extends Error { constructor(readonly code: string, message: string) { super(message) } }
const fingerprint = (value: unknown) => JSON.stringify(value) const fingerprint = (value: unknown) => JSON.stringify(value)
@@ -37,6 +37,7 @@ async function fixture() {
const sharedToken = Object.freeze({}), acquisition = Object.freeze({}), sharedBinding = { sourceOperationId: row.id, profile: structuredClone(profile), principal: structuredClone(principal), const sharedToken = Object.freeze({}), acquisition = Object.freeze({}), sharedBinding = { sourceOperationId: row.id, profile: structuredClone(profile), principal: structuredClone(principal),
deadline: now + 10000, contextSources: structuredClone(input.contextSources), candidateId: "b" } deadline: now + 10000, contextSources: structuredClone(input.contextSources), candidateId: "b" }
const providers: Record<string, unknown> = { const providers: Record<string, unknown> = {
"./routing-network-execution-session.js": { isVerifiedManualEffectOwner: (owner: unknown) => owner === sharedToken && manualOwner },
zod: { z }, "@mmapp/contracts/mcp-routing": { routeAiProfileSchema: { parse: (value: unknown) => value } }, zod: { z }, "@mmapp/contracts/mcp-routing": { routeAiProfileSchema: { parse: (value: unknown) => value } },
"../../db/index.js": { pool: { query: async () => { sourceReads++; return { rows: [{ ...row, plan_id: sourceChanged ? "changed" : row.plan_id }] } } } }, "../../db/index.js": { pool: { query: async () => { sourceReads++; return { rows: [{ ...row, plan_id: sourceChanged ? "changed" : row.plan_id }] } } } },
"../../lib/permissions.js": { hasPermission: (values: string[], permission: string) => values.includes(permission) }, "../../lib/permissions.js": { hasPermission: (values: string[], permission: string) => values.includes(permission) },
@@ -93,7 +94,7 @@ async function fixture() {
const reseal = () => { row.evidence.sealed = JSON.stringify(evidence) } const reseal = () => { row.evidence.sealed = JSON.stringify(evidence) }
return { resolve: () => resolve(row.id, action), resolveShared: (token = sharedToken) => resolveShared(token, action), sharedToken, sharedBinding, return { resolve: () => resolve(row.id, action), resolveShared: (token = sharedToken) => resolveShared(token, action), sharedToken, sharedBinding,
consume, close, resolveRound: () => resolveRound(sharedToken, action), readRound: (token: object, read = acquisition) => readRound(token, read), closeRound, acquisition, consume, close, resolveRound: () => resolveRound(sharedToken, action), readRound: (token: object, read = acquisition) => readRound(token, read), closeRound, acquisition,
row, input, profile, checkpoint, packet, action, authority, reseal, row, input, profile, checkpoint, packet, action, authority, reseal, brandManual: () => { manualOwner = true },
stats: () => ({ nativeReads, profileChecks, sourceReads, configChecks, sharedReads, sharedClosed }), advance: (ms: number) => { now += ms }, stats: () => ({ nativeReads, profileChecks, sourceReads, configChecks, sharedReads, sharedClosed }), advance: (ms: number) => { now += ms },
revoke: () => { revoked = true }, sourceChanged: () => { sourceChanged = true }, checkpointMissing: () => { checkpointMissing = true }, revoke: () => { revoked = true }, sourceChanged: () => { sourceChanged = true }, checkpointMissing: () => { checkpointMissing = true },
checkpointChanged: () => { checkpointChanged = true }, guard: () => { guard = true }, checkpointChanged: () => { checkpointChanged = true }, guard: () => { guard = true },
@@ -110,6 +111,25 @@ test("fixed resolver captures opaque context and consume rechecks configuration
assert.equal(f.stats().nativeReads, 2); assert(f.stats().profileChecks > 5) assert.equal(f.stats().nativeReads, 2); assert(f.stats().profileChecks > 5)
await assert.rejects(f.consume(token), /Unknown or consumed/); await assert.rejects(f.resolve(), /already issued/) await assert.rejects(f.consume(token), /Unknown or consumed/); await assert.rejects(f.resolve(), /already issued/)
}) })
for (const mode of ["positive", "unbranded", "wrong-checkpoint", "terminal-source", "standalone"] as const) {
test(`manual revision-2 round ${mode}`, async () => {
const f = await fixture()
f.input.proofRevision = 2; f.input.manualEffectPolicy = "observational-v1"; delete f.input.persistentProfileAuthority
f.row.state = mode === "terminal-source" ? "applied" : "running"; f.row.step_state = "intent"
f.packet.revision = mode === "wrong-checkpoint" ? 2 : 1
if (mode !== "unbranded") f.brandManual()
f.reseal()
if (mode !== "positive") {
await assert.rejects(mode === "standalone" ? f.resolve() : f.resolveRound())
assert.equal(f.stats().nativeReads, 0)
return
}
const round = await f.resolveRound(), observation = await f.readRound(round)
assert.equal(observation.sharedNetworkBudgetVerified, true)
assert.equal(observation.forwardingVerified, false)
})
}
test("caller objects, JSON, another resolver and closed contexts cannot be consumed", async () => { test("caller objects, JSON, another resolver and closed contexts cannot be consumed", async () => {
const f = await fixture(), other = await fixture(), token = await f.resolve() const f = await fixture(), other = await fixture(), token = await f.resolve()
await assert.rejects(f.consume({}), /Unknown or consumed/); await assert.rejects(f.consume(JSON.parse(JSON.stringify(token))), /Unknown or consumed/) await assert.rejects(f.consume({}), /Unknown or consumed/); await assert.rejects(f.consume(JSON.parse(JSON.stringify(token))), /Unknown or consumed/)
@@ -201,12 +221,13 @@ test("missing genuine checkpoint closes shared owner without accepting equality"
test("forward round context reuses one admitted source only through its exact ephemeral acquisition", async () => { test("forward round context reuses one admitted source only through its exact ephemeral acquisition", async () => {
const f = await fixture(), token = await f.resolveRound() const f = await fixture(), token = await f.resolveRound()
assert.equal(f.stats().sharedReads, 0, "Round admission grants no native-success observation")
for (let round = 0; round < 2; round++) { for (let round = 0; round < 2; round++) {
const before = await f.readRound(token), after = await f.readRound(token) const before = await f.readRound(token), after = await f.readRound(token)
assert.equal(before.operationId, f.row.id); assert.equal(after.securityProof, false) assert.equal(before.operationId, f.row.id); assert.equal(after.securityProof, false)
assert.equal(after.forwardingVerified, false); assert.equal(after.sharedNetworkBudgetVerified, true) assert.equal(after.forwardingVerified, false); assert.equal(after.sharedNetworkBudgetVerified, true)
} }
assert.equal(f.stats().sharedReads, 5) assert.equal(f.stats().sharedReads, 4)
await assert.rejects(f.resolveShared(), /already issued/) await assert.rejects(f.resolveShared(), /already issued/)
}) })
@@ -221,3 +242,16 @@ for (const fault of ["clone", "owner", "body", "dependency", "checkpoint", "revo
await assert.rejects(f.readRound(fault === "clone" ? JSON.parse(JSON.stringify(token)) : token, fault === "owner" ? {} : f.acquisition)) await assert.rejects(f.readRound(fault === "clone" ? JSON.parse(JSON.stringify(token)) : token, fault === "owner" ? {} : f.acquisition))
assert.equal(f.stats().sharedClosed, !["clone", "close"].includes(fault)) assert.equal(f.stats().sharedClosed, !["clone", "close"].includes(fault))
}) })
for (const drift of ["body", "dependency", "checkpoint", "revoke"] as const)
test("first forward-round read detects " + drift + " after source-only admission", async () => {
const f = await fixture(), token = await f.resolveRound()
assert.equal(f.stats().nativeReads, 0)
if (drift === "body" || drift === "dependency") f.drift(drift)
if (drift === "checkpoint") f.checkpointChanged()
if (drift === "revoke") f.revoke()
await assert.rejects(f.readRound(token))
assert.equal(f.stats().sharedClosed, true)
await assert.rejects(f.readRound(token))
})
@@ -8,6 +8,7 @@ import { findMcpOperation } from "./registry.js"
import { accessGeneration, decryptSecret, fingerprint, McpError, redact } from "./security.js" import { accessGeneration, decryptSecret, fingerprint, McpError, redact } from "./security.js"
import { assertScopedCommunityRequest } from "./routing-community-scoped-request.js" import { assertScopedCommunityRequest } from "./routing-community-scoped-request.js"
import { loadScopedConfigurationPacket } from "./routing-community-scoped-checkpoint.js" import { loadScopedConfigurationPacket } from "./routing-community-scoped-checkpoint.js"
import { isVerifiedManualEffectOwner } from "./routing-network-execution-session.js"
import { assertPersistentScopedProfileAuthority, routingProfileAuthorityBindingSchema } from "./routing-profile-authority.js" import { assertPersistentScopedProfileAuthority, routingProfileAuthorityBindingSchema } from "./routing-profile-authority.js"
import { readRouteCommunityContext, readRouteCommunityContextWithSharedNativeSession } from "./routing-community-context.js" import { readRouteCommunityContext, readRouteCommunityContextWithSharedNativeSession } from "./routing-community-context.js"
import { readRoutingNetworkScopedCommunityContext } from "./routing-network-scoped-context.js" import { readRoutingNetworkScopedCommunityContext } from "./routing-network-scoped-context.js"
@@ -68,7 +69,9 @@ export async function resolveRoutingNetworkConfigurationContextFromSharedContext
* as the existing bridge. Only its owning session can use it inside a queued * as the existing bridge. Only its owning session can use it inside a queued
* round acquisition; JSON copies and caller-supplied observations cannot. */ * round acquisition; JSON copies and caller-supplied observations cannot. */
export async function resolveRoutingNetworkForwardRoundConfigurationContext(sharedContext: RoutingNetworkSharedContext, action: ActionContext): Promise<RoutingNetworkForwardRoundConfigurationContext> { export async function resolveRoutingNetworkForwardRoundConfigurationContext(sharedContext: RoutingNetworkSharedContext, action: ActionContext): Promise<RoutingNetworkForwardRoundConfigurationContext> {
const initial = await resolveRoutingNetworkConfigurationContextFromSharedContext(sharedContext, action) const binding = inspectRoutingNetworkSharedContext(sharedContext)
if (fingerprint(binding.principal) !== fingerprint(action.principal)) throw fail("Shared configuration principal changed")
const initial = await resolveConfigurationContext(binding.sourceOperationId, action, sharedContext, true)
const entry = contexts.get(initial) const entry = contexts.get(initial)
if (!entry) throw fail("Configuration bridge admission disappeared") if (!entry) throw fail("Configuration bridge admission disappeared")
contexts.delete(initial) contexts.delete(initial)
@@ -96,7 +99,8 @@ export function closeRoutingNetworkForwardRoundConfigurationContext(context: Rou
roundContexts.delete(context); entry?.close() roundContexts.delete(context); entry?.close()
} }
async function resolveConfigurationContext(sourceOperationId: string, action: ActionContext, sharedContext?: RoutingNetworkSharedContext): Promise<RoutingNetworkConfigurationContext> { async function resolveConfigurationContext(sourceOperationId: string, action: ActionContext, sharedContext?: RoutingNetworkSharedContext, deferredRoundReadback = false): Promise<RoutingNetworkConfigurationContext> {
if (deferredRoundReadback && !sharedContext) throw fail("Forward round requires its original shared owner")
z.string().uuid().parse(sourceOperationId) z.string().uuid().parse(sourceOperationId)
const principal = freeze(structuredClone(action.principal)), connection = freeze(structuredClone(action.connection)) const principal = freeze(structuredClone(action.principal)), connection = freeze(structuredClone(action.connection))
const abort = new AbortController() const abort = new AbortController()
@@ -134,9 +138,16 @@ async function resolveConfigurationContext(sourceOperationId: string, action: Ac
if (binding.sourceOperationId !== sourceOperationId || fingerprint(binding.profile) !== fingerprint(profile) || if (binding.sourceOperationId !== sourceOperationId || fingerprint(binding.profile) !== fingerprint(profile) ||
fingerprint(binding.contextSources) !== fingerprint(input.contextSources) || binding.candidateId !== input.candidateId) throw fail("Shared configuration source binding changed") fingerprint(binding.contextSources) !== fingerprint(input.contextSources) || binding.candidateId !== input.candidateId) throw fail("Shared configuration source binding changed")
} }
if (input.proofRevision !== 3 || input.persistentProfileAuthority === undefined || profile.autoRecovery) throw fail("Persistent manual revision-3 source required; recovery guard remains closed") const manual = !!sharedContext && deferredRoundReadback && isVerifiedManualEffectOwner(sharedContext)
if (profile.autoRecovery) throw fail("Recovery guard remains closed")
if (manual) {
if (row.state !== "running" || row.step_state !== "intent" || input.proofRevision !== 2 ||
input.persistentProfileAuthority !== undefined || input.manualEffectPolicy !== "observational-v1") throw fail("Exact running manual effect source required")
} else {
if (input.proofRevision !== 3 || input.persistentProfileAuthority === undefined) throw fail("Persistent manual revision-3 source required")
const authority = routingProfileAuthorityBindingSchema.parse(input.persistentProfileAuthority) const authority = routingProfileAuthorityBindingSchema.parse(input.persistentProfileAuthority)
deadline = Math.min(deadline, Date.parse(authority.packet.validUntil)) deadline = Math.min(deadline, Date.parse(authority.packet.validUntil))
}
signal = AbortSignal.any([signal, AbortSignal.timeout(Math.max(1, deadline - Date.now()))]) signal = AbortSignal.any([signal, AbortSignal.timeout(Math.max(1, deadline - Date.now()))])
active() active()
const ids = [...new Set([profile.serverId, ...profile.candidates.flatMap(candidate => [candidate.jumpHostId, candidate.exitNodeId])])].sort((a,b) => a-b) const ids = [...new Set([profile.serverId, ...profile.candidates.flatMap(candidate => [candidate.jumpHostId, candidate.exitNodeId])])].sort((a,b) => a-b)
@@ -157,7 +168,7 @@ async function resolveConfigurationContext(sourceOperationId: string, action: Ac
const checkpoint = async () => { const checkpoint = async () => {
await authorize() await authorize()
const result = await guarded(() => loadScopedConfigurationPacket(input, requestSeal, { operationId: sourceOperationId, stepIndex: 0 })) const result = await guarded(() => loadScopedConfigurationPacket(input, requestSeal, { operationId: sourceOperationId, stepIndex: 0 }))
if (!result || result.packet.revision !== 2 || result.packet.operationId !== sourceOperationId || result.packet.stepIndex !== 0 || if (!result || result.packet.revision !== (manual ? 1 : 2) || result.packet.operationId !== sourceOperationId || result.packet.stepIndex !== 0 ||
result.packet.phase !== "CONFIG_APPLIED" || Date.parse(result.observedAt) < row.created_at.getTime() || Date.parse(result.observedAt) > Date.now() || result.packet.phase !== "CONFIG_APPLIED" || Date.parse(result.observedAt) < row.created_at.getTime() || Date.parse(result.observedAt) > Date.now() ||
Date.parse(result.observedAt) >= deadline) throw fail("Genuine persistent CONFIG_APPLIED unavailable") Date.parse(result.observedAt) >= deadline) throw fail("Genuine persistent CONFIG_APPLIED unavailable")
await authorize() await authorize()
@@ -190,7 +201,11 @@ async function resolveConfigurationContext(sourceOperationId: string, action: Ac
bodyIdentity: fingerprint(original.packet.desiredRule), dependencyIdentity: original.packet.dependencyProof }, bodyIdentity: fingerprint(original.packet.desiredRule), dependencyIdentity: original.packet.dependencyProof },
securityProof: false as const, forwardingVerified: false as const, sharedNetworkBudgetVerified: !!sharedContext }) securityProof: false as const, forwardingVerified: false as const, sharedNetworkBudgetVerified: !!sharedContext })
} }
await readback() // A forward-round token carries no native success observation. Its fixed
// owner must perform a fresh read immediately BEFORE every snapshot and
// another immediately AFTER. Avoid an unused eager native read here;
// independent/shared one-shot bridge semantics remain unchanged.
if (!deferredRoundReadback) await readback()
const token = Object.freeze({}) as RoutingNetworkConfigurationContext const token = Object.freeze({}) as RoutingNetworkConfigurationContext
contexts.set(token, { consumed: false, close, read: readback, consume: async () => { contexts.set(token, { consumed: false, close, read: readback, consume: async () => {
try { return await readback() } try { return await readback() }
@@ -0,0 +1,91 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import test from 'node:test';
import { createHash } from 'node:crypto';
const require = createRequire(new URL('../../../package.json', import.meta.url));
const ts = require('typescript');
const code = ts.transpileModule(await readFile(new URL('./routing-network-execution-session.ts', import.meta.url), 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }
}).outputText;
function fixture() {
const now = Date.now();
const metadata = { approved: true, phase: 'forward', stepIndex: 0, operationName: 'routing.community.switch', operationVersion: 7,
serviceName: 'routing.community.scoped', operationId: 'real-operation', planId: 'real-plan', principal: { connectionId: 'real-connection', credentialExpiresAt: new Date(now + 90000).toISOString() },
serviceInput: { persistentProfileAuthority: { packet: { validUntil: new Date(now + 70000).toISOString() } } } };
metadata.sealedEvidenceProof = createHash('sha256').update('evidence-ciphertext').digest('hex');
metadata.sealedPayloadProof = createHash('sha256').update('payload-ciphertext').digest('hex');
const original = structuredClone(metadata);
const row = { created_at: new Date(now - 40000), expires_at: new Date(now + 100000), evidence: { sealed: 'evidence-ciphertext' }, payload: { sealed: 'payload-ciphertext' },
intended_state: { kind: 'service', service: metadata.serviceName, input: structuredClone(metadata.serviceInput) } };
const lifetime = { expiresAt: now + 60000, monotonicDeadline: performance.now() + 60000, signal: new AbortController().signal };
let calls = 0, inspections = 0, closed = 0, options, query, originAvailable = true, afterCreate = () => {};
const connection = { owner_valid_until: new Date(now + 110000), policy: { expiresAt: new Date(now + 120000).toISOString() } };
const token = Object.freeze({}), sharedContext = Object.freeze({});
const providers = {
'node:crypto': { createHash },
'../../db/index.js': { pool: { query: async value => { query = value; return { rows: [row] }; } } },
'../../services/config-mutation-policy.js': {
captureVerifiedMcpServiceExecution: async () => { if (!originAvailable) throw Error('no-origin'); return token; },
inspectVerifiedMcpServiceExecution: async value => { assert.equal(value, token); inspections++; return structuredClone(metadata); },
inspectVerifiedMcpServiceExecutionLifetime: async value => { assert.equal(value, token); return lifetime; }
},
'./store.js': { effectiveMcpConnection: async principal => { assert.deepEqual(principal, original.principal); return connection; } },
'./registry.js': { findMcpOperation: () => ({ version: 7 }) },
'./security.js': { fingerprint: JSON.stringify, redact: value => value, McpError: Error },
'./routing-profile-authority.js': { routingProfileAuthorityBindingSchema: { parse: value => value } },
'./routing-network-native-session.js': { createRoutingNetworkNativeSession: async value => { calls++; options = value; afterCreate(); return { sharedContext, close() { closed++; } }; } }
};
const exports = {};
new Function('exports', 'require', code)(exports, name => { assert.ok(providers[name], name); return providers[name]; });
const manifest = { servers: [1, 2], probes: [] };
return { metadata, row, lifetime, connection, manifest, now, sharedContext, isManual: exports.isVerifiedManualEffectOwner, run: () => exports.createRoutingNetworkSessionForCurrentExecution(manifest),
revokeOrigin: () => { originAvailable = false; }, afterCreate: fn => { afterCreate = fn; },
stats: () => ({ calls, closed, inspections, options, query }) };
}
for (const mode of ['positive', 'missing-policy', 'missing-selection', 'recovery', 'late-revocation']) {
test(`manual effect owner origin ${mode}`, async () => {
const f = fixture();
f.metadata.serviceInput = { proofRevision: 2, manualEffectPolicy: 'observational-v1', profile: { autoRecovery: mode === 'recovery' }, originalEffectSelection: { status: 'prepared' } };
if (mode === 'missing-policy') delete f.metadata.serviceInput.manualEffectPolicy;
if (mode === 'missing-selection') delete f.metadata.serviceInput.originalEffectSelection;
if (mode === 'late-revocation') f.afterCreate(() => { f.metadata.approved = false; });
f.row.intended_state.input = structuredClone(f.metadata.serviceInput);
if (mode === 'positive') {
await f.run();
assert.equal(f.isManual(f.sharedContext), true);
assert.equal(f.isManual(JSON.parse(JSON.stringify(f.sharedContext))), false);
} else {
await assert.rejects(f.run());
assert.equal(f.isManual(f.sharedContext), false);
assert.equal(f.stats().calls, mode === 'late-revocation' ? 1 : 0);
assert.equal(f.stats().closed, mode === 'late-revocation' ? 1 : 0);
}
});
}
test('real origin supplies operation, principal, signal and minimum immutable horizon', async () => {
const f = fixture(); await f.run(); const { options, query } = f.stats();
assert.equal(options.sourceOperationId, f.metadata.operationId); assert.equal(options.context.signal, f.lifetime.signal);
assert.equal(options.deadline, f.lifetime.expiresAt - 15000); assert.equal(options.manifest, f.manifest);
assert.deepEqual(query.values, [f.metadata.operationId, f.metadata.principal.connectionId, f.metadata.planId]);
assert.match(query.text, /o.state='running'/); assert.match(query.text, /s.state='intent'/);
});
test('elapsed original 120 seconds cannot be renewed from current time', async () => {
const f = fixture(); f.row.created_at = new Date(f.now - 120001); await assert.rejects(f.run); assert.equal(f.stats().calls, 0);
});
for (const [name, change] of [
['plan', f => { f.row.expires_at = new Date(f.now + 30000); }],
['owner', f => { f.connection.owner_valid_until = new Date(f.now + 30000); }],
['policy', f => { f.connection.policy.expiresAt = new Date(f.now + 30000).toISOString(); }],
['profile', f => { f.metadata.serviceInput.persistentProfileAuthority.packet.validUntil = new Date(f.now + 30000).toISOString(); f.row.intended_state.input = structuredClone(f.metadata.serviceInput); }],
['operation', f => { f.row.created_at = new Date(f.now - 90000); }]
]) test(`minimum ${name} expiry is conserved with publication reserve`, async () => { const f = fixture(); change(f); await f.run(); assert.equal(f.stats().options.deadline, f.now + 15000); });
for (const [key, value] of [['approved', false], ['phase', 'recovery'], ['stepIndex', 1], ['operationName', 'operations.recover'], ['operationVersion', 6], ['serviceName', 'routing.community']]) {
test(`wrong live origin ${key} is denied before native factory`, async () => { const f = fixture(); f.metadata[key] = value; await assert.rejects(f.run); assert.equal(f.stats().calls, 0); });
}
test('outside verified origin cannot supply authority', async () => { const f = fixture(); f.revokeOrigin(); await assert.rejects(f.run); assert.equal(f.stats().calls, 0); });
test('persisted intended state must match the live service input', async () => { const f = fixture(); f.row.intended_state.input = {}; await assert.rejects(f.run); assert.equal(f.stats().calls, 0); });
for (const field of ['evidence', 'payload']) test(`changed randomized sealed ${field} is denied before native`, async () => { const f = fixture(); f.row[field].sealed = 'changed'; await assert.rejects(f.run); assert.equal(f.stats().calls, 0); });
test('monotonic expiry denies open even with unexpired wall horizon', async () => { const f = fixture(); f.lifetime.monotonicDeadline = performance.now() - 1; await assert.rejects(f.run); assert.equal(f.stats().calls, 0); });
test('late origin change closes the newly opened native owner', async () => { const f = fixture(); f.afterCreate(() => { f.metadata.planId = 'changed'; }); await assert.rejects(f.run); assert.equal(f.stats().closed, 1); });
test('returned native authorize callback remains tied to original metadata', async () => { const f = fixture(); await f.run(); f.metadata.operationId = 'changed'; await assert.rejects(f.stats().options.authorize); });
@@ -0,0 +1,77 @@
import { createHash } from "node:crypto"
import type { QueryConfig } from "pg"
import { pool } from "../../db/index.js"
import { captureVerifiedMcpServiceExecution, inspectVerifiedMcpServiceExecution, inspectVerifiedMcpServiceExecutionLifetime } from "../../services/config-mutation-policy.js"
import { effectiveMcpConnection } from "./store.js"
import { findMcpOperation } from "./registry.js"
import { fingerprint, McpError, redact } from "./security.js"
import { routingProfileAuthorityBindingSchema } from "./routing-profile-authority.js"
import { createRoutingNetworkNativeSession, type RoutingNetworkNativeManifest, type RoutingNetworkSharedContext } from "./routing-network-native-session.js"
const manualEffectOwners = new WeakSet<RoutingNetworkSharedContext>()
/** Only the verified execution factory can mark a genuine shared owner. */
export function isVerifiedManualEffectOwner(owner: RoutingNetworkSharedContext): boolean {
return manualEffectOwners.has(owner)
}
/** Private producer for the existing native/configuration-round reader. The
* manifest selects reads, never identity or authority. Its exact scope, probe
* ownership and whole-session budget are still checked by the native factory.
* Only the original live verified service step can open or use this session.
* No operation/principal/deadline/authorization callback is caller supplied.
* This grants no write, recovery or authenticated log-interval coverage. */
export async function createRoutingNetworkSessionForCurrentExecution(manifest: RoutingNetworkNativeManifest): Promise<Awaited<ReturnType<typeof createRoutingNetworkNativeSession>>> {
const fail = () => new McpError("STATE_CONFLICT", "Нет исходного running scoped execution для native acquisition", 409)
const origin = await captureVerifiedMcpServiceExecution()
const initial = await inspectVerifiedMcpServiceExecution(origin)
if (!initial.approved || initial.phase !== "forward" || initial.stepIndex !== 0 ||
initial.operationName !== "routing.community.switch" || initial.operationVersion !== findMcpOperation(initial.operationName)?.version ||
initial.serviceName !== "routing.community.scoped") throw fail()
const input = initial.serviceInput, manual = input.proofRevision === 2
if (manual && (input.manualEffectPolicy !== "observational-v1" || input.persistentProfileAuthority !== undefined ||
(input.profile as { autoRecovery?: unknown } | undefined)?.autoRecovery !== false ||
(input.originalEffectSelection as { status?: unknown } | undefined)?.status !== "prepared")) throw fail()
const lifetime = await inspectVerifiedMcpServiceExecutionLifetime(origin)
const authorize = async () => {
const current = await inspectVerifiedMcpServiceExecution(origin)
if (fingerprint(current) !== fingerprint(initial)) throw fail()
}
await authorize()
const connection = await effectiveMcpConnection(initial.principal)
await authorize()
// The service lifetime may be narrower than plan expiry, but it does not
// replace the operation-created-at +120s native horizon. Never renew at now.
const query: QueryConfig & { query_timeout: number } = { text: `SELECT o.created_at,p.expires_at,
CASE WHEN octet_length(p.evidence::text)<=1048576 THEN p.evidence END AS evidence,
CASE WHEN octet_length(p.payload::text)<=1048576 THEN p.payload END AS payload,
CASE WHEN octet_length(s.intended_state::text)<=1048576 THEN s.intended_state END AS intended_state FROM mcp_operations o
JOIN mcp_plans p ON p.id=o.plan_id JOIN mcp_operation_steps s ON s.operation_id=o.id
WHERE o.id=$1 AND o.connection_id=$2 AND o.plan_id=$3 AND o.state='running'
AND s.step_index=0 AND s.state='intent' LIMIT 2`,
values: [initial.operationId, initial.principal.connectionId, initial.planId],
query_timeout: Math.max(1, Math.min(10000, lifetime.expiresAt - Date.now())) }
const source = await pool.query(query)
await authorize()
if (source.rows.length !== 1 || !(source.rows[0].created_at instanceof Date) || !(source.rows[0].expires_at instanceof Date)) throw fail()
const sha = (value: string) => createHash("sha256").update(value).digest("hex")
if (typeof source.rows[0].evidence?.sealed !== "string" || typeof source.rows[0].payload?.sealed !== "string" ||
sha(source.rows[0].evidence.sealed) !== initial.sealedEvidenceProof || sha(source.rows[0].payload.sealed) !== initial.sealedPayloadProof) throw fail()
if (fingerprint(source.rows[0].intended_state) !== fingerprint(redact({ kind: "service", service: initial.serviceName, input: initial.serviceInput }))) throw fail()
const authorityExpiry = initial.serviceInput.persistentProfileAuthority === undefined ? Infinity :
Date.parse(routingProfileAuthorityBindingSchema.parse(initial.serviceInput.persistentProfileAuthority).packet.validUntil)
const deadline = Math.min(source.rows[0].created_at.getTime() + 120000, source.rows[0].expires_at.getTime(), lifetime.expiresAt,
Date.parse(initial.principal.credentialExpiresAt), connection.owner_valid_until.getTime(), Date.parse(connection.policy.expiresAt), authorityExpiry)
// Reserve publication/current-authority time for the existing manual result.
// This only narrows the original horizon; it never creates a renewed grant.
const readDeadline = deadline - 15000
if (!Number.isFinite(readDeadline) || Date.now() >= readDeadline || lifetime.signal.aborted || performance.now() >= lifetime.monotonicDeadline) throw fail()
const session = await createRoutingNetworkNativeSession({ sourceOperationId: initial.operationId,
context: { principal: initial.principal, connection, signal: lifetime.signal }, authorize, deadline: readDeadline, manifest })
try {
await authorize()
if (manual) manualEffectOwners.add(session.sharedContext)
return session
}
catch (error) { session.close(); throw error }
}
Loaded 100 of 313 files, more files were not shown because too many files have changed in this diff. Show more