Compare commits

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

No files matched your search

+1 -3
View File
@@ -155,9 +155,7 @@ jobs:
cp -R packages/contracts/src "$STAGING/packages/contracts/"
cp backend/package.json backend/tsconfig.json backend/mcp-coverage.json "$STAGING/backend/"
cp -R backend/src "$STAGING/backend/"
mkdir -p "$STAGING/backend/scripts/writer-profile"
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/"
node backend/scripts/stage-database-operator.mjs "$STAGING/backend/scripts"
mkdir -p "$STAGING/deploy/netflow-agent"
cp deploy/netflow-agent/install.sh "$STAGING/deploy/netflow-agent/"
if [ -d backend/drizzle ]; then
+7 -5
View File
@@ -16,7 +16,7 @@ permissions:
packages: write
jobs:
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
timeout-minutes: 40
env:
@@ -24,6 +24,7 @@ jobs:
EXPECTED_SHA: ${{ inputs.expected_sha }}
DISPATCH_SHA: ${{ gitea.sha }}
DISPATCH_EVENT: ${{ gitea.event_name }}
DISPATCH_REF: ${{ gitea.ref }}
REPOSITORY: ${{ gitea.repository }}
RUN_ID: ${{ gitea.run_id }}
RUN_ATTEMPT: ${{ gitea.run_attempt }}
@@ -42,6 +43,7 @@ jobs:
run: |
set -euo pipefail
test "$DISPATCH_EVENT" = workflow_dispatch
test "$DISPATCH_REF" = refs/heads/codex/mcp-acceptance-current-20261103
[[ "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]
test "$EXPECTED_SHA" = "$DISPATCH_SHA"
test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
@@ -88,9 +90,7 @@ jobs:
if [ -d backend/drizzle ]; then
cp -R backend/drizzle "$STAGING/backend/"
fi
mkdir -p "$STAGING/backend/scripts/writer-profile"
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/"
node backend/scripts/stage-database-operator.mjs "$STAGING/backend/scripts"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to existing Gitea registry
@@ -139,7 +139,7 @@ jobs:
if-no-files-found: error
retention-days: 14
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
timeout-minutes: 40
env:
@@ -147,6 +147,7 @@ jobs:
EXPECTED_SHA: ${{ inputs.expected_sha }}
DISPATCH_SHA: ${{ gitea.sha }}
DISPATCH_EVENT: ${{ gitea.event_name }}
DISPATCH_REF: ${{ gitea.ref }}
REPOSITORY: ${{ gitea.repository }}
RUN_ID: ${{ gitea.run_id }}
RUN_ATTEMPT: ${{ gitea.run_attempt }}
@@ -178,6 +179,7 @@ jobs:
run: |
set -euo pipefail
test "$DISPATCH_EVENT" = workflow_dispatch
test "$DISPATCH_REF" = refs/heads/codex/mcp-acceptance-current-20261103
[[ "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]
test "$EXPECTED_SHA" = "$DISPATCH_SHA"
test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
+9 -6
View File
@@ -924,18 +924,21 @@ export default function TrafficPage() {
})
return () => { cancelled = true }
}
void Promise.all([
getTrafficFlows(backendUrl, range),
getFlowExporters(backendUrl, range),
getFlowClients(backendUrl, range),
]).then(([stats, exporters, clients]) => {
const loadFlowCards = async () => {
// These reads share the collector's constrained admission budget.
const stats = await getTrafficFlows(backendUrl, range)
if (cancelled) return
const exporters = await getFlowExporters(backendUrl, range)
if (cancelled) return
const clients = await getFlowClients(backendUrl, range)
if (cancelled) return
setFlowResult({ key: flowKey, receivedAt: Date.now(), stats, exporters: exporters.exporters, clients: clients.clients })
setSelectedId(prev => {
const list = flowScope === "users" ? clients.clients : exporters.exporters
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 : "Не удалось загрузить потоки" })
})
return () => { cancelled = true }
+4 -2
View File
@@ -74,6 +74,7 @@ function UsersContent() {
const isLive = mode === "live"
const canRead = can("mm:users:read")
const canWrite = can("mm:users:write")
const canManagePermissions = !isLive || can("mm:settings:admin")
const canReadServers = can("mm:servers:read")
const [users, setUsers] = useState<AppUser[]>(() => isLive ? [] : INIT_USERS)
@@ -184,8 +185,8 @@ function UsersContent() {
setSaving(true); setSaveError(null); setOperationNotice(null)
try {
const saved = await saveUserSteps(form, checkpoint, {
create: f => createAppUser(backendUrl, userProfileRequest(f), init()),
update: (id, f) => updateAppUser(backendUrl, id, userProfileRequest(f), init()),
create: f => createAppUser(backendUrl, userProfileRequest(f, canManagePermissions), init()),
update: (id, f) => updateAppUser(backendUrl, id, userProfileRequest(f, canManagePermissions), init()),
read: id => getAppUser(backendUrl, id, init()),
removeBinding: (userId, id) => deleteUserBinding(backendUrl, userId, id, init()),
addBinding: (userId, b) => createUserBinding(backendUrl, userId, {
@@ -328,6 +329,7 @@ function UsersContent() {
users={users}
servers={serverOptions}
isLive={isLive}
canManagePermissions={canManagePermissions}
backendUrl={backendUrl}
saving={saving}
saveError={saveError}
+10 -1
View File
@@ -1,7 +1,16 @@
@import "tailwindcss";
@import "tailwindcss" source(none);
@import "tw-animate-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. */
.network-map-focusable {
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": [],
"reason": "Управление самим MCP, транспорт или машинная авторизация: отдельного AI-инструмента нет."
},
{
"method": "GET",
"path": "/api/mcp/database/receipts/:id/audit-status",
"status": "excluded",
"operations": [],
"reason": "Человеческая административная сверка аудита внешней квитанции восстановления: управление самим MCP, без отдельного AI-инструмента."
},
{
"method": "GET",
"path": "/api/mcp/devices/:serverId/:resource",
@@ -590,6 +597,13 @@
"operations": [],
"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",
"path": "/api/mcp/drift",
@@ -1604,6 +1618,13 @@
"operations": [],
"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",
"path": "/api/mcp/management-channels",
+14 -8
View File
@@ -7,11 +7,15 @@
"test:mcp:module-completion": "tsx src/services/ipsec-server-init-model.test.ts && tsx src/modules/mcp/ipsec-server-init-actions.test.ts && tsx src/services/wireguard-revision-restore.test.ts && tsx src/modules/mcp/vrf-ticket-runtime.test.ts && tsx src/services/ipsec-restore.test.ts && tsx src/services/ipsec-pool-dependencies.test.ts && tsx src/services/vrf-production-transition.test.ts && tsx src/services/vrf-production-action-binding.test.ts && tsx src/services/ipsec-revision-restore-model.test.ts && tsx src/modules/mcp/ipsec-revision-actions.test.ts && tsx src/modules/mcp/service-native-progress.test.ts && tsx src/services/ipsec-revision-restore.test.ts && tsx src/services/ipsec-revision-secret.test.ts",
"test:mcp:module-completion:pg": "node scripts/test-mcp-postgres.mjs --module-completion --restore",
"test:mcp:pg": "node scripts/test-mcp-postgres.mjs",
"test:mcp:users-bindings:pg": "node scripts/test-mcp-postgres.mjs --users-bindings",
"test:mcp:bridge-vlan:pg": "node scripts/test-mcp-postgres.mjs --bridge-vlan-configuration",
"test:mcp:external-process:pg": "node scripts/mcp-external-process/run-owned.mjs",
"test:mcp:bridge-vlan:sdk": "node scripts/mcp-external-process/run-owned.mjs .. bridge-vlan",
"test:traffic-flow:ownership": "tsx src/services/traffic-flow-batch-proof.test.ts && tsx src/services/traffic-flow-owned-batch.test.ts && tsx src/services/traffic-flow-owned-writer.test.ts && tsx src/services/traffic-flow-write-transaction.test.ts && tsx src/services/traffic-flow-writer-exclusion.test.ts && tsx src/services/traffic-flow-drain-protocol.test.ts && tsx src/services/traffic-flow-facts-rebuild-execution.test.ts && tsx src/services/traffic-flow-facts-rebuild-source.test.ts && tsx src/services/traffic-flow-facts-rebuild-classifier.test.ts && tsx src/services/traffic-flow-facts-rebuild-service.test.ts && tsx src/services/traffic-flow-ripe-prune-owned.test.ts && tsx src/services/traffic-flow-engine-owned.test.ts && tsx src/routes/traffic-flow-rebuild.test.ts && tsx src/services/traffic-flow-settings-execution.test.ts",
"test:traffic-flow:rebuild:pg": "node scripts/test-mcp-postgres.mjs --flow-rebuild",
"test:traffic-flow:pg": "node scripts/test-mcp-postgres.mjs --flow-writes",
"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",
"mcp:stdio": "node dist/scripts/mcp-stdio.js",
"mcp:stdio:dev": "tsx src/scripts/mcp-stdio.ts",
@@ -33,11 +37,11 @@
"test:probes": "npm run test:speed-workflow && tsx src/services/manual-data-collection-execution.test.ts && tsx src/routes/manual-data-collection.test.ts && tsx src/services/network-host-resolution.test.ts && tsx src/routes/probes.test.ts && tsx src/routes/probes.cancel.test.ts && tsx src/services/ping-measurement.test.ts && tsx src/services/uptime-probe-observations.test.ts && tsx src/services/ping-measurement-sql.test.ts",
"test:exec": "tsx src/services/mikrotik-logs.test.ts && tsx src/services/terminal-diagnostics.test.ts && tsx src/routes/exec.test.ts && tsx src/plugins/auth.exec.test.ts",
"test:auth": "tsx --test src/modules/mcp/routing-manual-inverse-history.test.ts && tsx src/db/application-writer-profile-registry-structure.test.ts && tsx src/db/application-writer-profile-registry-read.test.ts && tsx src/db/application-writer-profile-registry-read-parity.test.ts && tsx src/db/application-writer-profile-format.test.ts && tsx src/plugins/auth-path.test.ts && tsx src/lib/permissions.test.ts && tsx src/plugins/auth-provenance.test.ts && tsx src/services/bandwidth-rest-speed-original.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts && npm run test:mcp",
"test:mcp": "tsx --test src/services/server-poll-error-provenance.test.ts && tsx --test src/modules/mcp/plan-expiry.test.ts src/modules/mcp/result-budget.test.ts src/modules/mcp/result-delivery.test.ts src/modules/mcp/wire-admission.test.ts src/modules/mcp/flow-envelope-allowance.test.ts src/modules/mcp/sdk-terminal-budget.test.ts src/modules/mcp/inflight-owner.test.ts && npm run test:database-source-binding && tsx --test src/modules/mcp/integration-fixture-expiry.test.ts && npm run test:mcp:operation-origin && npm run test:mcp:service-context && npm run test:mcp:operation-locks && tsx --test src/modules/mcp/routing-community-public.test.ts src/modules/mcp/management-transport.test.ts src/modules/mcp/routing-community-context-model.test.ts src/modules/mcp/routing-community-context-native.test.ts ../packages/contracts/src/mcp-routing.test.ts && tsx src/modules/mcp/human-artifact-receipts.test.ts && tsx src/modules/mcp/chr-overlay-readiness.test.ts && tsx src/modules/mcp/chr-recovery-source.test.ts && tsx src/modules/mcp/chr-native-failure.test.ts && tsx src/modules/mcp/ospf-owner-graph.test.ts && tsx src/modules/mcp/routing-measurement-address.test.ts && tsx src/modules/mcp/routing-observation-source.test.ts && tsx src/services/traffic-flow-talkers-model.test.ts && tsx src/services/bgp-prefix-model.test.ts && tsx src/services/vrf-members.test.ts && tsx src/services/routing-gateway-reference.test.ts && tsx src/services/bgp-export-model.test.ts && tsx src/services/traffic-history-model.test.ts && tsx src/services/internet-path-snapshot.test.ts && tsx src/services/internet-path-observation.test.ts && tsx src/modules/mcp/coverage.test.ts && tsx src/modules/mcp/policy.test.ts && tsx src/modules/mcp/events-batch-model.test.ts && tsx src/modules/mcp/operation-revisions.test.ts && tsx src/modules/mcp/native-create-identity.test.ts && tsx src/modules/mcp/ros-step-verification.test.ts && tsx src/modules/mcp/ros-prefix.test.ts && tsx src/modules/mcp/native-conservation-http.test.ts && tsx src/modules/mcp/security.test.ts && tsx src/modules/mcp/state-file-publish.test.ts && tsx src/modules/mcp/routing-model.test.ts && tsx src/modules/mcp/routing-observations-model.test.ts && tsx src/modules/mcp/ospf-health.test.ts && tsx src/modules/mcp/ecmp-preflight.test.ts && tsx src/modules/mcp/recursive-recovery.test.ts && tsx src/modules/mcp/control-path-model.test.ts && tsx src/modules/mcp/routing-community-scoped-model.test.ts && tsx src/modules/mcp/routing-community-scoped-native.test.ts && tsx src/modules/mcp/path-verification.test.ts && tsx src/modules/mcp/database-maintenance.test.ts && tsx src/routes/mcp.test.ts && tsx ../lib/auth-mcp.test.ts && tsx --test src/modules/mcp/routing-controller-fence.test.ts && tsx src/test/chr-vrf-companion-proof.test.ts && tsx src/test/chr-vrf-companion-telemetry.test.ts && tsx src/test/chr-vrf-companion-address-space.test.ts && tsx src/modules/mcp/flow-facts-rebuild-actions.test.ts && tsx src/modules/mcp/ipsec-peer-create-dependencies.test.ts && tsx src/services/ipsec-certificate-update-model.test.ts && tsx src/modules/mcp/ipsec-native-dispatch.test.ts && tsx src/modules/mcp/ipsec-user-update-actions.test.ts && tsx src/modules/mcp/ipsec-server-delete-actions.test.ts && tsx src/services/ipsec-proof-telemetry.test.ts && tsx src/services/vrf-production-admission.test.ts && tsx src/modules/mcp/flow-host-keys-actions.test.ts && tsx src/services/vrf-production-read.test.ts && tsx src/modules/mcp/ipsec-user-create-actions.test.ts && tsx src/modules/mcp/flow-settings-actions.test.ts && tsx src/services/vrf-production-ticket-store.test.ts && npm run test:mcp:routing-baseline",
"test: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:mcp": "node scripts/vrf-production-profile.test.mjs && node src/services/vrf-production-peer-telemetry.test.mjs && node src/services/vrf-execution-diagnostic.test.mjs && node src/services/vrf-transition-refusal.test.mjs && node src/services/vrf-production-gre-cycle.test.mjs && tsx src/services/vrf-production-gre-link-local.test.ts && tsx src/services/vrf-production-bridge-delete.test.ts && tsx src/services/vrf-production-transition.test.ts && tsx --test src/services/routeros-snapshot-read.test.ts src/services/bridge-vlan-configuration.test.ts src/modules/mcp/bridge-vlan-projection.test.ts src/modules/mcp/vrf-preparation-diagnostic.test.ts && tsx --test src/services/server-poll-error-provenance.test.ts && tsx --test src/modules/mcp/plan-expiry.test.ts src/modules/mcp/result-budget.test.ts src/modules/mcp/result-delivery.test.ts src/modules/mcp/wire-admission.test.ts src/modules/mcp/flow-envelope-allowance.test.ts src/modules/mcp/sdk-terminal-budget.test.ts src/modules/mcp/inflight-owner.test.ts && npm run test:database-source-binding && tsx --test src/modules/mcp/integration-fixture-expiry.test.ts && npm run test:mcp:operation-origin && npm run test:mcp:service-context && npm run test:mcp:operation-locks && tsx --test src/modules/mcp/routing-community-public.test.ts src/modules/mcp/management-transport.test.ts src/modules/mcp/routing-community-context-model.test.ts src/modules/mcp/routing-community-context-native.test.ts ../packages/contracts/src/mcp-routing.test.ts && tsx src/modules/mcp/human-artifact-receipts.test.ts && tsx src/modules/mcp/chr-overlay-readiness.test.ts && tsx src/modules/mcp/chr-recovery-source.test.ts && tsx src/modules/mcp/chr-native-failure.test.ts && tsx src/modules/mcp/ospf-owner-graph.test.ts && tsx src/modules/mcp/routing-measurement-address.test.ts && tsx src/modules/mcp/routing-observation-source.test.ts && tsx src/services/traffic-flow-talkers-model.test.ts && tsx src/services/bgp-prefix-model.test.ts && tsx src/services/vrf-members.test.ts && tsx src/services/routing-gateway-reference.test.ts && tsx src/services/bgp-export-model.test.ts && tsx src/services/traffic-history-model.test.ts && tsx src/services/internet-path-snapshot.test.ts && tsx src/services/internet-path-observation.test.ts && tsx src/modules/mcp/coverage.test.ts && tsx src/modules/mcp/policy.test.ts && tsx src/modules/mcp/events-batch-model.test.ts && tsx src/modules/mcp/operation-revisions.test.ts && tsx src/modules/mcp/native-create-identity.test.ts && tsx src/modules/mcp/ros-step-verification.test.ts && tsx src/modules/mcp/ros-prefix.test.ts && tsx src/modules/mcp/native-conservation-http.test.ts && tsx src/modules/mcp/security.test.ts && tsx src/modules/mcp/state-file-publish.test.ts && tsx src/modules/mcp/routing-model.test.ts && tsx src/modules/mcp/routing-observations-model.test.ts && tsx src/modules/mcp/ospf-health.test.ts && tsx src/modules/mcp/ecmp-preflight.test.ts && tsx src/modules/mcp/recursive-recovery.test.ts && tsx src/modules/mcp/control-path-model.test.ts && tsx src/modules/mcp/routing-community-scoped-model.test.ts && tsx src/modules/mcp/routing-community-scoped-native.test.ts && tsx src/modules/mcp/path-verification.test.ts && tsx src/modules/mcp/database-maintenance.test.ts && tsx src/routes/mcp.test.ts && tsx ../lib/auth-mcp.test.ts && tsx --test src/modules/mcp/routing-controller-fence.test.ts && tsx src/test/chr-vrf-companion-proof.test.ts && tsx src/test/chr-vrf-companion-telemetry.test.ts && tsx src/test/chr-vrf-companion-address-space.test.ts && tsx src/modules/mcp/flow-facts-rebuild-actions.test.ts && tsx src/modules/mcp/ipsec-peer-create-dependencies.test.ts && tsx src/services/ipsec-certificate-update-model.test.ts && tsx src/modules/mcp/ipsec-native-dispatch.test.ts && tsx src/modules/mcp/ipsec-user-update-actions.test.ts && tsx src/modules/mcp/ipsec-server-delete-actions.test.ts && tsx src/services/ipsec-proof-telemetry.test.ts && tsx src/services/vrf-production-admission.test.ts && tsx src/modules/mcp/flow-host-keys-actions.test.ts && tsx src/services/vrf-production-read.test.ts && tsx --test src/services/vrf-production-wireguard-alias.test.ts && tsx src/modules/mcp/ipsec-user-create-actions.test.ts && tsx src/modules/mcp/flow-settings-actions.test.ts && tsx src/services/vrf-production-ticket-store.test.ts && npm run test:mcp:routing-baseline",
"test:wireguard": "tsx src/services/mikrotik-response-bounds.test.ts && tsx --test src/services/wireguard-dependency-read.test.ts && tsx src/services/wireguard-interface-update.test.ts && tsx src/services/wireguard-peer-update.test.ts && npx tsx src/services/wireguard-config.test.ts && npx tsx src/services/wireguard-ipv6.test.ts && npx tsx --test src/services/wireguard-import.test.ts src/services/wireguard-import-apply.test.ts src/services/wireguard-import-revision.test.ts src/services/wireguard-delete.test.ts && npx tsx src/routes/wireguard-import.test.ts && tsx src/services/wireguard-interface-create.test.ts && tsx src/services/wireguard-peer-create.test.ts && tsx src/services/wireguard-import-plan.test.ts && tsx src/services/wireguard-import-execution.test.ts && tsx src/modules/mcp/wireguard-import-actions.test.ts && tsx --test src/services/wireguard-export.test.ts",
"test:traffic-rate": "tsx src/services/traffic-rate.test.ts && tsx src/services/traffic-observation.test.ts && tsx src/services/traffic-freshness.test.ts && tsx src/services/traffic-live.test.ts && tsx src/services/traffic-observation-sql.test.ts && tsx src/services/traffic-history-sql.test.ts && tsx src/services/traffic-live-sql.test.ts && tsx src/services/traffic-volume-sql.test.ts && tsx src/services/traffic-binding-status.test.ts && tsx src/services/traffic-status-sql.test.ts && tsx src/services/traffic-gap.test.ts && tsx src/services/traffic-identity.test.ts && tsx src/services/traffic-membership.test.ts && tsx src/services/traffic-aggregate-peak.test.ts && tsx src/services/traffic-users-cache.test.ts && tsx src/services/traffic-collection-lock.test.ts && tsx src/services/traffic-concurrency-sql.test.ts",
"test:traffic-flow": "tsx --test src/services/traffic-flow-heartbeat.test.ts src/services/traffic-flow-heartbeat-wiring.test.ts src/services/traffic-flow-heartbeat-receiver.test.ts src/services/traffic-flow-ingest-quality.test.ts src/services/traffic-flow-quality-contract.test.ts && tsx src/services/traffic-flow-catalog-read.test.ts && tsx src/routes/traffic-flow-query.test.ts && tsx src/services/traffic-flow-month.test.ts && tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-ipv6-pipeline.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/geoip-download.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-fact-accumulator.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts && tsx src/services/geoip-manual-update.test.ts && tsx src/services/traffic-flow-overlay-model.test.ts && npm run test:traffic-flow:ownership && tsx src/services/traffic-flow-worker-platform.test.ts && tsx src/services/traffic-flow-template-cache.test.ts && tsx --test src/services/traffic-flow-template-lifetime.test.ts && tsx src/services/traffic-flow-protocol-scope.test.ts && npm run test:traffic-flow:read-budget && npm run test:traffic-flow:resource-profile",
"test:users": "tsx ../packages/contracts/src/users.test.ts && tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts",
"test:users": "tsx ../packages/contracts/src/users.test.ts && tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/binding-target.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts",
"test:pg": "tsx src/db/pg-pool.test.ts && tsx src/db/sql-bind.test.ts && tsx src/db/sqlite-json.test.ts && tsx src/db/traffic-flags.test.ts && tsx src/db/pg-schema.test.ts && tsx src/db/scheduler-retention-index.test.ts && tsx src/services/config-revisions.test.ts",
"test:recursive-routes": "tsx src/services/recursive-route-identity.test.ts && tsx src/routes/recursive-routes.test.ts && tsx src/services/recursive-route-catalog.test.ts && tsx src/services/recursive-effective-properties.test.ts && tsx src/services/recursive-apply.test.ts",
"test:config-sync": "tsx src/services/owned-routing-policy.test.ts && tsx src/services/routing-filter-body.test.ts && tsx src/services/bgp-community-policy-model.test.ts && tsx src/services/bgp-connection-read.test.ts && tsx src/services/bgp-filter-binding-model.test.ts && tsx src/services/filter-rules-write-model.test.ts && tsx src/services/filter-rules-mutation.test.ts && tsx src/utils/server-id.test.ts && tsx src/services/config-apply-plan.test.ts && tsx src/services/entity-snapshots.test.ts && tsx src/services/gre-native-identity.test.ts && tsx src/services/gre-ipv6.test.ts && tsx src/routes/gre-ipv6-validation.test.ts && tsx src/services/network-disabled-server.test.ts",
@@ -54,7 +58,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: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: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: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",
@@ -65,7 +69,7 @@
"test:mcp:ipsec-export-journal:pg": "node scripts/test-mcp-postgres.mjs --ipsec-export-journal --restore",
"test:mcp:ipsec-export:http:pg": "node scripts/test-mcp-postgres.mjs --ipsec-export-http --restore",
"test:mcp:diagnostic-cancel-host:pg": "node scripts/test-mcp-postgres.mjs --diagnostic-cancel-host --restore",
"test:server-probe-management": "tsx src/services/server-probe-authority.test.ts && tsx src/services/server-probe-public-result.test.ts && tsx src/services/server-probe-execution.test.ts && tsx src/services/server-probe-rest-contract.test.ts && tsx src/services/server-probe-rest-execution.test.ts && tsx src/services/server-probe-journal.test.ts && tsx src/services/server-probe-journal-history.test.ts && tsx src/modules/mcp/server-probe-actions.test.ts && tsx src/modules/mcp/diagnostic-cancellation.test.ts && tsx src/modules/mcp/service-diagnostic-progress.test.ts && tsx src/services/mikrotik-get-admission.test.ts && tsx src/services/server-probe-storage-journal-adapter.test.ts && tsx --test src/services/server-probe-storage-history.test.ts && npm run test:server-probe-storage-models",
"test:server-probe-management": "tsx src/services/server-probe-authority.test.ts && tsx src/services/server-probe-public-result.test.ts && tsx src/services/server-probe-execution.test.ts && tsx src/services/server-probe-rest-contract.test.ts && tsx src/services/server-probe-rest-execution.test.ts && tsx src/services/server-probe-journal.test.ts && tsx src/services/server-probe-journal-history.test.ts && tsx src/modules/mcp/server-probe-actions.test.ts && tsx src/modules/mcp/diagnostic-cancellation.test.ts && tsx src/modules/mcp/service-diagnostic-progress.test.ts && tsx src/services/mikrotik-get-admission.test.ts && tsx src/services/server-probe-storage-journal-adapter.test.ts && npm run test:server-probe-history && npm run test:server-probe-storage-models",
"test:server-poll-http:pg": "node scripts/test-server-poll-http-owned.mjs",
"test:server-poll-approval:pg": "node scripts/test-server-poll-approval-owned.mjs",
"test:server-poll-atomic:pg": "node scripts/test-server-poll-atomic-owned.mjs",
@@ -111,6 +115,7 @@
"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: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-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",
@@ -140,7 +145,7 @@
"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: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: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",
@@ -153,8 +158,9 @@
"test:server-poll-durable:pg": "node scripts/test-mcp-postgres.mjs --server-poll-durable --restore",
"test:server-probe-legacy:pg": "node scripts/test-mcp-postgres.mjs --probe-legacy-orphans",
"test:server-probe-storage:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-journal",
"test:server-probe-history": "tsx --test src/services/server-probe-storage-history.test.ts",
"test:server-probe-history": "tsx --test src/services/server-probe-storage-history.test.ts src/services/server-probe-retired-source.test.ts",
"test:server-probe-history:pg": "node scripts/test-mcp-postgres.mjs --probe-storage-history",
"test:server-probe-retirement:pg": "node scripts/test-mcp-postgres.mjs --probe-operation-retirement",
"test:server-probe-storage-models": "tsx src/services/server-probe-journal-maintenance.test.ts && tsx src/modules/mcp/diagnostic-cancellation-source.test.ts && tsx src/services/server-probe-storage-source.test.ts && tsx src/services/server-probe-storage-connection.test.ts && tsx src/modules/mcp/server-probe-storage-host.test.ts && tsx src/services/server-probe-storage-rest.test.ts && tsx src/services/server-probe-storage-metadata.test.ts && tsx src/services/server-probe-legacy-verifier.test.ts && tsx src/services/server-probe-legacy-snapshot.test.ts && tsx src/services/server-probe-legacy-inspection.test.ts && tsx src/db/database-source-observation.test.ts && tsx src/services/server-probe-retention.test.ts && tsx src/modules/mcp/server-probe-registration.test.ts",
"test:mcp:migration-data:pg": "node scripts/test-mcp-postgres.mjs --migration-data-inventory --restore",
"test:mcp:poll-memory:pg": "npm run build -w @mmapp/contracts && node scripts/acceptance/build-poll-memory.mjs && node scripts/test-mcp-postgres.mjs --poll-memory",
@@ -165,7 +171,7 @@
"test:traffic-flow:read-budget": "tsx --test src/services/traffic-flow-read-budget.test.ts src/modules/mcp/flow-read-quality.test.ts && node scripts/test-flow-read-adapters.mjs",
"test:traffic-flow:read-http:pg": "node scripts/test-flow-read-http-owned.mjs",
"test:traffic-flow:read-source:pg": "tsx scripts/test-flow-read-sql-owned.mts",
"test:mcp:routing-baseline": "tsx --test src/modules/mcp/routing-controller-active-writer.test.ts src/modules/mcp/routing-controller-fence.test.ts src/modules/mcp/routing-community-public.test.ts src/modules/mcp/routing-controller-overlap.test.ts",
"test:mcp:routing-baseline": "tsx --test src/modules/mcp/routing-controller-active-writer.test.ts src/modules/mcp/routing-controller-baseline.test.ts src/modules/mcp/routing-controller-fence.test.ts src/modules/mcp/routing-community-public.test.ts src/modules/mcp/routing-controller-overlap.test.ts",
"test:mcp:routing-baseline:pg": "node scripts/test-routing-controller-baseline.mjs",
"test:mcp:routing-baseline-inverse:pg": "node scripts/test-routing-controller-baseline.mjs --default-scoped",
"test:sqlite-import-control:pg": "node scripts/test-sqlite-import-control-postgres.mjs",
@@ -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 ts from 'typescript'
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/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',
])
import {OPERATOR_SOURCES} from './database-operator-sources.mjs'
export {OPERATOR_SOURCES}
const emitted=source=>source.replace(/\.mts$/,'.mjs').replace(/\.ts$/,'.js')
const digest=value=>createHash('sha256').update(value).digest('hex')
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 {fileURLToPath,pathToFileURL} from 'node:url'
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)),'..')
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'))
await mkdir(projectBackend,{recursive:true});await writeFile(path.join(projectBackend,'package.json'),JSON.stringify({type:'module',dependencies:packageJson.dependencies}))
const first=await buildDatabaseOperator({backendDirectory:backend,outputDirectory:output}),second=await buildDatabaseOperator({backendDirectory:backend,outputDirectory:output})
assert.deepEqual(first,second);assert.equal(first.modules.length,OPERATOR_SOURCES.length);assert.equal(first.modules.length,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,20);assert.deepEqual(first.runtimeDependencies,['dotenv','pg','zod']);assert.equal(first.backendDistModules.length,12)
const copied=new Set()
async function copyDist(relative){
if(copied.has(relative))return;copied.add(relative)
@@ -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)
assert.deepEqual([...copied].sort(),first.backendDistModules.map(item=>item.file).sort())
// 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 buildScript=path.join(projectBackend,'scripts','build-database-operator.mjs');await copyFile(fileURLToPath(new URL('./build-database-operator.mjs',import.meta.url)),buildScript)
const scripts=path.join(projectBackend,'scripts')
assert.equal(await stageDatabaseOperatorScripts(scripts,path.join(backend,'scripts')),OPERATOR_SOURCES.length+2)
const buildScript=path.join(scripts,'build-database-operator.mjs')
const built=spawnSync(process.execPath,[buildScript],{cwd:temporary,env:{...process.env,NODE_OPTIONS:''},encoding:'utf8',timeout:30000})
assert.equal(built.status,0,built.stderr);assert.equal(JSON.parse(built.stdout.trim()).modules,17)
assert.equal(built.status,0,built.stderr);assert.equal(JSON.parse(built.stdout.trim()).modules,20)
assert.deepEqual(JSON.parse(await readFile(path.join(output,'build-manifest.json'),'utf8')),first)
const configFile=path.join(projectBackend,'dist','config.js'),originalConfig=await readFile(configFile,'utf8')
for(const injected of ["import 'tsx';","import '../src/config.js';","import '../outside.js';"]){
@@ -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 importer=path.join(temporary,'import-all.mjs');await writeFile(importer,`import {pathToFileURL} from 'node:url';for(const file of ${JSON.stringify(modulesToImport)})await import(pathToFileURL(file).href);console.log(JSON.stringify({imported:${modulesToImport.length},readyAuthorized:false}));`)
const imported=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,importer],{cwd:temporary,env,encoding:'utf8',timeout:30000})
assert.equal(imported.status,0,imported.stderr);assert.equal(JSON.parse(imported.stdout.trim()).imported,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,20);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0)
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']]){
const child=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,path.join(output,entry),'invalid'],{cwd:temporary,env,encoding:'utf8',timeout:30000})
for(const [entry,event,args] of [['database-writer-bootstrap-operator.mjs','application-writer-bootstrap-refused',['invalid']],['database-writer-profile-operator.mjs','application-writer-profile-publication-refused',['invalid']],['database-writer-profile-operator.mjs','database-maintenance-command-refused',['maintenance-inspect']],['database-writer-profile-operator.mjs','database-maintenance-command-refused',['maintenance-resolve']],['database-writer-profile-operator.mjs','database-source-activation-refused',['source-activate']]]){
const child=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,path.join(output,entry),...args],{cwd:temporary,env,encoding:'utf8',timeout:30000})
assert.equal(child.status,1,child.stderr);assert.equal(child.stdout,'');assert.equal(JSON.parse(child.stderr.trim()).event,event);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0);refusals.push(event)
}
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/source-activation.mts',
'writer-profile/catalogue.mts','writer-profile/catalogue-value-bridge.ts','writer-profile/catalogue-projection.ts','writer-profile/catalogue-leaves.ts',
'writer-profile/application-writer-profile-catalogue.ts','writer-profile/application-writer-profile-catalogue-values.ts',
'writer-profile/application-writer-profile-catalogue-parent-policy.ts','writer-profile/application-writer-profile-canonical.ts',
'writer-profile/fixed44-build-sql.ts','writer-profile/fixed44-build-definition.ts','writer-profile/expanded44-data-policy.ts',
])
@@ -8,9 +8,14 @@ import {approval,reconciliationApproval,poolOptions,roleKeys,assertJsDriverEnvir
import {deploymentRequest} from './database-writer-bootstrap-operator.mjs'
const hex=(v:unknown)=>typeof v==='string'&&/^[a-f0-9]{64}$/.test(v)
const parse=(b:Buffer)=>JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(b))
const sourceCommands=['source-activate','source-revoke','source-status']
export async function main(args:string[]){
if(sourceCommands.includes(args[0]))return (await import('./writer-profile/source-activation.mjs')).sourceActivationMain(args)
if(['maintenance-inspect','maintenance-resolve'].includes(args[0]))return (await import('./writer-profile/maintenance-resolution.mjs')).maintenanceMain(args)
const activePreflight=args[0]==='source-preflight',preflight=activePreflight||args[0]==='preflight'
const flags=['bootstrap','bootstrap-sha256','approval','approval-sha256','definition','definition-sha256','transport','transport-sha256','ca','source-registry','source-spki-sha256','journal']
assert(['publish','reconcile','preflight'].includes(args[0])&&args.length===1+2*flags.length)
if(activePreflight)flags.push('activation-directory','activation-sha256')
assert((preflight||['publish','reconcile'].includes(args[0]))&&args.length===1+2*flags.length)
assertJsDriverEnvironment(process.env)
const values=new Map<string,string>()
for(let i=1;i<args.length;i+=2){assert(args[i].startsWith('--')&&flags.includes(args[i].slice(2))&&!values.has(args[i].slice(2))&&args[i+1]);values.set(args[i].slice(2),args[i+1])}
@@ -31,29 +36,49 @@ export async function main(args:string[]){
// All explicit trust files are checked before app helper/PG imports. No .env URL is used.
// Existing source helper loads config/dotenv. Complete it BEFORE pg's first
// import so a newly loaded NODE_PG_FORCE_NATIVE cannot select another driver.
const {createDatabaseSourceBindingVerifier}=await import('../src/db/database-source-binding.js')
const {createProtectedDatabaseSourceBindingVerifier}=await import('./writer-profile/protected-source-registry.mjs')
assertJsDriverEnvironment(process.env)
const [{default:pg},{createConnectedPublisher},{collectSource44Catalogue,approvedDefinition}]=await Promise.all([
const [{default:pg},{createConnectedPublisher,requireCurrentCatalogueDefinition},{collectSource44Catalogue,approvedDefinition}]=await Promise.all([
import('pg'),import('./writer-profile/publisher.mjs'),import('./writer-profile/catalogue.mjs')])
assertJsDriverEnvironment(process.env)
assert.equal(approvedDefinition(definition),pinned.definitionSha256)
const installation=Object.freeze({...subject,roles:Object.fromEntries(roleKeys.map(k=>[k,request.roles[k].signedOid])),roleBindings:request.roles,expectedSource:request.source})
const verifier=createDatabaseSourceBindingVerifier({registryDirectory:get('source-registry'),installationId:subject.installationId,publicKeySpkiSha256:get('source-spki-sha256'),currentHeadSha256:request.sourceHeadSha256})
const publisher=createConnectedPublisher(installation,(s:Parameters<typeof collectSource44Catalogue>[0])=>collectSource44Catalogue(s,installation),verifier,pinned)
const pool=new pg.Pool(options)
// A pool-level async error never becomes raw stdout or a retry signal.
let asyncFailure=false;pool.on('error',()=>{asyncFailure=true})
let result,primary:unknown,failed=false
try{result=args[0]==='preflight'?await publisher.preflightCurrentProfile(pool,{deadlineAt:Date.now()+120000}):args[0]==='reconcile'?await publisher.reconcilePendingPublication(pool,{journalDirectory:get('journal'),deadlineAt:Date.now()+120000}):await publisher.publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256:pinned.expectedHeadSha256,journalDirectory:get('journal'),validityMs:pinned.validityMs,deadlineAt:Date.now()+120000})}
catch(error){primary=error;failed=true}
try{await pool.end()}catch(error){primary??=error;failed=true}
if(args[0]==='preflight'&&result?.state==='matched'&&!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(result?.outcome==='committed')return {event:'application-writer-profile-publication',outcome:'committed',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,
cleanupUnconfirmed:result.cleanupUnconfirmed||asyncFailure||failed,journalUnconfirmed:!!result.journalUnconfirmed,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
if(args[0]==='reconcile'&&result&&!failed&&!asyncFailure&&['idle','confirmed','not-committed'].includes(result.state))return {event:'application-writer-profile-reconciliation',outcome:result.state,headSha256:result.headSha256??null,cleanupUnconfirmed:false,journalUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
throw primary??Error('Publisher terminal unconfirmed')
const reviewedDefinition=approvedDefinition(definition)
assert.equal(reviewedDefinition.definitionSha256,pinned.definitionSha256)
if(args[0]!=='reconcile')requireCurrentCatalogueDefinition(pinned,reviewedDefinition)
const installation=Object.freeze({...subject,roles:Object.fromEntries(roleKeys.map(k=>[k,request.roles[k].signedOid])),roleBindings:request.roles,expectedSource:request.source,tls:Object.freeze({...request.tls})})
const sourcePin={registryDirectory:get('source-registry'),installationId:subject.installationId,publicKeySpkiSha256:get('source-spki-sha256'),currentHeadSha256:request.sourceHeadSha256}
// Use the same protected registry evidence for inspection and publication.
const verifier=createProtectedDatabaseSourceBindingVerifier(sourcePin)
async function connected(sourceVerifier:typeof verifier,deadlineAt:number){
const publisher=createConnectedPublisher(installation,(s:Parameters<typeof collectSource44Catalogue>[0])=>collectSource44Catalogue(s,installation),sourceVerifier,pinned,reviewedDefinition)
const pool=new pg.Pool(options)
// A pool-level async error never becomes raw stdout or a retry signal.
let asyncFailure=false;pool.on('error',()=>{asyncFailure=true})
let result,primary:unknown,failed=false
try{result=preflight?await publisher.preflightCurrentProfile(pool,{deadlineAt}):args[0]==='reconcile'?await publisher.reconcilePendingPublication(pool,{journalDirectory:get('journal'),deadlineAt}):await publisher.publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256:pinned.expectedHeadSha256,journalDirectory:get('journal'),validityMs:pinned.validityMs,deadlineAt})}
catch(error){primary=error;failed=true}
try{await pool.end()}catch(error){primary??=error;failed=true}
if(preflight&&result?.state==='matched'&&result.platformProtectionVerified===true&&!failed&&!asyncFailure)return {event:'application-writer-profile-preflight',outcome:'matched',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,sourceMatched:true,rolesMatched:true,catalogueMatched:true,platformProtectionVerified:true,writerAuthorized:false,cleanupUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
if(result?.outcome==='committed')return {event:'application-writer-profile-publication',outcome:'committed',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,
cleanupUnconfirmed:result.cleanupUnconfirmed||asyncFailure||failed,journalUnconfirmed:!!result.journalUnconfirmed,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
if(args[0]==='reconcile'&&result&&!failed&&!asyncFailure&&['idle','confirmed','not-committed'].includes(result.state))return {event:'application-writer-profile-reconciliation',outcome:result.state,headSha256:result.headSha256??null,cleanupUnconfirmed:false,journalUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
throw primary??Error('Publisher terminal unconfirmed')
}
const deadlineAt=Date.now()+120000
if(!activePreflight)return connected(verifier,deadlineAt)
const {withActiveSourcePreflight}=await import('./writer-profile/source-activation.mjs')
const publisherRole=request.roles.publisher.name
const expectedSource={...request.source,databaseOid:Number(request.source.databaseOid),sessionUser:publisherRole,currentUser:publisherRole}
const result=await withActiveSourcePreflight({directory:get('activation-directory'),expectedSha256:get('activation-sha256'),sourcePin,expectedSource,tlsSpkiSha256:request.tls.spkiSha256,deadlineAt},verifier,
guardedVerifier=>connected(guardedVerifier,deadlineAt))
assert.equal(result.event,'application-writer-profile-preflight')
return {...result,event:'application-writer-profile-source-preflight',sourceActivationMatched:true}
}
if(process.argv[1]&&import.meta.url===pathToFileURL(path.resolve(process.argv[1])).href){
try{const result=await main(process.argv.slice(2));console.log(JSON.stringify(result));if(result.cleanupUnconfirmed||result.journalUnconfirmed)process.exitCode=2}
catch{console.error(JSON.stringify({event:'application-writer-profile-publication-refused',automaticReplay:false,inspectPendingBeforeRetry:true,readyAuthorized:false}));process.exitCode=1}
catch(error){
const sourceCommand=sourceCommands.includes(process.argv[2])
const unknown=sourceCommand&&(error as NodeJS.ErrnoException|null)?.code==='SOURCE_ACTIVATION_TERMINAL_UNKNOWN'
console.error(JSON.stringify({event:sourceCommand?(unknown?'database-source-activation-unknown':'database-source-activation-refused'):
['maintenance-inspect','maintenance-resolve'].includes(process.argv[2])?'database-maintenance-command-refused':process.argv[2]==='source-preflight'?'application-writer-profile-source-preflight-refused':'application-writer-profile-publication-refused',automaticReplay:false,inspectPendingBeforeRetry:true,readyAuthorized:false}));process.exitCode=unknown?2:1
}
}
@@ -6,22 +6,38 @@ import fs from 'node:fs/promises'
import {createRequire} from 'node:module'
import {runInNewContext} from 'node:vm'
import {EventEmitter} from 'node:events'
import {generateKeyPairSync} from 'node:crypto'
import path from 'node:path'
import * as format from '../src/db/application-writer-profile-format.ts'
import {MIGRATION_MANIFEST,migrationManifestDigest} from '../src/db/migration-manifest.ts'
import {readCurrentApplicationWriterProfile} from '../src/db/application-writer-profile-registry-read.ts'
import {syntheticFixture,stagedModelClient} from '../src/db/application-writer-profile-registry-read-model.test.ts'
import {approvedDefinition} from './writer-profile/catalogue.mts'
import {BUILD44_PROVENANCE,FIXED44_ROLE_TEMPLATE} from './writer-profile/fixed44-build-definition.ts'
const require=createRequire(new URL('../package.json',import.meta.url)),ts=require('typescript'),pg=require('pg')
const candidate=await fs.readFile(new URL('./writer-profile/publisher.mjs',import.meta.url),'utf8')
interface PreflightDiagnostics {state:string;headSha256:string;sourceMatched:boolean;rolesMatched:boolean;catalogueMatched:boolean;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 OperatorModule {main(args:string[]):Promise<{event:string;outcome:string;writerAuthorized: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 {
requireCurrentCatalogueDefinition(approval:unknown,definition:unknown):string
createConnectedPublisher(installation:unknown,collect:()=>Promise<unknown>,sourceVerifier:{verify:()=>Promise<{platformProtectionVerified:boolean}>},approval:unknown,definition:unknown):{
preflightCurrentProfile(pool:unknown,options:{deadlineAt:number}):Promise<PreflightDiagnostics>
publishCurrentProfile(pool:unknown,options:{privateKeyFile:string;expectedHeadSha256:null;journalDirectory:string;deadlineAt:number}):Promise<{outcome:string}>
}
}
interface OperatorModule {main(args:string[]):Promise<{event:string;outcome:string;platformProtectionVerified:boolean;writerAuthorized:boolean;readyAuthorized:boolean;sourceActivationMatched?:boolean}>}
function compile<T>(source:string,modules:Record<string,unknown>):T{
const exports:Record<string,unknown>={};const compiled=ts.transpileModule(source.split('\nif(process.argv[1]')[0].replaceAll('import.meta.url',JSON.stringify('file:///candidate-not-cli.mts')),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText
runInNewContext(compiled,{exports,require:(name:string)=>{assert(name in modules,'Unapproved import '+name);return modules[name]},Buffer,URL,TextDecoder,Date,setTimeout,clearTimeout,Object,Error,AggregateError,Promise,Map,Set,process:{env:{},argv:[]}}, {filename:'candidate.mjs'})
// The isolated VM loads the exact source above; tests exercise this narrow export contract.
return exports as T
}
function fixture(fault=''){
const f=syntheticFixture('1'),catalogue={catalogSha256:'f'.repeat(64),definitionSha256:f.policy.definitionSha256,leafCount:117},security={sha256:'e'.repeat(64),roles:{...f.policy.roles}}
const frozenDefinition=approvedDefinition({format:'application-writer-source44-definition-approval-v1',provenance:BUILD44_PROVENANCE,template:FIXED44_ROLE_TEMPLATE})
const frozenMigrations=MIGRATION_MANIFEST.slice(0,44),markerIds=frozenMigrations.map(pin=>pin.file.slice(0,-4))
const markerSql='SELECT id FROM public.schema_migrations ORDER BY id COLLATE "C" LIMIT $1'
function fixture(fault='',forPublication=false){
const f=syntheticFixture('1'),catalogue={...frozenDefinition,catalogSha256:'f'.repeat(64),leafCount:117},security={sha256:'e'.repeat(64),roles:{...f.policy.roles}}
const currentBuild=fault==='current-build'||fault==='current-approved-frozen-definition'
f.policy.manifestSha256=frozenDefinition.manifestSha256;f.policy.definitionSha256=frozenDefinition.definitionSha256
f.policy.catalogSha256=format.sha(format.canonical({base:catalogue.catalogSha256,security:security.sha256}))
if(fault==='signed-source')f.policy.source.databaseOid='999'
if(fault==='signed-role')f.policy.roles.operator='99'
@@ -34,63 +50,174 @@ function fixture(fault=''){
f.state.current[0].head_sha256=top.head_sha256;f.state.policy=[{policy_sha256:hash,policy_bytes:raw}]
if(fault==='tamper')f.state.policy[0].policy_bytes=Buffer.from(raw.toString().replace('synthetic','tampered'))
if(fault==='empty'){f.state.current=[];f.state.latest=[];f.state.policy=[]}
const model=stagedModelClient(f.state),queries:string[]=[],events:string[]=[];let clocks=0
const model=stagedModelClient(f.state),queries:string[]=[],events:string[]=[],markerLimits:unknown[][]=[]
let clocks=0,checkouts=0
const client=Object.assign(new EventEmitter(),{query:async(sql:string,values?:unknown[])=>{
queries.push(sql)
if(sql.includes('clock_timestamp()')&&++clocks===2&&fault==='late-clock')return {rows:[{now:'1'}]}
if(sql===markerSql){
markerLimits.push(values??[])
const ids=fault==='marker-missing'?markerIds.slice(0,-1):fault==='marker-extra'||fault==='marker-drift'&&markerLimits.length===2?[...markerIds,'9999_foreign']:markerIds
return {rows:ids.map(id=>({id}))}
}
if(sql.startsWith('WITH')||sql.includes('clock_timestamp()'))return model.client.query(sql,values)
if(sql.startsWith('SELECT pg_catalog.pg_advisory_unlock'))return {rows:[{released:true}]}
if(sql==='ROLLBACK')return {rows:[],command:fault==='rollback-ack'?'':'ROLLBACK'}
if(sql.startsWith('INSERT INTO mmapp_control.current'))return {rows:[{id:1}]}
return {rows:[],command:sql.split(' ')[0]}
},release:(bad:boolean)=>{events.push('release:'+bad)}})
const pool=new pg.Pool();pool.connect=async()=>client
const pool=new pg.Pool();pool.connect=async()=>{checkouts++;return client}
const source={systemIdentifier:'123',databaseOid:'456',databaseName:'synthetic',serverVersionNum:180004,inRecovery:false}
let contexts=0,verifications=0
const installation={...f.subject,expectedSource:source,roleBindings:{publisher:{name:'publisher',signedOid:'14'}}}
const approval={expiresAt:Date.now()+60000,manifestSha256:'b'.repeat(64),definitionSha256:catalogue.definitionSha256,catalogSha256:format.sha(format.canonical({base:catalogue.catalogSha256,security:security.sha256}))}
const fixtureKey=forPublication?generateKeyPairSync('ed25519'):null
const subject=fixtureKey?{...f.subject,publicKeyPem:fixtureKey.publicKey.export({format:'pem',type:'spki'}).toString(),publicKeySpkiSha256:format.sha(fixtureKey.publicKey.export({format:'der',type:'spki'}))}:f.subject
const installation={...subject,expectedSource:source,roleBindings:{publisher:{name:'publisher',signedOid:'14'}}}
const approval={expiresAt:Date.now()+60000,manifestSha256:['approval-manifest','current-approved-frozen-definition'].includes(fault)?migrationManifestDigest():frozenDefinition.manifestSha256,definitionSha256:catalogue.definitionSha256,catalogSha256:format.sha(format.canonical({base:catalogue.catalogSha256,security:security.sha256}))}
const definition=fault==='static-definition'?{...frozenDefinition,definitionSha256:'0'.repeat(64)}:frozenDefinition
const privateKeyFile=path.resolve('modeled-fixture-key.pem'),journalDirectory=path.resolve('modeled-journal'),files=new Map<string,Buffer>()
if(fixtureKey)files.set(privateKeyFile,Buffer.from(fixtureKey.privateKey.export({format:'pem',type:'pkcs8'})))
function file(filename:string){const value=files.get(filename);if(!value)throw Object.assign(Error('Fixture file absent'),{code:'ENOENT'});return value}
function stat(filename:string){return {size:file(filename).length,dev:1,ino:1,isFile:()=>true,isSymbolicLink:()=>false}}
const modules={
pg,'node:crypto':require('node:crypto'),'node:fs/promises':{},'node:path':require('node:path'),
'./operator-files.mjs':{withJournalLease:()=>{throw Error('Preflight must not use publication journal')},writeFile:()=>{throw Error('No publication writes')}},
pg,'node:crypto':require('node:crypto'),'node:fs/promises':{realpath:async(file:string)=>file,lstat:async(filename:string)=>stat(filename),unlink:async(filename:string)=>files.delete(filename),open:async(filename:string)=>({stat:async()=>stat(filename),readFile:async()=>Buffer.from(file(filename)),close:async()=>{}})},'node:path':path,
'./operator-contract.mjs':{retainPublisherTlsPeer:(actual:unknown)=>{
assert.equal(actual,client)
if(fault==='initial-tls')throw Error('TLS peer refused')
return ()=>{if(fault==='lost-tls'&&verifications===2)throw Error('Retained TLS lost')}
}},
'./operator-files.mjs':{withJournalLease:async(_directory:string,work:()=>Promise<unknown>)=>{assert(forPublication);events.push('journal-lease');return work()},writeFile:async(filename:string,value:Buffer)=>{assert(forPublication);files.set(filename,value)}},
'../../src/db/database-source-observation.js':{captureDatabaseSourceObservation:async()=>{events.push('capture-source');return {}},assertDatabaseSourceObservationCurrent:async()=>{events.push('fresh-source');if(fault==='late-source')throw Error('Source changed')}},
'../../src/db/migration-manifest.js':{MIGRATION_MANIFEST:[],migrationManifestDigest:()=> 'b'.repeat(64)},
'../../src/db/migration-manifest.js':{MIGRATION_MANIFEST:currentBuild?MIGRATION_MANIFEST:frozenMigrations,migrationManifestDigest:()=>currentBuild?migrationManifestDigest():frozenDefinition.manifestSha256},
'../../src/db/application-writer-profile-format.js':format,
'./context.mjs':{ownedContext:async()=>{contexts++;if(fault==='late-actor'&&contexts===2)throw Error('Actor changed');return {pid:1,source}},sameOwned:(a:unknown,b:unknown)=>assert.deepEqual(a,b),sourceCoordinates:(v:{source:typeof source})=>v.source,protectedStructure:async()=>{events.push('roles');if(fault==='role-structure')throw Error('Unsafe role');return security},readCurrent:readCurrentApplicationWriterProfile,HISTORY:'history',CURRENT:'current'},
'./context.mjs':{ownedContext:async()=>{contexts++;if(fault==='late-actor'&&contexts===2)throw Error('Actor changed');return {pid:1,source}},sameOwned:(a:unknown,b:unknown)=>assert.deepEqual(a,b),sourceCoordinates:(v:{source:typeof source})=>v.source,protectedStructure:async()=>{events.push('roles');if(fault==='role-structure')throw Error('Unsafe role');return security},readCurrent:forPublication?async()=>null:readCurrentApplicationWriterProfile,HISTORY:'history',CURRENT:'current'},
}
const api=compile<PublisherModule>(candidate,modules).createConnectedPublisher(installation,async()=>{events.push('catalogue');if(fault==='catalogue-eof')throw Error('Incomplete EOF');return catalogue},{verify:async()=>{verifications++;events.push('verify-source');if(fault==='source-verifier')throw Error('Unsigned source');if(fault==='late-expiry'&&verifications===2)approval.expiresAt=1}},approval)
return {run:()=>api.preflightCurrentProfile(pool,{deadlineAt:Date.now()+30000}),queries,events,f}
const api=compile<PublisherModule>(candidate,modules).createConnectedPublisher(installation,async()=>{events.push('catalogue');if(fault==='catalogue-eof')throw Error('Incomplete EOF');return fault==='catalogue-manifest'?{...catalogue,manifestSha256:migrationManifestDigest()}:fault==='catalogue-definition'?{...catalogue,definitionSha256:'0'.repeat(64)}:catalogue},{verify:async()=>{verifications++;events.push('verify-source');if(fault==='source-verifier')throw Error('Unsigned source');if(fault==='late-expiry'&&verifications===2)approval.expiresAt=1;return {platformProtectionVerified:!(fault==='initial-unprotected'&&verifications===1||fault==='terminal-unprotected'&&verifications===2)}}},approval,definition)
return {run:()=>api.preflightCurrentProfile(pool,{deadlineAt:Date.now()+30000}),publish:()=>api.publishCurrentProfile(pool,{privateKeyFile,journalDirectory,expectedHeadSha256:null,deadlineAt:Date.now()+30000}),queries,events,markerLimits,files,get checkouts(){return checkouts},f}
}
test('current44 preflight authenticates signed current and actual component inputs on one read-only lease',async()=>{
const f=fixture(),result=await f.run();assert.equal(result.state,'matched');assert.equal(result.headSha256,f.f.state.current[0].head_sha256)
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(f.queries.filter(q=>q.startsWith('BEGIN')).length,1);assert(f.queries.includes('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'));assert.equal(f.queries.filter(q=>q==='ROLLBACK').length,1)
assert.equal(format.canonical(f.markerLimits),'[[45]]')
assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT|CREATE|ALTER|DROP)\b/.test(q)));assert.equal(f.events.at(-1),'release:false')
assert.deepEqual(f.events.slice(0,6),['capture-source','verify-source','roles','catalogue','verify-source','fresh-source'])
})
test('publisher refuses before SQL on a wrong peer and stops SQL when the retained TLS socket is lost',async()=>{
const initial=fixture('initial-tls')
await assert.rejects(initial.run());assert.deepEqual(initial.queries,[]);assert.deepEqual(initial.events,['release:true'])
const lost=fixture('lost-tls')
await assert.rejects(lost.run());assert.equal(lost.events.at(-1),'release:true')
assert(!lost.queries.some(q=>q==='ROLLBACK'||q.includes('pg_advisory_unlock')))
})
for(const fault of ['initial-unprotected','terminal-unprotected'])test('preflight cannot promote '+fault+' into protected evidence',async()=>{
const f=fixture(fault),result=await f.run();assert.equal(result.platformProtectionVerified,false)
assert.equal(result.writerAuthorized,false);assert.equal(result.readyAuthorized,false)
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()=>{
const f=fixture(fault);await assert.rejects(f.run());assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT|CREATE|ALTER|DROP)\b/.test(q)));assert.equal(f.queries.filter(q=>q.startsWith('BEGIN')).length,1)
})
test('existing operator main dispatches preflight only after protected input validation, without private key or publication',async()=>{
for(const fault of ['current-build','current-approved-frozen-definition','approval-manifest','static-definition'])test('incompatible static manifest/definition refuses '+fault+' before either pool checkout or publication lease',async()=>{
const f=fixture(fault,true)
await assert.rejects(f.run(),/Approved catalogue definition is not bound/)
await assert.rejects(f.publish(),/Approved catalogue definition is not bound/)
assert.equal(f.checkouts,0);assert.deepEqual(f.events,[]);assert.deepEqual(f.queries,[])
})
for(const fault of ['catalogue-manifest','catalogue-definition','marker-missing','marker-extra'])test('retained preflight refuses '+fault+' and rolls back',async()=>{
const f=fixture(fault)
await assert.rejects(f.run(),error=>error instanceof Error&&/Catalogue evidence differs|Exact current migration marker/.test((error.cause as Error).message))
assert.equal(f.checkouts,1);assert.equal(f.queries.filter(q=>q==='ROLLBACK').length,1)
assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT)\b/.test(q)))
if(fault.startsWith('marker-'))assert.equal(format.canonical(f.markerLimits),'[[45]]')
else assert.deepEqual(f.markerLimits,[])
})
test('publication checks exact marker set on both retained transactions and refuses late marker drift before registry writes',async()=>{
const positive=fixture('',true)
assert.equal((await positive.publish()).outcome,'committed')
assert.equal(positive.checkouts,1);assert.equal(format.canonical(positive.markerLimits),'[[45],[45]]')
assert.equal(positive.queries.filter(q=>q==='COMMIT').length,2)
const drift=fixture('marker-drift',true)
await assert.rejects(drift.publish(),error=>error instanceof Error&&/Exact current migration marker/.test((error.cause as Error).message))
assert.equal(drift.checkouts,1);assert.equal(format.canonical(drift.markerLimits),'[[45],[45]]')
assert.equal(drift.queries.filter(q=>q==='COMMIT').length,1);assert.equal(drift.queries.filter(q=>q==='ROLLBACK').length,1)
assert(!drift.queries.some(q=>/^(?:INSERT|UPDATE|DELETE)\b/.test(q)))
assert([...drift.files.keys()].some(filename=>filename.endsWith('pending-publication.json')))
})
test('operator requires the protected source verifier for preflight, publication and reconciliation',async()=>{
const cli=await fs.readFile(new URL('./database-writer-profile-operator.mts',import.meta.url),'utf8'),f=syntheticFixture('1'),reads:string[]=[],events:string[]=[]
const request={installationId:f.subject.installationId,roles:Object.fromEntries(['runtime','operator','capture','publisher','control'].map((k,i)=>[k,{name:k,signedOid:String(i+11)}])),source:f.policy.source,sourceHeadSha256:'a'.repeat(64),tls:{caSha256:'a'.repeat(64)}}
const files:Record<string,unknown>={bootstrap:{format:'application-writer-bootstrap-v1',publicationAuthorized:false,readyAuthorized:false,deploymentRequest:request,verificationSubject:f.subject},approval:{expiresAt:Date.now()+60000,definitionSha256:'b'.repeat(64)},definition:{},transport:{},ca:'fixture'}
let failEnd=false,asyncError=false
const request={installationId:f.subject.installationId,roles:Object.fromEntries(['runtime','operator','capture','publisher','control'].map((k,i)=>[k,{name:k,signedOid:String(i+11)}])),source:f.policy.source,sourceHeadSha256:'a'.repeat(64),tls:{caSha256:'a'.repeat(64),spkiSha256:'d'.repeat(64)}}
const files:Record<string,unknown>={bootstrap:{format:'application-writer-bootstrap-v1',publicationAuthorized:false,readyAuthorized:false,deploymentRequest:request,verificationSubject:f.subject},approval:{expiresAt:Date.now()+60000,definitionSha256:'b'.repeat(64)},definition:{},transport:{},ca:'fixture','signing-private.pem':'fixture'}
let failEnd=false,asyncError=false,protectedRefused=false,activationRefused=false,incompatibleDefinition=false
const retainedPools:Pool[]=[]
class Pool extends EventEmitter{constructor(){super();retainedPools.push(this)}async end(){events.push('pool-end');if(failEnd)throw Error('Pool cleanup unconfirmed')}}
const result={state:'matched',epoch:'1',headSha256:'c'.repeat(64),notAfter:Date.now()+60000}
const result={state:'matched',epoch:'1',headSha256:'c'.repeat(64),notAfter:Date.now()+60000,platformProtectionVerified:true}
const protectedVerifier={verify:async()=>({platformProtectionVerified:true})}
const guardedVerifier={...protectedVerifier}
const modules={
'node:assert/strict':assert,'node:path':require('node:path'),'node:url':require('node:url'),'node:crypto':require('node:crypto'),
'./writer-profile/operator-files.mjs':{readProtected:async(file:string)=>{reads.push(file);assert(file in files);return Buffer.from(JSON.stringify(files[file]))},protectedDirectory:async()=>{},sha:format.sha},
'./writer-profile/operator-contract.mjs':{approval:(value:unknown)=>{events.push('approval');return value},reconciliationApproval:()=>{throw Error('No expired reconciliation bypass')},poolOptions:()=>{events.push('transport');return {}},roleKeys:['runtime','operator','capture','publisher','control'],assertJsDriverEnvironment:()=>{}},
'./database-writer-bootstrap-operator.mjs':{deploymentRequest:(value:unknown)=>value},'../src/db/database-source-binding.js':{createDatabaseSourceBindingVerifier:()=>({})},
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/catalogue.mjs':{collectSource44Catalogue:()=>{},approvedDefinition:()=> 'b'.repeat(64)},
'./writer-profile/operator-contract.mjs':{approval:(value:unknown)=>{events.push('approval');return value},reconciliationApproval:(value:unknown)=>{events.push('reconciliation-approval');return value},poolOptions:()=>{events.push('transport');return {}},roleKeys:['runtime','operator','capture','publisher','control'],assertJsDriverEnvironment:()=>{}},
'./database-writer-bootstrap-operator.mjs':{deploymentRequest:(value:unknown)=>value},
'./writer-profile/protected-source-registry.mjs':{createProtectedDatabaseSourceBindingVerifier:(pin:{registryDirectory:string;currentHeadSha256:string})=>{events.push('protected-source');assert.equal(pin.registryDirectory,'source-registry');assert.equal(pin.currentHeadSha256,request.sourceHeadSha256);if(protectedRefused)throw Error('Protected source refused');return protectedVerifier}},
'./writer-profile/source-activation.mjs':{withActiveSourcePreflight:async(input:{directory:string;expectedSha256:string;sourcePin:{currentHeadSha256:string};expectedSource:unknown;tlsSpkiSha256:string;deadlineAt:number},verifier:unknown,work:(verifier:unknown)=>Promise<unknown>)=>{
assert.equal(verifier,protectedVerifier);assert.equal(input.directory,'active-registry');assert.equal(input.expectedSha256,'e'.repeat(64))
assert.equal(input.sourcePin.currentHeadSha256,request.sourceHeadSha256);assert.equal(input.tlsSpkiSha256,request.tls.spkiSha256);assert(input.deadlineAt>Date.now())
assert.equal(format.canonical(input.expectedSource),format.canonical({...request.source,databaseOid:Number(request.source.databaseOid),sessionUser:'publisher',currentUser:'publisher'}))
events.push('activation-admission');if(activationRefused)throw Error('Activation refused')
const value=await work(guardedVerifier);events.push('activation-terminal');return value
}},
pg:{Pool},'./writer-profile/publisher.mjs':{requireCurrentCatalogueDefinition:()=>{if(incompatibleDefinition)throw Error('Build manifest refused')},createConnectedPublisher:(_installation:unknown,_collect:unknown,verifier:unknown)=>{
assert.equal(verifier,events.includes('activation-admission')?guardedVerifier:protectedVerifier)
return {
preflightCurrentProfile:async()=>{events.push('preflight');if(asyncError){const activePool=retainedPools.at(-1);assert(activePool);activePool.emit('error',Error('Async pool fault'))}return result},
publishCurrentProfile:async()=>{events.push('publish');return {...result,outcome:'committed'}},
reconcilePendingPublication:async()=>{events.push('reconcile');return {state:'confirmed',headSha256:result.headSha256}},
}
}},
'./writer-profile/catalogue.mjs':{collectSource44Catalogue:()=>{},approvedDefinition:()=>({definitionSha256:'b'.repeat(64),manifestSha256:frozenDefinition.manifestSha256})},
}
const flags=['bootstrap','bootstrap-sha256','approval','approval-sha256','definition','definition-sha256','transport','transport-sha256','ca','source-registry','source-spki-sha256','journal'],args=['preflight',...flags.flatMap(flag=>['--'+flag,flag.endsWith('sha256')?'a'.repeat(64):flag])]
const output=await compile<OperatorModule>(cli,modules).main(args)
assert.equal(output.event,'application-writer-profile-preflight');assert.equal(output.outcome,'matched');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.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','protected-source','preflight','pool-end'])
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/)
asyncError=false;result.platformProtectionVerified=false;await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Publisher terminal unconfirmed/)
result.platformProtectionVerified=true;protectedRefused=true;const before=retainedPools.length
await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Protected source refused/);assert.equal(retainedPools.length,before,'Protection failure must not open a Pool or fall back')
for(const [command,approvalEvent,outcome] of [['publish','approval','committed'],['reconcile','reconciliation-approval','confirmed']]){
const commandArgs=[command,...args.slice(1)]
const poolCount=retainedPools.length
await assert.rejects(compile<OperatorModule>(cli,modules).main(commandArgs),/Protected source refused/)
assert.equal(retainedPools.length,poolCount)
protectedRefused=false
events.length=0;reads.length=0
const terminal=await compile<OperatorModule>(cli,modules).main(commandArgs)
assert.equal(terminal.outcome,outcome);assert.equal(terminal.readyAuthorized,false)
assert.deepEqual(events,[approvalEvent,'transport','protected-source',command,'pool-end'])
assert.equal(reads.includes('signing-private.pem'),command==='publish')
protectedRefused=true
}
const activeArgs=['source-preflight',...args.slice(1),'--activation-directory','active-registry','--activation-sha256','e'.repeat(64)]
protectedRefused=false;activationRefused=true;events.length=0;reads.length=0
const beforeActivation=retainedPools.length
await assert.rejects(compile<OperatorModule>(cli,modules).main(activeArgs),/Activation refused/)
assert.equal(retainedPools.length,beforeActivation)
assert.deepEqual(events,['approval','transport','protected-source','activation-admission'])
activationRefused=false;events.length=0;reads.length=0
const active=await compile<OperatorModule>(cli,modules).main(activeArgs)
assert.equal(active.event,'application-writer-profile-source-preflight');assert.equal(active.sourceActivationMatched,true)
assert.equal(active.readyAuthorized,false);assert.equal(active.writerAuthorized,false)
assert.deepEqual(events,['approval','transport','protected-source','activation-admission','preflight','pool-end','activation-terminal'])
assert.deepEqual(reads,['bootstrap','approval','definition','transport','ca'])
incompatibleDefinition=true;events.length=0;reads.length=0
const beforeBuildRefusal=retainedPools.length
for(const incompatibleArgs of [args,activeArgs,['publish',...args.slice(1)]])await assert.rejects(compile<OperatorModule>(cli,modules).main(incompatibleArgs),/Build manifest refused/)
assert.equal(retainedPools.length,beforeBuildRefusal)
assert(!events.some(event=>event==='protected-source'||event==='activation-admission'))
const historical=await compile<OperatorModule>(cli,modules).main(['reconcile',...args.slice(1)])
assert.equal(historical.outcome,'confirmed');assert.equal(historical.readyAuthorized,false)
})
@@ -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())) } }
}
+151 -5
View File
@@ -6,6 +6,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'
const endpoint = new URL(process.env.OWN_MCP_ENDPOINT), origin = endpoint.origin
assert.equal(endpoint.hostname, '127.0.0.1'); assert.equal(process.env.OWN_EXTERNAL_CLIENT, 'true')
let phase = 'start', next = 0, maxResponseBytes = 0, stderrBytes = 0, bridgePid = null
const responseLimitBytes = 1024 * 1024
const pending = new Map(), cases = []
const activeClients = new Set()
let closing = false
@@ -22,7 +23,7 @@ async function boundedFetch(input, init) {
if (response.body) {
const reader = response.body.getReader()
try {
for (;;) { const item = await reader.read(); if (item.done) break; count += item.value.length; if (count >= 1048576) { await reader.cancel(); throw Error('Own external client response bound') }; chunks.push(item.value) }
for (;;) { const item = await reader.read(); if (item.done) break; count += item.value.length; if (count >= responseLimitBytes) { await reader.cancel(); throw Error('Own external client response bound') }; chunks.push(item.value) }
} finally { reader.releaseLock() }
}
const bytes = Buffer.concat(chunks); maxResponseBytes = Math.max(maxResponseBytes, bytes.length)
@@ -36,14 +37,14 @@ function content(result) {
return parsed
}
async function denied(work) { try { const r = await work(); assert.equal(r?.isError, true) } catch (e) { if (e?.code === 'ERR_ASSERTION') throw e; assert(!/timeout|timed out/i.test(String(e?.message)), 'A timeout is not authorization denial') } }
async function opened(mode, token) {
async function opened(mode, token, maxBufferSize = 131072) {
const client = new Client({ name: 'actual-external-process-' + mode, version: '1.0.0' })
activeClients.add(client)
let transport
if (mode === 'http') transport = new StreamableHTTPClientTransport(endpoint, { requestInit: { headers: { authorization: 'Bearer ' + token } }, fetch: boundedFetch })
else {
transport = new StdioClientTransport({ command: process.execPath, args: ['--import', import.meta.resolve('tsx'), process.env.OWN_BRIDGE_FILE],
cwd: process.cwd(), env: { MCP_URL: endpoint.href, MCP_TOKEN: token }, stderr: 'pipe', maxBufferSize: 131072 })
cwd: process.cwd(), env: { MCP_URL: endpoint.href, MCP_TOKEN: token }, stderr: 'pipe', maxBufferSize })
transport.stderr?.on('data', chunk => { stderrBytes += chunk.length; const text = String(chunk); assert(!text.includes(token)); assert(stderrBytes < 1048576) })
const start = transport.start.bind(transport)
transport.start = async () => { await start(); bridgePid = transport.pid; assert(bridgePid && bridgePid !== process.pid && bridgePid !== Number(process.env.OWN_SERVER_PID)); process.send({ kind: 'bridge-pid', pid: bridgePid }) }
@@ -68,6 +69,36 @@ try {
phase = mode + ':initialize'; let client = await opened(mode, token)
try {
await scoped(client); cases.push(mode + ': separate SDK process initializes, filters tool/host scope, bounds page/result and redacts credentials')
phase = mode + ':policy-edit'
const editScope = async operationIds => {
const response = await boundedFetch(origin + '/api/mcp/connections/' + initial.connectionId, {
method: 'PUT', headers: { authorization: 'Bearer ' + process.env.OWN_PORTAL_JWT, 'content-type': 'application/json' },
body: JSON.stringify({ name: 'Owned external SDK process', mode: 'observer', expiresAt: new Date(Date.now() + 600000).toISOString(),
permissions: ['mm:servers:read'], scope: { serverIds: [Number(process.env.OWN_ALLOWED_SERVER)], operationIds, allowGlobal: false }, limits: { callsPerMinute: 200 } }),
})
assert.equal(response.status, 200)
}
await editScope(['logs.query'])
assert.deepEqual((await client.listTools({}, { timeout: 5000 })).tools.map(tool => tool.name), ['mm_logs_query'])
const allowedServer = Number(process.env.OWN_ALLOWED_SERVER), forbiddenServer = Number(process.env.OWN_FORBIDDEN_SERVER)
const queryLogs = arguments_ => client.callTool({ name: 'mm_logs_query', arguments: arguments_ }, undefined, { timeout: 5000 })
const filters = { q: 'external-sdk-log-', limit: 1 }
const first = content(await queryLogs(filters))
assert.ok(first.nextCursor)
const second = content(await queryLogs({ ...filters, cursor: first.nextCursor }))
assert.equal(second.nextCursor, null)
assert.deepEqual([first.entries, second.entries].map(entries => entries.map(({ serverId, message }) => ({ serverId, message }))), [
[{ serverId: allowedServer, message: 'external-sdk-log-1' }],
[{ serverId: allowedServer, message: 'external-sdk-log-2' }],
])
await denied(() => queryLogs({ ...filters, serverId: forbiddenServer }))
cases.push(mode + ': populated log search pages return only allowed server records and reject the forbidden server')
await denied(() => client.callTool({ name: 'mm_servers_list', arguments: {} }, { timeout: 5000 }))
await editScope([])
assert.deepEqual((await client.listTools({}, { timeout: 5000 })).tools, [])
await editScope(['servers.list'])
await scoped(client)
cases.push(mode + ': established client observes removed and restored tool scope through the web policy API')
if (mode === 'stdio' && process.env.OWN_EXTERNAL_CLEANUP_FAULT === 'failure') throw Error('Owned deliberate external client failure with bridge active')
if (mode === 'stdio' && process.env.OWN_EXTERNAL_CLEANUP_FAULT === 'timeout') { process.send({ kind: 'arm-cleanup-timeout' }); await new Promise(() => {}) }
phase = mode + ':result-bound'; await control('large-result')
@@ -86,7 +117,7 @@ try {
} finally { await client.close().catch(() => {}) }
}
phase = 'http:body-bound'; const bound = await control('new-connection')
const oversized = await fetch(endpoint, { method: 'POST', headers: { authorization: 'Bearer ' + bound.token, 'content-type': 'application/json', accept: 'application/json,text/event-stream' },
const oversized = await fetch(endpoint, { method: 'POST', headers: { authorization: 'Bearer ' + bound.token, 'content-type': 'application/json', accept: 'application/json,text/event-stream', connection: 'close' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'mm_servers_list', arguments: { padding: 'x'.repeat(1048576) } } }), signal: AbortSignal.timeout(5000) })
assert.equal(oversized.status, 413); assert((await oversized.text()).length < 65536)
cases.push('actual HTTP request envelope over1MiB refused before tool execution; successful client results remain bounded')
@@ -126,8 +157,123 @@ try {
await denied(() => oauthClient.callTool({ name: 'mm_servers_list', arguments: {} }, undefined, { timeout: 5000 }))
cases.push('actual OAuth family revocation refuses established and fresh external transport; no implicit new consent granted')
} finally { await oauthClient.close().catch(() => {}) }
phase = 'discovery:issue'
const catalogue = await control('discovery-connection')
const expectedNames = catalogue.names.toSorted()
assert(expectedNames.includes('mm_logs_follow'))
const discoveryByTransport = {}
for (const mode of ['http', 'stdio']) {
phase = `discovery:${mode}`
const client = await opened(mode, catalogue.token, responseLimitBytes)
try {
const names = [], cursors = new Set()
let cursor, largestPageBytes = 0, pages = 0
do {
const page = await client.listTools(cursor ? { cursor } : {}, { timeout: 10000 })
assert(page.tools.length > 0)
for (const tool of page.tools) {
assert.equal(tool.inputSchema.type, 'object')
names.push(tool.name)
}
const encoded = JSON.stringify(page)
assert(!encoded.includes(catalogue.token) && !encoded.includes(process.env.OWN_SERVER_SECRET))
largestPageBytes = Math.max(largestPageBytes, Buffer.byteLength(encoded))
assert(largestPageBytes < responseLimitBytes)
pages++
cursor = page.nextCursor
if (cursor) { assert(!cursors.has(cursor)); cursors.add(cursor) }
assert(names.length <= expectedNames.length)
} while (cursor)
assert.deepEqual(names.toSorted(), expectedNames)
discoveryByTransport[mode] = { tools: names.length, pages, largestPageBytes }
cases.push(mode + ': external SDK discovers every delegated tool exactly once with an object input schema')
} finally { await client.close().catch(() => {}) }
}
phase = 'discovery:revoke'
await control('revoke')
for (const [mode, arm] of [['http', 'arm-log-revocation'], ['stdio', 'arm-log-revocation-after-commit'], ['http', 'arm-log-follow-revocation']]) {
phase = mode + ':' + arm
const { token } = await control(arm)
const logClient = await opened(mode, token)
try {
const follow = arm === 'arm-log-follow-revocation'
const tool = follow ? 'mm_logs_follow' : 'mm_logs_query'
const filters = { q: follow ? 'external-sdk-live-event' : 'external-sdk-log-1', limit: 1 }
if (follow) {
Object.assign(filters, { serverId: Number(process.env.OWN_ALLOWED_SERVER), waitSeconds: 0 })
const start = content(await logClient.callTool({ name: tool, arguments: filters }, undefined, { timeout: 5000 }))
assert.deepEqual(start.entries, [])
assert.equal(typeof start.cursor, 'string')
filters.cursor = start.cursor
filters.waitSeconds = 10
}
const query = logClient.callTool({ name: tool, arguments: filters }, undefined, { timeout: 10000 })
const checked = query.then(result => {
assert.equal(result.isError, true)
const wire = JSON.stringify(result)
assert.match(wire, /OWNER_CLAIMS_REVOKED|UNAUTHORIZED/)
assert.doesNotMatch(wire, /external-sdk-log-1|external-sdk-live-event|"entries"|"nextCursor"|"cursor"/)
})
const deliver = async () => {
if (follow) await control('publish-follow-fixture-row')
await control('deliver-log-revocation')
}
await Promise.all([checked, deliver()])
await assert.rejects(opened(mode, token))
cases.push(mode + ': signed portal revocation ' + (mode === 'stdio' ? 'after' : 'before') + ' audit commit denies ' + tool + ' result and reconnect')
} finally { await logClient.close().catch(() => {}) }
}
phase = 'portal:revoke-export-download'
const exportAccess = await control('new-artifact-connection')
const exportClient = await opened('http', exportAccess.token)
try {
const exported = content(await exportClient.callTool({ name: 'mm_logs_export', arguments: {
serverId: Number(process.env.OWN_ALLOWED_SERVER), q: 'external-sdk-log-1', format: 'json',
} }, undefined, { timeout: 5000 }))
assert.equal(exported.rows, 1)
assert.equal(exported.downloadPath, '/api/mcp/artifacts/' + exported.artifactId)
assert.doesNotMatch(JSON.stringify(exported), /external-sdk-log-1/)
const download = jwt => boundedFetch(origin + exported.downloadPath, { headers: { authorization: 'Bearer ' + jwt } })
const before = await download(exportAccess.ownerJwt)
assert.equal(before.status, 200)
assert.equal(before.headers.get('cache-control'), 'private, no-store')
const bytes = Buffer.from(await before.arrayBuffer())
assert.equal(createHash('sha256').update(bytes).digest('hex'), exported.sha256)
assert.match(bytes.toString('utf8'), /external-sdk-log-1/)
const fresh = await control('revoke-artifact-owner')
for (const jwt of [exportAccess.ownerJwt, fresh.ownerJwt]) {
const refused = await download(jwt)
assert.ok([401, 403].includes(refused.status))
assert.equal(refused.headers.has('content-disposition'), false)
assert.doesNotMatch(await refused.text(), /external-sdk-log-1/)
}
const management = await boundedFetch(origin + '/api/mcp/connections', { headers: { authorization: 'Bearer ' + fresh.ownerJwt } })
assert.equal(management.status, 200)
cases.push('portal owner revocation blocks an existing unexpired log artifact even with refreshed owner login')
} finally { await exportClient.close().catch(() => {}) }
phase = 'portal:revoke-approved-plan'
const planAccess = await control('new-revocable-plan-connection')
const planClient = await opened('http', planAccess.token)
try {
const plan = content(await planClient.callTool({ name: 'mm_logs_settings_update_plan', arguments: {
settings: { enabled: false },
} }, undefined, { timeout: 5000 }))
assert.equal(plan.state, 'awaiting-approval')
const approved = await boundedFetch(origin + '/api/mcp/plans/' + plan.planId + '/decision', {
method: 'POST', headers: { authorization: 'Bearer ' + planAccess.ownerJwt, 'content-type': 'application/json' },
body: JSON.stringify({ approve: true }),
})
assert.equal(approved.status, 200)
assert.deepEqual(await approved.json(), { ok: true })
await control('revoke-approved-plan-owner')
await denied(() => planClient.callTool({ name: 'mm_change_apply', arguments: {
planId: plan.planId, idempotencyKey: randomUUID(),
} }, undefined, { timeout: 5000 }))
cases.push('signed portal revocation invalidates the exact approved log settings plan before apply')
} finally { await planClient.close().catch(() => {}) }
process.send({ kind: 'result', result: { cases: cases.length, names: cases, externalClientPid: process.pid, serverPid: Number(process.env.OWN_SERVER_PID), bridgePid,
sdkOnlyExternalClient: true, maxObservedHttpResponseBytes: maxResponseBytes, clientResponseLimitBytes: 1048576, stdioSdkBufferLimitBytes: 131072,
sdkOnlyExternalClient: true, maxObservedHttpResponseBytes: maxResponseBytes, clientResponseLimitBytes: responseLimitBytes, stdioSdkBufferLimitBytes: 131072,
discoveryByTransport, discoveryStdioSdkBufferLimitBytes: responseLimitBytes,
actualPublicStringLimitObserved: 16000, wholeGenericResultEnvelopeLimitAccepted: false,
syntheticPortalJwtAndConsent: true, liveAuthPortal: false, livePortalSender: false, endUserUi: false, wholeExternalDirectionComplete: false } })
} catch (error) { process.send?.({ kind: 'failure', phase, errorType: error?.name ?? 'Error', completedCases: cases }); process.exitCode = 1 }
@@ -6,10 +6,16 @@ import { randomBytes, createHash } from 'node:crypto'
import { createServer } from 'node:net'
import { spawn } from 'node:child_process'
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',
'backend/src/modules/mcp/runtime.ts', 'backend/src/modules/mcp/actions.ts', 'backend/src/modules/mcp/security.ts', 'backend/src/modules/mcp/protocol-limits.ts',
'backend/src/modules/mcp/portal-revocation.ts', 'backend/src/plugins/auth.ts', 'backend/src/index.ts', 'node_modules/@modelcontextprotocol/client/package.json', 'node_modules/@modelcontextprotocol/server/package.json']
if (scenario === 'acceptance') paths.push('backend/scripts/mcp-external-process/client.mjs', 'backend/scripts/mcp-external-process/server-fixture.ts',
'backend/src/routes/mcp-portal.ts', 'backend/src/modules/mcp/portal-revocation-event.ts', 'backend/src/services/router-logs-query.ts')
else paths.push('backend/scripts/mcp-external-process/bridge-vlan-client.mjs', 'backend/scripts/mcp-external-process/bridge-vlan-fixture.ts',
'backend/src/modules/mcp/bridge-vlan-actions.ts', 'backend/src/services/bridge-vlan-configuration.ts', 'packages/contracts/src/bridge-vlan.ts')
async function pins() { const result = []; for (const path of paths) result.push({ path, sha256: createHash('sha256').update(await readFile(resolve(sourceRoot, path))).digest('hex') }); return result }
const before = await pins(), directory = await mkdtemp(join(resolve(sourceRoot, 'tmp'), 'mm-mcp-external-')), state = join(directory, 'state'); await mkdir(state)
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 +25,7 @@ const postgres = new EmbeddedPostgres({ databaseDir: join(directory, 'pg'), port
try {
await postgres.initialise(); await postgres.start()
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]])),
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' } })
@@ -1,4 +1,5 @@
import assert from "node:assert/strict"
import { createHash, createHmac, randomUUID } from "node:crypto"
import { spawn } from "node:child_process"
import { createServer } from "node:net"
import { fileURLToPath, pathToFileURL } from "node:url"
@@ -19,6 +20,7 @@ const { pool, closePool } = await import(source("db/index.js"))
const { applySqlMigrations } = await import(source("db/migrate.js"))
const { mcpConnectionCreateSchema } = await import("@mmapp/contracts/mcp")
const { createMcpConnection, rotateConnectionToken, setConnectionStatus, updateMcpSettings } = await import(source("modules/mcp/store.js"))
const { listActions } = await import(source("modules/mcp/actions.js")) as typeof import("../../src/modules/mcp/actions.js")
const { env } = await import(source("config.js"))
const { buildApp } = await import(source("index.js"))
const actor = { id: "owned-external-process-owner", email: "external@example.invalid", name: "Owned external SDK fixture", apps: ["mm"],
@@ -27,7 +29,27 @@ let currentId: string | undefined, app: Awaited<ReturnType<typeof buildApp>> | u
const credentials: string[] = []
const bridgePids = new Set<number>()
let childExit: Promise<number> | undefined
let releaseLogRead: (() => void) | undefined
let logReadCompleted: Promise<void> | undefined
let revocationOwnerId: string | undefined
const revokedLogRequests: { connectionId: string; tool: string }[] = []
const originalConnect = pool.connect
const delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
async function deliverPortalRevocation(ownerId: string) {
const secret = randomUUID() + randomUUID()
process.env.MCP_PORTAL_WEBHOOK_SECRET = secret
const occurredAt = Date.now(), timestamp = String(Math.floor(occurredAt / 1000))
const eventId = randomUUID(), body = JSON.stringify({ version: 2, eventId, userId: ownerId, type: "permissions.changed", eventOccurredAt: occurredAt })
const signature = "sha256=" + createHmac("sha256", secret).update(timestamp + "." + body).digest("hex")
const response = await fetch(origin + "/_internal/mcp/portal/revocations", {
method: "POST", headers: { "content-type": "application/json", "x-mm-timestamp": timestamp, "x-mm-signature": signature },
body, signal: AbortSignal.timeout(5000),
})
assert.equal(response.status, 200)
assert.deepEqual(await response.json(), { ok: true, duplicate: false })
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_portal_events WHERE id=$1", [eventId])).rows[0].count, 1)
return occurredAt
}
function alive(pid: number) { try { process.kill(pid, 0); return true } catch (error) { if ((error as NodeJS.ErrnoException).code === "ESRCH") return false; throw error } }
async function drainChildAndBridges() {
if (child && child.exitCode === null && child.signalCode === null) { child.kill(); await Promise.race([childExit, delay(5000).then(() => { throw Error("Owned external child failed to exit") })]) }
@@ -46,9 +68,73 @@ try {
const secret = "OWNED_DEVICE_CREDENTIAL_MUST_NOT_ESCAPE"
const allowed = (await pool.query("INSERT INTO servers(name,host,enabled,password) VALUES('owned allowed','192.0.2.1',false,$1) RETURNING id", [secret])).rows[0].id
const forbidden = (await pool.query("INSERT INTO servers(name,host,enabled) VALUES('owned forbidden','192.0.2.2',false) RETURNING id")).rows[0].id
await pool.query(`INSERT INTO router_log_entries(server_id,server_name,host,router_id,raw_time,received_at,effective_at,topics,level,message,generation)
SELECT id,name,host,'fixture','00:00:01',now(),now()-n*interval '1 second',ARRAY['system'],'info','external-sdk-log-'||n,0
FROM servers CROSS JOIN generate_series(1,2) AS n WHERE id=ANY($1::int[])`, [[allowed, forbidden]])
await updateMcpSettings({ enabled: true }, actor)
app = await buildApp({ startScheduler: false, logger: false }); await app.listen({ port, host: "127.0.0.1" })
const jwt = await new SignJWT({ sub: actor.id, apps: actor.apps, permissions: actor.permissions }).setProtectedHeader({ alg: "HS256" }).setIssuer(env.authIssuer).setIssuedAt().setExpirationTime("1h").sign(new TextEncoder().encode(env.jwtSecret))
const artifactOwner = { ...actor, id: actor.id + "-artifact" }
const signArtifactOwner = () => new SignJWT({ sub: artifactOwner.id, apps: artifactOwner.apps, permissions: artifactOwner.permissions })
.setProtectedHeader({ alg: "HS256" }).setIssuer(env.authIssuer).setIssuedAt().setExpirationTime("1h").sign(new TextEncoder().encode(env.jwtSecret))
let artifactConnectionId: string | undefined
const planOwner = { ...actor, id: actor.id + "-approved-plan", permissions: [...actor.permissions, "mm:mcp:approve"] }
let planConnectionId: string | undefined
const portalHeaders = { authorization: `Bearer ${jwt}`, "content-type": "application/json" }
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_connections WHERE owner_id=$1", [actor.id])).rows[0].count, 0)
const callback = "http://localhost:45678/callback?registered=1"
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
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]!])),
@@ -67,10 +153,131 @@ try {
if (message.kind !== "control" || !Number.isSafeInteger(message.id)) return
try {
let value: unknown
if (message.command === "new-connection") {
if (message.command === "new-revocable-plan-connection") {
assert(!planConnectionId)
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({
name: "Owned approved plan revocation", mode: "approval", permissions: ["mm:settings:admin"],
expiresAt: new Date(Date.now() + 600000).toISOString(),
scope: { serverIds: [Number(allowed), Number(forbidden)], operationIds: ["logs.settings.update", "changes.apply"], allowGlobal: true },
}), planOwner)
currentId = planConnectionId = issued.connection.id
const ownerJwt = await new SignJWT({ sub: planOwner.id, apps: planOwner.apps, permissions: planOwner.permissions })
.setProtectedHeader({ alg: "HS256" }).setIssuer(env.authIssuer).setIssuedAt().setExpirationTime("1h").sign(new TextEncoder().encode(env.jwtSecret))
credentials.push(issued.token, ownerJwt)
value = { token: issued.token, ownerJwt }
} else if (message.command === "revoke-approved-plan-owner") {
assert(planConnectionId && currentId === planConnectionId)
const plans = (await pool.query("SELECT id,state,approved_by FROM mcp_plans WHERE connection_id=$1", [planConnectionId])).rows
assert.equal(plans.length, 1)
assert.equal(plans[0].state, "approved")
assert.equal(plans[0].approved_by, planOwner.id)
await deliverPortalRevocation(planOwner.id)
value = { ok: true }
} else if (message.command === "new-artifact-connection") {
assert(!artifactConnectionId)
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({
name: "Owned export revocation", mode: "observer", permissions: ["mm:servers:read"],
expiresAt: new Date(Date.now() + 600000).toISOString(),
scope: { serverIds: [Number(allowed)], operationIds: ["logs.export"], allowGlobal: false },
}), artifactOwner)
currentId = artifactConnectionId = issued.connection.id
const ownerJwt = await signArtifactOwner()
credentials.push(issued.token, ownerJwt)
value = { token: issued.token, ownerJwt }
} else if (message.command === "revoke-artifact-owner") {
assert(artifactConnectionId && currentId === artifactConnectionId)
const occurredAt = await deliverPortalRevocation(artifactOwner.id)
while (Math.floor(Date.now() / 1000) <= Math.floor(occurredAt / 1000)) await delay(25)
const ownerJwt = await signArtifactOwner()
credentials.push(ownerJwt)
value = { ownerJwt }
} else if (["arm-log-revocation", "arm-log-revocation-after-commit", "arm-log-follow-revocation"].includes(message.command ?? "")) {
assert(!releaseLogRead)
const afterCommit = message.command === "arm-log-revocation-after-commit"
const follow = message.command === "arm-log-follow-revocation"
const owner = { ...actor, id: `${actor.id}-${follow ? "follow" : afterCommit ? "postcommit" : "precommit"}` }
revocationOwnerId = owner.id
if (follow) {
await pool.query("UPDATE servers SET enabled=true WHERE id=$1", [allowed])
await pool.query("UPDATE router_log_settings SET enabled=true WHERE id=1")
}
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({
name: "Owned portal revocation during log read", mode: "observer", permissions: ["mm:servers:read"],
expiresAt: new Date(Date.now() + 600000).toISOString(),
scope: { serverIds: [Number(allowed)], operationIds: [follow ? "logs.follow" : "logs.query"], allowGlobal: false },
}), owner)
currentId = issued.connection.id
revokedLogRequests.push({ connectionId: currentId, tool: follow ? "mm_logs_follow" : "mm_logs_query" })
credentials.push(issued.token)
let completed!: () => void
logReadCompleted = new Promise<void>(resolve => { completed = resolve })
const hold = new Promise<void>(resolve => { releaseLogRead = resolve })
pool.connect = ((callback?: unknown) => {
if (typeof callback === "function") return Reflect.apply(originalConnect, pool, [callback])
return (async () => {
const client = await originalConnect.call(pool)
const query = client.query, release = client.release
let completingLog = false
client.query = (async (...args: unknown[]) => {
const params = args[1]
if (typeof args[0] === "string" && args[0].startsWith("UPDATE mcp_requests SET status=$2,result=")
&& Array.isArray(params) && params[1] === "completed" && String(params[2]).includes(follow ? "external-sdk-live-event" : "external-sdk-log-1")) {
completingLog = true
if (!afterCommit) { completed(); await hold }
}
const result = await Reflect.apply(query, client, args)
if (afterCommit && completingLog && args[0] === "COMMIT") { completed(); await hold }
return result
}) as typeof query
client.release = error => {
client.query = query
client.release = release
release(error)
}
return client
})()
}) as typeof originalConnect
value = { token: issued.token }
} else if (message.command === "publish-follow-fixture-row") {
assert(releaseLogRead && currentId && revokedLogRequests.at(-1)?.tool === "mm_logs_follow")
const deadline = Date.now() + 3000
for (;;) {
const live = await pool.query("SELECT count(*)::int AS count FROM mcp_read_leases WHERE connection_id=$1", [currentId])
if (live.rows[0].count === 1) break
assert(Date.now() < deadline, "Owned follow did not acquire a live read lease")
await delay(25)
}
await pool.query(`INSERT INTO router_log_entries(server_id,server_name,host,router_id,raw_time,received_at,effective_at,topics,level,message,generation)
SELECT id,name,host,'fixture','00:00:01',now(),now(),ARRAY['system'],'info','external-sdk-live-event',0
FROM servers WHERE id=$1`, [allowed])
value = { ok: true }
} else if (message.command === "deliver-log-revocation") {
assert(logReadCompleted && releaseLogRead && currentId && revocationOwnerId)
try {
await Promise.race([logReadCompleted, delay(5000).then(() => { throw Error("Owned log read did not reach the barrier") })])
await deliverPortalRevocation(revocationOwnerId)
value = { ok: true }
} finally {
pool.connect = originalConnect
releaseLogRead()
releaseLogRead = undefined
logReadCompleted = undefined
revocationOwnerId = undefined
}
} else if (message.command === "new-connection") {
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Owned external SDK process", mode: "observer", expiresAt: new Date(Date.now() + 1200000).toISOString(),
permissions: ["mm:servers:read"], scope: { serverIds: [Number(allowed)], operationIds: ["servers.list"], allowGlobal: false }, limits: { callsPerMinute: 200 } }), actor)
currentId = issued.connection.id; credentials.push(issued.token); value = { token: issued.token, connectionId: currentId }
} else if (message.command === "discovery-connection") {
const operations = listActions().map(action => action.operation)
const permissions = [...new Set(operations.flatMap(operation => operation.requiredPermissions))]
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({
name: "Owned full catalogue discovery", mode: "approval", expiresAt: new Date(Date.now() + 600000).toISOString(),
permissions, scope: { serverIds: [Number(allowed)], operationIds: operations.map(operation => operation.id), allowGlobal: true },
}), { ...actor, permissions: [...new Set([...actor.permissions, ...permissions])] })
currentId = issued.connection.id
credentials.push(issued.token)
value = { token: issued.token, names: operations.map(operation => operation.toolName) }
} else if (message.command === "large-result" || message.command === "reset-result") {
await pool.query("UPDATE servers SET comment=$2 WHERE id=$1", [allowed, message.command === "large-result" ? "Z".repeat(40000) + "OWNED_TRUNCATION_TAIL" : ""])
value = { ok: true }
@@ -88,11 +295,30 @@ try {
const code = await childExit; clearTimeout(timer)
if (code !== 0 || !result) { console.log("OWNED_EXTERNAL_PROCESS_FAILURE", JSON.stringify({ code, failure, stdout: stdout.slice(0,1000), stderr: stderr.slice(0,1000) })); throw Error("External SDK process did not complete") }
const logs = JSON.stringify((await pool.query("SELECT tool,input,result,error_code FROM mcp_requests ORDER BY created_at")).rows)
assert.equal(revokedLogRequests.length, 3)
assert.ok(planConnectionId)
const invalidated = (await pool.query("SELECT state FROM mcp_plans WHERE connection_id=$1", [planConnectionId])).rows
assert.deepEqual(invalidated, [{ state: "invalidated" }])
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_operations WHERE connection_id=$1", [planConnectionId])).rows[0].count, 0)
assert.equal((await pool.query("SELECT enabled FROM router_log_settings WHERE id=1")).rows[0].enabled, true)
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_artifacts WHERE connection_id=$1 AND expires_at>now()", [artifactConnectionId])).rows[0].count, 1)
for (const { connectionId, tool } of revokedLogRequests) {
const revokedRead = (await pool.query("SELECT status,error_code,result FROM mcp_requests WHERE connection_id=$1 AND tool=$2 AND status='error'", [connectionId, tool])).rows
assert.equal(revokedRead.length, 1)
assert.equal(revokedRead[0].status, "error")
assert.deepEqual(Object.keys(revokedRead[0].result), ["message"])
assert.doesNotMatch(JSON.stringify(revokedRead[0].result), /external-sdk-log-1|external-sdk-live-event|"entries"|"nextCursor"|"cursor"/)
assert.ok(["OWNER_CLAIMS_REVOKED", "UNAUTHORIZED"].includes(revokedRead[0].error_code))
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_read_leases WHERE connection_id=$1", [connectionId])).rows[0].count, 0)
}
for (const token of [...credentials, jwt, secret]) { assert(!logs.includes(token)); assert(!stdout.includes(token)); assert(!stderr.includes(token)) }
const final = { ...(result as object), actualOwnPg: true, actualProductionHttpServer: true, serverFixturePid: process.pid,
controlTransitions: transcript, auditCredentialLeakAbsent: true, 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))
console.log("Owned external process SDK result:", JSON.stringify(final))
} finally {
pool.connect = originalConnect
releaseLogRead?.()
try { await drainChildAndBridges() } finally { try { if (app) await app.close() } finally { await closePool() } }
}
@@ -0,0 +1,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}))
}
@@ -0,0 +1,105 @@
import assert from 'node:assert/strict'
import {execFileSync} from 'node:child_process'
import {randomBytes, X509Certificate} from 'node:crypto'
import {mkdtemp, mkdir, cp, readFile, rm, chmod, access} from 'node:fs/promises'
import {createServer} from 'node:net'
import {tmpdir} from 'node:os'
import {dirname, join, resolve} from 'node:path'
import EmbeddedPostgres from 'embedded-postgres'
import pg from 'pg'
import {poolOptions, retainPublisherTlsPeer} from './writer-profile/operator-contract.mjs'
import {sha} from './writer-profile/operator-files.mjs'
const openssl = process.env.MMAPP_TLS_FIXTURE_OPENSSL || 'openssl'
const host = 'localhost'
const reservePort = async () => {
const listener = createServer()
await new Promise((ready, reject) => listener.once('error', reject).listen(0, '127.0.0.1', ready))
return listener
}
const releasePort = listener => new Promise((ready, reject) => listener.close(error => error ? reject(error) : ready()))
const sourcePort = await reservePort()
const clonePort = await reservePort()
const port = sourcePort.address().port, clonePortNumber = clonePort.address().port
await Promise.all([releasePort(sourcePort), releasePort(clonePort)])
const transport = {host, port, user: 'mcp_test', password: randomBytes(24).toString('hex'), database: 'postgres'}
const certFile = 'server.pem', keyFile = 'server-key.pem'
const root = await mkdtemp(join(tmpdir(), 'mm-writer-tls-owned-'))
const databaseDir = join(root, 'pg'), cloneDir = join(root, 'pg-clone')
const sourceTlsDir = join(root, 'source-tls'), cloneTlsDir = join(root, 'clone-tls')
const cluster = (databaseDir, port, tlsDir) => new EmbeddedPostgres({databaseDir, port, user: transport.user, password: transport.password,
persistent: true, createPostgresUser: false, postgresFlags: ['-h', '127.0.0.1', '-c', 'ssl=on', '-c', `ssl_cert_file=${join(tlsDir, certFile)}`, '-c', `ssl_key_file=${join(tlsDir, keyFile)}`],
onLog: () => {}, onError: () => {}})
const postgres = cluster(databaseDir, port, sourceTlsDir), clone = cluster(cloneDir, clonePortNumber, cloneTlsDir)
let sourceStarted = false, cloneStarted = false
const fingerprint = ca => sha(new X509Certificate(ca).publicKey.export({format:'der', type:'spki'}))
const issue = async tlsDir => {
await mkdir(tlsDir, {mode:0o700})
execFileSync(openssl, ['req', '-new', '-newkey', 'rsa:2048', '-x509', '-nodes', '-days', '1', '-subj', '/CN=localhost',
'-addext', 'subjectAltName=DNS:localhost', '-keyout', keyFile, '-out', certFile], {cwd: tlsDir, stdio: 'ignore', windowsHide: true})
await chmod(join(tlsDir, keyFile), 0o600)
return readFile(join(tlsDir, certFile))
}
const withClient = async (config, work) => {
const pool = new pg.Pool({connectionTimeoutMillis:5000, ...config})
let client
try { client = await pool.connect(); return await work(client) }
finally { client?.release(true); await pool.end() }
}
const query = "SELECT (pg_control_system()).system_identifier::text AS system, oid::text AS oid FROM pg_database WHERE datname=current_database()"
try {
await postgres.initialise()
const first = await issue(sourceTlsDir)
await postgres.start(); sourceStarted = true
const pin = {serverName:host, caSha256:sha(first), spkiSha256:fingerprint(first)}
const request = {tls:pin, roles:{publisher:{name:transport.user}}, source:{databaseName:transport.database}}
const baseline = await withClient(poolOptions(transport, request, first), async client => {
const retained = retainPublisherTlsPeer(client, pin)
retained()
const {rows:[source]} = await client.query(query)
const {rows:[status]} = await client.query('SELECT ssl FROM pg_stat_ssl WHERE pid=pg_backend_pid()')
assert.equal(status.ssl, true)
await client.query("CREATE TABLE owned_tls_fixture (note text PRIMARY KEY); INSERT INTO owned_tls_fixture VALUES ('original')")
const connection = client.connection, socket = connection.stream
connection.stream = undefined
try { assert.throws(retained, /retained TLS peer unavailable/) } finally { connection.stream = socket }
return source
})
await withClient({...transport, ssl:false}, async client => assert.throws(() => retainPublisherTlsPeer(client,pin), /retained TLS peer unavailable/))
await postgres.stop(); sourceStarted = false
await cp(databaseDir, cloneDir, {recursive:true, force:false, errorOnExist:true})
await assert.rejects(access(join(cloneDir, keyFile)), {code:'ENOENT'})
const second = await issue(cloneTlsDir)
assert.notEqual(fingerprint(second), pin.spkiSha256)
await postgres.start(); sourceStarted = true
await clone.start(); cloneStarted = true
const cloneTransport = {...transport, port:clonePortNumber}
const bothTrusted = Buffer.concat([first,second])
await assert.rejects(withClient(poolOptions(cloneTransport, {...request,tls:{...pin,caSha256:sha(bothTrusted)}}, bothTrusted),
() => assert.fail('Wrong physical cluster must not reach application SQL')), /TLS SPKI refused/)
await withClient({...cloneTransport, ssl:{ca:bothTrusted, servername:host, rejectUnauthorized:true}}, async client => {
assert.equal(client.connection.stream.authorized, true)
const {rows:[source]} = await client.query(query)
assert.deepEqual(source, baseline)
assert.throws(() => retainPublisherTlsPeer(client,pin), /TLS SPKI refused/)
retainPublisherTlsPeer(client,{...pin,spkiSha256:fingerprint(second)})()
assert.deepEqual((await client.query('SELECT note FROM owned_tls_fixture')).rows, [{note:'original'}])
await client.query("INSERT INTO owned_tls_fixture VALUES ('clone-only')")
})
await withClient(poolOptions(transport, request, first), async client => {
retainPublisherTlsPeer(client,pin)()
assert.deepEqual((await client.query('SELECT note FROM owned_tls_fixture')).rows, [{note:'original'}])
})
console.log('Genuine PostgreSQL TLS: original accepted; independently writable physical clone with identical DB coordinates and a different CA-valid TLS key refused')
} finally {
const stops = await Promise.allSettled([
cloneStarted ? clone.stop() : undefined,
sourceStarted ? postgres.stop() : undefined,
])
assert(stops.every(result => result.status === 'fulfilled'), 'Owned cluster stop unconfirmed')
assert.equal(dirname(resolve(root)), resolve(tmpdir()))
assert.match(root, /mm-writer-tls-owned-/)
await rm(root, {recursive:true, force:true, maxRetries:20, retryDelay:100})
await assert.rejects(access(root), {code:'ENOENT'})
console.log('Owned PostgreSQL and temporary TLS material removed')
}
+20 -7
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')
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')
const routingForwardRounds = process.argv.includes('--routing-network-forward-rounds')
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 routingOriginalEffects = process.argv.includes('--routing-original-effects')
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')
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')
@@ -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')
const externalPeerMetadata = process.argv.includes('--external-peer-metadata')
if (externalPeerMetadata && process.argv.slice(2).some(arg => arg !== '--external-peer-metadata')) throw new Error('Guarded external peer metadata requires a separate disposable cluster without restore')
const mcpOperationOrigin = process.argv.includes('--mcp-operation-origin')
if (mcpOperationOrigin && process.argv.slice(2).some(arg => arg !== '--mcp-operation-origin')) throw new Error('MCP operation root requires a separate disposable cluster without restore')
const pollOperationRetirement = process.argv.includes('--poll-operation-retirement')
const ordinaryOperationRetirement = process.argv.includes('--ordinary-operation-retirement')
const probeOperationRetirement = process.argv.includes('--probe-operation-retirement')
const mcpOperationOrigin = process.argv.includes('--mcp-operation-origin') || pollOperationRetirement || ordinaryOperationRetirement || probeOperationRetirement
if (mcpOperationOrigin && process.argv.slice(2).some(arg => arg !== (probeOperationRetirement ? '--probe-operation-retirement' : ordinaryOperationRetirement ? '--ordinary-operation-retirement' : pollOperationRetirement ? '--poll-operation-retirement' : '--mcp-operation-origin'))) throw new Error('MCP operation root requires a separate disposable cluster without restore')
const mcpSpeedOriginal = process.argv.includes('--mcp-speed-original')
if (mcpSpeedOriginal && process.argv.slice(2).some(arg => arg !== '--mcp-speed-original')) throw new Error('MCP speed component requires a separate disposable cluster without restore')
const routerConfigurationGuard = process.argv.includes('--router-configuration-guard')
@@ -161,6 +165,10 @@ const chrVpn = process.argv.includes('--chr-vpn-catalog')
const chrFilters = process.argv.includes('--chr-filter-write')
const chrLegacyOspf = process.argv.includes('--chr-ospf-legacy')
const greRevision = process.argv.includes('--gre-revision')
const usersBindings = process.argv.includes('--users-bindings')
if (usersBindings && (process.argv.length !== 3 || process.argv[2] !== '--users-bindings')) throw new Error('User bindings acceptance requires its own disposable cluster')
const bridgeVlanConfiguration = process.argv.includes('--bridge-vlan-configuration')
if (bridgeVlanConfiguration && process.argv.slice(2).some(arg => arg !== '--bridge-vlan-configuration')) throw new Error('Bridge VLAN acceptance requires its own disposable cluster')
const applicationAdapters = process.argv.includes('--application-adapters')
const scopedRoutingDiagnostics = process.argv.includes('--scoped-routing-diagnostics')
const interfaceEventsCompatibility = process.argv.includes('--interface-events-compatibility')
@@ -242,17 +250,18 @@ try {
if (regressionOnly) console.log('Selected regression only: primary integration and terminal restore are not run')
if (profilesOnly) console.log('Profiles-only regression matrix: primary integration NOT RUN; terminal restore NOT RUN; full acceptance is not claimed')
const result = regressionOnly || profilesOnly ? 0 : await new Promise((resolve, reject) => {
const child = spawn(process.execPath, pollMemory ? ['--max-old-space-size=192', '--max-semi-space-size=16', fileURLToPath(new URL('../dist-mcp-memory/services/server-poll-memory.owned.test.js', import.meta.url))] : ['--import', 'tsx', principalSecuritySource ? 'src/modules/mcp/principal-security-source.owned.test.ts' : pollJournalSnapshot ? 'src/services/server-poll-journal-snapshot.owned.test.ts' : pollTerminalArchive ? 'src/services/server-poll-terminal-archive.owned.test.ts' : pollArchiveSchema ? 'src/db/server-poll-terminal-archives.sql.test.ts' : telemetrySettingsConfiguration ? 'src/modules/mcp/telemetry-settings-configuration.owned.test.ts' : routingReservationSchema ? 'src/db/routing-reservation-schema.owned.test.ts' : routingForwardRounds ? 'src/modules/mcp/routing-network-forward-rounds.owned.test.ts' : flowActiveServices ? 'src/services/traffic-flow-active-services.owned.test.ts' : flowActiveRebuild ? 'src/services/traffic-flow-active-rebuild.owned.test.ts' : flowQuality ? 'src/services/traffic-flow-quality.owned.test.ts' : flowWorkerIpc ? 'src/services/traffic-flow-worker-ipc.owned.test.ts' : manualUptime ? 'src/modules/mcp/manual-uptime-http.test.ts' : manualPingGroup ? 'src/modules/mcp/manual-ping-group-http.test.ts' : manualTraffic ? 'src/modules/mcp/manual-traffic-http.test.ts' : manualApiPing ? 'src/modules/mcp/manual-api-ping-http.test.ts' : simpleSpeedChr ? 'src/modules/mcp/simple-speed-chr.test.ts' : simpleSpeed ? 'src/modules/mcp/simple-speed-http.test.ts' : externalPeerMetadata ? 'src/services/bandwidth-external-peer-metadata.sql.test.ts' : operatorPeerMetadata ? 'src/services/bandwidth-operator-metadata.sql.test.ts' : publicRegistryMetadata ? 'src/services/bandwidth-public-registry.sql.test.ts' : operatorDockerSql ? 'src/services/bandwidth-operator-docker-stop.sql.test.ts' : mcpOperationOrigin ? 'src/modules/mcp/operation-execution-origin.sql.test.ts' : mcpSpeedOriginal ? 'src/modules/mcp/speed-bandwidth-original.sql.test.ts' : routerConfigurationGuard ? 'src/services/router-configuration-guard.sql.test.ts' : scheduledSpeedOriginal ? 'src/services/bandwidth-scheduled-speed-original.sql.test.ts' : bandwidthSpeedWorkflow ? 'src/services/bandwidth-speed-workflow.sql.test.ts' : restSpeedOriginal ? 'src/services/bandwidth-rest-speed-original.sql.test.ts' : operationLocks ? 'src/services/operation-config-lock-lease.sql.test.ts' : serviceExecutionOrigin ? 'src/modules/mcp/service-execution-origin.sql.test.ts' : bandwidthNodeSource ? 'src/services/bandwidth-node-source.sql.test.ts' : bandwidthExternalRegistry ? 'src/services/bandwidth-external-node-registry.sql.test.ts' : bandwidthNodeRegistry ? 'src/services/bandwidth-node-registry.sql.test.ts' : bandwidthControlSource ? 'src/services/bandwidth-control-source.sql.test.ts' : bandwidthAdmission ? 'src/services/bandwidth-admission.sql.test.ts' : serverPollStorageFresh ? 'src/services/server-poll-storage-fresh.sql.test.ts' : serverPollStorage ? 'src/services/server-poll-storage.sql.test.ts' : probeStorageFresh ? 'src/services/server-probe-storage-fresh.sql.test.ts' : probeLegacyOrphans ? 'src/services/server-probe-legacy-orphans.sql.test.ts' : diagnosticCancelHost ? 'src/modules/mcp/diagnostic-cancellation-storage.sql.test.ts' : ipsecExportHttp ? 'src/modules/mcp/ipsec-existing-certificate-export-http.sql.test.ts' : ipsecExportJournal ? 'src/services/ipsec-existing-certificate-export-rest-journal.sql.test.ts' : migrationDataInventory ? 'src/db/migration-data-inventory.sql.test.ts' : probeStorageHistory ? 'src/services/server-probe-storage-history.sql.test.ts' : probeStorageJournal ? 'src/services/server-probe-storage-journal.sql.test.ts' : controlledSchemaReference ? 'src/db/controlled-schema-reference.sql.test.ts' : controlledMigrations ? 'src/db/controlled-migrations.sql.test.ts' : databaseSchemaControl ? 'src/db/database-schema-control.sql.test.ts' : migrationSchemaInventory ? 'src/db/migration-schema-inventory.sql.test.ts' : routingNetworkNative ? 'src/modules/mcp/routing-network-native.sql.test.ts' : routingReservationRuntime ? 'src/modules/mcp/routing-reservation-runtime.sql.test.ts' : routingReservations ? 'src/modules/mcp/routing-reservation.sql.test.ts' : collectorPartitionWriters ? 'src/services/collector-partition-writes.sql.test.ts' : serverPollDurable ? 'src/services/server-poll-durable-session.sql.test.ts' : routingRecoveryAuthority ? 'src/modules/mcp/routing-recovery-authority.sql.test.ts' : legacyFlowRebuild ? 'src/test/legacy-flow-rebuild-sql.ts' : probeJournal ? 'src/services/server-probe-journal.sql.test.ts' : flowHostKeys ? 'src/modules/mcp/flow-host-keys-http.test.ts' : flowRebuildMcp ? 'src/modules/mcp/flow-facts-rebuild-http.test.ts' : flowRebuild ? 'src/test/flow-facts-rebuild-sql.ts' : flowWrites ? 'src/services/traffic-flow-write-transaction.pg.test.ts' : nativeConservation ? 'src/modules/mcp/native-conservation-sql.test.ts' : nativeCreateId ? 'src/modules/mcp/native-create-id-http.test.ts' : ipsecAdapters ? "src/mLine truncated
const child = spawn(process.execPath, pollMemory ? ['--max-old-space-size=192', '--max-semi-space-size=16', fileURLToPath(new URL('../dist-mcp-memory/services/server-poll-memory.owned.test.js', import.meta.url))] : ['--import', 'tsx', usersBindings ? 'src/modules/mcp/users-bindings-owned.test.ts' : bridgeVlanConfiguration ? 'src/modules/mcp/bridge-vlan-configuration-http.test.ts' : principalSecuritySource ? 'src/modules/mcp/principal-security-source.owned.test.ts' : pollJournalSnapshot ? 'src/services/server-poll-journal-snapshot.owned.test.ts' : pollTerminalArchive ? 'src/services/server-poll-terminal-archive.owned.test.ts' : pollArchiveSchema ? 'src/db/server-poll-terminal-archives.sql.test.ts' : telemetrySettingsConfiguration ? 'src/modules/mcp/telemetry-settings-configuration.owned.test.ts' : routingReservationSchema ? 'src/db/routing-reservation-schema.owned.test.ts' : routingForwardRounds ? 'src/modules/mcp/routing-network-forward-rounds.owned.test.ts' : flowActiveServices ? 'src/services/traffic-flow-active-services.owned.test.ts' : flowActiveRebuild ? 'src/services/traffic-flow-active-rebuild.owned.test.ts' : flowQuality ? 'src/services/traffic-flow-quality.owned.test.ts' : flowWorkerIpc ? 'src/services/traffic-flow-worker-ipc.owned.test.ts' : manualUptime ? 'src/modules/mcp/manual-uptime-http.test.ts' : manualPingGroup ? 'src/modules/mcp/manual-ping-group-http.test.ts' : manualTraffic ? 'src/modules/mcp/manual-traffic-http.test.ts' : manualApiPing ? 'src/modules/mcp/manual-api-ping-http.test.ts' : simpleSpeedChr ? 'src/modules/mcp/simple-speed-chr.test.ts' : simpleSpeed ? 'src/modules/mcp/simple-speed-http.test.ts' : externalPeerMetadata ? 'src/services/bandwidth-external-peer-metadata.sql.test.ts' : operatorPeerMetadata ? 'src/services/bandwidth-operator-metadata.sql.test.ts' : publicRegistryMetadata ? 'src/services/bandwidth-public-registry.sql.test.ts' : operatorDockerSql ? 'src/services/bandwidth-operator-docker-stop.sql.test.ts' : probeOperationRetirement ? 'src/services/server-probe-operation-retirement.owned.test.ts' : ordinaryOperationRetirement ? 'src/modules/mcp/ordinary-operation-retirement.owned.test.ts' : pollOperationRetirement ? 'src/services/server-poll-operation-retirement.owned.test.ts' : mcpOperationOrigin ? 'src/modules/mcp/operation-execution-origin.sql.test.ts' : mcpSpeedOriginal ? 'src/modules/mcp/speed-bandwidth-original.sql.test.ts' : routerConfigurationGuard ? 'src/services/router-configuration-guard.sql.test.ts' : scheduledSpeedOriginal ? 'src/services/bandwidth-scheduled-speed-original.sql.test.ts' : bandwidthSpeedWorkflow ? 'src/services/bandwidth-speed-workflow.sql.test.ts' : restSpeedOriginal ? 'src/services/bandwidth-rest-speed-original.sql.test.ts' : operationLocks ? 'src/services/operation-config-lock-lease.sql.test.ts' : serviceExecutionOrigin ? 'src/modules/mcp/service-execution-origin.sql.test.ts' : bandwidthNodeSource ? 'src/services/bandwidth-node-source.sql.test.ts' : bandwidthExternalRegistry ? 'src/services/bandwidth-external-node-registry.sql.test.ts' : bandwidthNodeRegistry ? 'src/services/bandwidth-node-registry.sql.test.ts' : bandwidthControlSource ? 'src/services/bandwidth-control-source.sql.test.ts' : bandwidthAdmission ? 'src/services/bandwidth-admission.sql.test.ts' : serverPollStorageFresh ? 'src/services/server-poll-storage-fresh.sql.test.ts' : serverPollStorage ? 'src/services/server-poll-storage.sql.test.ts' : probeStorageFresh ? 'src/services/server-probe-storage-fresh.sql.test.ts' : probeLegacyOrphans ? 'src/services/server-probe-legacy-orphans.sql.test.ts' : diagnosticCancelHost ? 'src/modules/mcp/diagnostic-cancellation-storage.sql.test.ts' : ipsecExportHttp ? 'src/modules/mcp/ipsec-existing-certificate-export-http.sql.test.ts' : ipsecExportJournal ? 'src/services/ipsec-existing-certificate-export-rest-journal.sql.test.ts' : migrationDataInventory ? 'src/db/migration-data-inventory.sql.test.ts' : probeStorageHistory ? 'src/services/server-probe-storage-history.sql.test.ts' : probeStorageJournal ? 'src/services/server-probe-storage-journal.sql.test.ts' : controlledSchemaReference ? 'src/db/controlled-schema-reference.sql.test.ts' : controlledMigrations ? 'src/db/controlled-migrations.sql.test.ts' : databaseSchemaControl ? 'src/db/database-schema-control.sql.test.ts' : migrationSchemaInventory ? 'src/db/migration-schema-inventory.sql.test.ts' : routingNetworkNative ? 'src/modules/mcp/routing-network-native.sql.test.ts' : routingReservationRuntime ? 'src/modules/mcp/routing-reservation-runtime.sql.test.ts' : routingReservations ? 'src/modules/mcp/routing-reservation.sql.test.ts' : collectorPartitionWriters ? 'src/services/collector-partition-writes.sql.test.ts' : serverPollDurable ? 'src/services/server-poll-durable-session.sql.test.ts' : routingRecoveryAuthority ? 'src/modules/mcp/routing-recovery-authority.sql.test.ts' : legacyFlowRebuild ? 'src/test/legacy-flow-rebuild-sql.ts' : probeJournal ? 'src/services/server-probe-journal.sql.testLine truncated
cwd: pollMemory ? directory : new URL('../', import.meta.url), stdio: 'inherit', env: { ...(pollMemory ? pollMemoryEnvironment : process.env),
...(routingOriginalEffects ? { MMAPP_OWN_ROUTING_ORIGINAL_EFFECTS: 'true' } : {}),
DATABASE_URL: `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres`,
DATABASE_PATH: join(directory, 'no-legacy-database.sqlite'), SQLITE_IMPORT_FULL_HISTORY: 'false',
MCP_STATE_DIR: join(directory, 'state'), AUTH_REQUIRED: 'true', AUTH_JWT_SECRET: 'mcp-integration-test-only', MMAPP_TEST_WG_EXPORT_ONLY: String(wireguardExport),
MCP_PUBLIC_URL: 'http://localhost:8000/mcp', NODE_ENV: 'test', PATH: testPath, NODE_OPTIONS: pollMemory ? '' : process.env.NODE_OPTIONS, MMAPP_OWN_PRINCIPAL_SECURITY_SOURCE: String(principalSecuritySource), MMAPP_OWN_POLL_JOURNAL_SNAPSHOT: String(pollJournalSnapshot), MMAPP_OWN_POLL_MEMORY: String(pollMemory), MMAPP_OWN_POLL_BOOTSTRAP: String(pollMemoryBootstrap), MMAPP_OWN_POLL_COST_DIAGNOSTICS: String(pollMemoryCost), MMAPP_OWN_POLL_ARCHIVE_CLAIM_LOSS: String(pollArchiveClaimLoss), MMAPP_OWN_POLL_ARCHIVE_HISTORY: String(pollArchiveHistory), MMAPP_OWN_POLL_ARCHIVE_COLD: String(pollArchiveCold), MMAPP_OWN_POLL_ARCHIVE_LOST_ACK: String(pollArchiveLostAck), MMAPP_OWN_POLL_TERMINAL_ARCHIVE: String(pollTerminalArchive), MMAPP_OWN_POLL_ARCHIVE_SCHEMA: String(pollArchiveSchema), MMAPP_OWN_TELEMETRY_SETTINGS_CONFIGURATION: String(telemetrySettingsConfiguration), MMAPP_OWN_ROUTING_RESERVATION_SCHEMA: String(routingReservationSchema), MMAPP_OWN_ROUTING_FORWARD_ROUNDS: String(routingForwardRounds), MMAPP_OWN_FLOW_ACTIVE_SERVICES: String(flowActiveServices), MMAPP_OWN_FLOW_ACTIVE_REBUILD: String(flowActiveRebuild), MMAPP_OWN_FLOW_QUALITY: String(flowQuality), MMAPP_OWN_FLOW_WORKER_IPC: String(flowWorkerIpc), MMAPP_OWN_MANUAL_UPTIME: String(manualUptime), MMAPP_OWN_MANUAL_PING_GROUP: String(manualPingGroup), MMAPP_OWN_MANUAL_TRAFFIC: String(manualTraffic), MMAPP_OWN_MANUAL_API_PING: String(manualApiPing), MMAPP_OWN_SIMPLE_SPEED: String(simpleSpeed), MMAPP_OWN_SIMPLE_SPEED_CHR: String(simpleSpeedChr), MMAPP_OWN_DIAGNOSTIC_CANCEL_HOST: String(diagnosticCancelHost), MMAPP_OWN_OPERATOR_DOCKER_SQL: String(operatorDockerSql), MMAPP_OWN_PUBLIC_REGISTRY_METADATA: String(publicRegistryMetadata), MMAPP_OWN_OPERATOR_PEER_METADATA: String(operatorPeerMetadata), MMAPP_OWN_EXTERNAL_PEER_METADATA: String(externalPeerMetadata), MMAPP_OWN_BANDWIDTH_EXTERNAL_REGISTRY: String(externalPeerMetadata || operatorPeerMetadata || publicRegistryMetadata || operatorDockerSql || mcpSpeedOriginal || bandwidthSpeedWorkflow || bandwidthExternalRegistry), MMAPP_OWN_BANDWIDTH_SPEED_WORKFLOW: String(bandwidthSpeedWorkflow), MMAPP_OWN_REST_SPEED_ORIGINAL: String(restSpeedOriginal), MMAPP_OWN_MCP_OPERATION_ORIGIN: String(mcpOperationOrigin), MMAPP_OWN_MCP_SPEED_ORIGINAL: String(mcpSpeedOriginal), MMAPP_OWN_SCHEDULED_SPEED_ORIGINAL: String(scheduledSpeedOriginal), MMAPP_TEST_ROUTER_GUARD_OWNED: String(routerConfigurationGuard), MMAPP_TEST_ROUTER_GUARD_CLUSTER_DISPOSABLE: String(routerConfigurationGuard), MMAPP_TEST_ROUTER_GUARD_ADMIN_URL: routerConfigurationGuard ? `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres` : '', MMAPP_OWN_BANDWIDTH_ADMISSION: String(externalPeerMetadata || operatorPeerMetadata || publicRegistryMetadata || operatorDockerSql || mcpOperationOrigin || mcpSpeedOriginal || scheduledSpeedOriginal || bandwidthSpeedWorkflow || restSpeedOriginal || bandwidthExternalRegistry || operationLocks || serviceExecutionOrigin || bandwidthAdmission || bandwidthControlSource || bandwidthNodeRegistry || bandwidthNodeSource), MMAPP_OWN_BANDWIDTH_RESTORE: String((bandwidthAdmission || bandwidthControlSource || bandwidthNodeRegistry || bandwidthNodeSource) && process.argv.includes('--restore')), MMAPP_OWN_IPSEC_EXPORT_HTTP: String(ipsecExportHttp), MMAPP_OWN_IPSEC_EXPORT_JOURNAL: String(ipsecExportJournal), MMAPP_OWN_SQL_DB: 'postgres', MMAPP_OWN_PROBE_LEGACY_ORPHANS: String(probeLegacyOrphans), MMAPP_OWN_PROBE_STORAGE_HISTORY: String(probeStorageHistory), MMAPP_OWN_PROBE_STORAGE_FRESH: String(probeStorageFresh), MMAPP_OWN_PROBE_STORAGE_JOURNAL: String(probeStorageJournal || probeStorageFresh), MMAPP_OWN_CONTROLLED_SCHEMA_REFERENCE: String(controlledSchemaReference), MMAPP_OWN_CONTROLLED_MIGRATIONS: String(controlledMigrations), MMAPP_OWN_DATABASE_SCHEMA_CONTROL: String(databaseSchemaControl), MMAPP_OWN_MIGRATION_DATA_INVENTORY: String(migrationDataInventory), MMAPP_OWN_MIGRATION_SCHEMA_INVENTORY: String(migrationSchemaInventory), MMAPP_OWN_ROUTING_NETWORK_NATIVE: String(routingNetworkNative), MMAPP_OWN_ROUTING_RESERVATIONS: String(routingReservations || routingReservationRuntime), MMAPP_OWN_COLLECTOR_PARTITION_WRITERS: String(collectorPartitionWriters), MMAPP_OWN_SERVER_POLL_STORAGE_FRESH: String(serverPollStorageFresh), MMAPP_OWN_SERVER_POLL_STORAGE: String(serverPollStorage), MMAPP_OWN_SERVER_POLL_DURABLE: String(serverPollDurable), MMAPP_OWN_ROUTING_RECOVERY_AUTHORITY: String(routingRecoveryAuthority), MMAPP_OWN_LEGACY_FLOW_REBUILD: String(legacyFlowRebuild), MMAPP_OWN_PROBE_JOURNAL: String(probeJournal), MMAPP_MODULE_COMPLETION_ONLY: String(moduleCompletion), MMAPP_MODULE_COMPLETION_TARGET: completionTarget, MMAPP_OWN_FLOW_HOST_KEYS: String(flowHostKeys), MMAPP_OWN_MCP_FLOW_REBUILD: String(flowRebuildMcp), MMAPP_OWN_FLOW_WRITES: String(flowWrites), MMAPP_OWN_FLOW_REBUILD: String(flowRebuild) },
MCP_PUBLIC_URL: 'http://localhost:8000/mcp', NODE_ENV: 'test', PATH: testPath, NODE_OPTIONS: pollMemory ? '' : process.env.NODE_OPTIONS, MMAPP_OWN_PRINCIPAL_SECURITY_SOURCE: String(principalSecuritySource), MMAPP_OWN_POLL_JOURNAL_SNAPSHOT: String(pollJournalSnapshot), MMAPP_OWN_POLL_MEMORY: String(pollMemory), MMAPP_OWN_POLL_BOOTSTRAP: String(pollMemoryBootstrap), MMAPP_OWN_POLL_COST_DIAGNOSTICS: String(pollMemoryCost), MMAPP_OWN_POLL_ARCHIVE_CLAIM_LOSS: String(pollArchiveClaimLoss), MMAPP_OWN_POLL_ARCHIVE_HISTORY: String(pollArchiveHistory), MMAPP_OWN_POLL_ARCHIVE_COLD: String(pollArchiveCold), MMAPP_OWN_POLL_ARCHIVE_LOST_ACK: String(pollArchiveLostAck), MMAPP_OWN_POLL_TERMINAL_ARCHIVE: String(pollTerminalArchive), MMAPP_OWN_POLL_ARCHIVE_SCHEMA: String(pollArchiveSchema), MMAPP_OWN_TELEMETRY_SETTINGS_CONFIGURATION: String(telemetrySettingsConfiguration), MMAPP_OWN_ROUTING_RESERVATION_SCHEMA: String(routingReservationSchema), MMAPP_OWN_ROUTING_FORWARD_ROUNDS: String(routingForwardRounds), MMAPP_OWN_FLOW_ACTIVE_SERVICES: String(flowActiveServices), MMAPP_OWN_FLOW_ACTIVE_REBUILD: String(flowActiveRebuild), MMAPP_OWN_FLOW_QUALITY: String(flowQuality), MMAPP_OWN_FLOW_WORKER_IPC: String(flowWorkerIpc), MMAPP_OWN_MANUAL_UPTIME: String(manualUptime), MMAPP_OWN_MANUAL_PING_GROUP: String(manualPingGroup), MMAPP_OWN_MANUAL_TRAFFIC: String(manualTraffic), MMAPP_OWN_MANUAL_API_PING: String(manualApiPing), MMAPP_OWN_SIMPLE_SPEED: String(simpleSpeed), MMAPP_OWN_SIMPLE_SPEED_CHR: String(simpleSpeedChr), MMAPP_OWN_DIAGNOSTIC_CANCEL_HOST: String(diagnosticCancelHost), MMAPP_OWN_OPERATOR_DOCKER_SQL: String(operatorDockerSql), MMAPP_OWN_PUBLIC_REGISTRY_METADATA: String(publicRegistryMetadata), MMAPP_OWN_OPERATOR_PEER_METADATA: String(operatorPeerMetadata), MMAPP_OWN_EXTERNAL_PEER_METADATA: String(externalPeerMetadata), MMAPP_OWN_BANDWIDTH_EXTERNAL_REGISTRY: String(externalPeerMetadata || operatorPeerMetadata || publicRegistryMetadata || operatorDockerSql || mcpSpeedOriginal || bandwidthSpeedWorkflow || bandwidthExternalRegistry), MMAPP_OWN_BANDWIDTH_SPEED_WORKFLOW: String(bandwidthSpeedWorkflow), MMAPP_OWN_REST_SPEED_ORIGINAL: String(restSpeedOriginal), MMAPP_OWN_MCP_OPERATION_ORIGIN: String(mcpOperationOrigin), MMAPP_OWN_PROBE_OPERATION_RETIREMENT: String(probeOperationRetirement), MMAPP_OWN_MCP_SPEED_ORIGINAL: String(mcpSpeedOriginal), MMAPP_OWN_SCHEDULED_SPEED_ORIGINAL: String(scheduledSpeedOriginal), MMAPP_TEST_ROUTER_GUARD_OWNED: String(routerConfigurationGuard), MMAPP_TEST_ROUTER_GUARD_CLUSTER_DISPOSABLE: String(routerConfigurationGuard), MMAPP_TEST_ROUTER_GUARD_ADMIN_URL: routerConfigurationGuard ? `postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres` : '', MMAPP_OWN_BANDWIDTH_ADMISSION: String(externalPeerMetadata || operatorPeerMetadata || publicRegistryMetadata || operatorDockerSql || mcpOperationOrigin || mcpSpeedOriginal || scheduledSpeedOriginal || bandwidthSpeedWorkflow || restSpeedOriginal || bandwidthExternalRegistry || operationLocks || serviceExecutionOrigin || bandwidthAdmission || bandwidthControlSource || bandwidthNodeRegistry || bandwidthNodeSource), MMAPP_OWN_BANDWIDTH_RESTORE: String((bandwidthAdmission || bandwidthControlSource || bandwidthNodeRegistry || bandwidthNodeSource) && process.argv.includes('--restore')), MMAPP_OWN_IPSEC_EXPORT_HTTP: String(ipsecExportHttp), MMAPP_OWN_IPSEC_EXPORT_JOURNAL: String(ipsecExportJournal), MMAPP_OWN_SQL_DB: 'postgres', MMAPP_OWN_PROBE_LEGACY_ORPHANS: String(probeLegacyOrphans), MMAPP_OWN_PROBE_STORAGE_HISTORY: String(probeStorageHistory), MMAPP_OWN_PROBE_STORAGE_FRESH: String(probeStorageFresh), MMAPP_OWN_PROBE_STORAGE_JOURNAL: String(probeStorageJournal || probeStorageFresh), MMAPP_OWN_CONTROLLED_SCHEMA_REFERENCE: String(controlledSchemaReference), MMAPP_OWN_CONTROLLED_MIGRATIONS: String(controlledMigrations), MMAPP_OWN_DATABASE_SCHEMA_CONTROL: String(databaseSchemaControl), MMAPP_OWN_MIGRATION_DATA_INVENTORY: String(migrationDataInventory), MMAPP_OWN_MIGRATION_SCHEMA_INVENTORY: String(migrationSchemaInventory), MMAPP_OWN_ROUTING_NETWORK_NATIVE: String(routingNetworkNative), MMAPP_OWN_ROUTING_RESERVATIONS: String(routingReservations || routingReservationRuntime), MMAPP_OWN_COLLECTOR_PARTITION_WRITERS: String(collectorPartitionWriters), MMAPP_OWN_SERVER_POLL_STORAGE_FRESH: String(serverPollStorageFresh), MMAPP_OWN_SERVER_POLL_STORAGE: String(serverPollStorage), MMAPP_OWN_SERVER_POLL_DURABLE: String(serverPollDurable), MMAPP_OWN_ROUTING_RECOVERY_AUTHORITY: String(routingRecoveryAuthority), MMAPP_OWN_LEGACY_FLOW_REBUILD: String(legacyFlowRebuild), MMAPP_OWN_PROBE_JOURNAL: String(probeJournal), MMAPP_MODULE_COMPLETION_ONLY: String(moduleCompletion), MMAPP_MODULE_COMPLETION_TARGET: completionTarget, MMAPP_OWN_FLOW_HOST_KEYS: String(flowHostKeys), MMAPP_OWN_MCP_FLOW_REBUILD: String(flowRebuildMcp), MMAPP_OWN_FLOW_WRITES: String(flowWrites), MMAPP_OWN_FLOW_REBUILD: String(flowRebuild) },
})
child.once('error', reject); child.once('exit', code => resolve(code ?? 1))
})
exitCode = result
if (result === 0 && !regressionOnly && !profilesOnly && !pollMemory && !principalSecuritySource && !pollJournalSnapshot && !pollArchiveSchema && !telemetrySettingsConfiguration && !routingReservationSchema && !routingForwardRounds && !flowActiveServices && !flowActiveRebuild && !flowQuality && !flowWorkerIpc && !manualUptime && !manualPingGroup && !manualTraffic && !manualApiPing && !simpleSpeed && !simpleSpeedChr && !externalPeerMetadata && !operatorPeerMetadata && !publicRegistryMetadata && !operatorDockerSql && !mcpOperationOrigin && !mcpSpeedOriginal && !routerConfigurationGuard && !scheduledSpeedOriginal && !bandwidthSpeedWorkflow && !restSpeedOriginal && !operationLocks && !serviceExecutionOrigin && !bandwidthNodeSource && !bandwidthNodeRegistry && !bandwidthExternalRegistry && !bandwidthControlSource && !bandwidthAdmission && !probeLegacyOrphans && !probeStorageJournal && !probeStorageHistory && !probeJournal && !chr && !flowWrites && !flowRebuild && !flowRebuildMcp && (process.argv.includes('--restore') || process.env.MMAPP_PG_CLIENT_BIN)) {
if (result === 0 && !regressionOnly && !profilesOnly && !usersBindings && !bridgeVlanConfiguration && !pollMemory && !principalSecuritySource && !pollJournalSnapshot && !pollArchiveSchema && !telemetrySettingsConfiguration && !routingReservationSchema && !routingForwardRounds && !flowActiveServices && !flowActiveRebuild && !flowQuality && !flowWorkerIpc && !manualUptime && !manualPingGroup && !manualTraffic && !manualApiPing && !simpleSpeed && !simpleSpeedChr && !externalPeerMetadata && !operatorPeerMetadata && !publicRegistryMetadata && !operatorDockerSql && !mcpOperationOrigin && !mcpSpeedOriginal && !routerConfigurationGuard && !scheduledSpeedOriginal && !bandwidthSpeedWorkflow && !restSpeedOriginal && !operationLocks && !serviceExecutionOrigin && !bandwidthNodeSource && !bandwidthNodeRegistry && !bandwidthExternalRegistry && !bandwidthControlSource && !bandwidthAdmission && !probeLegacyOrphans && !probeStorageJournal && !probeStorageHistory && !probeJournal && !chr && !flowWrites && !flowRebuild && !flowRebuildMcp && (process.argv.includes('--restore') || process.env.MMAPP_PG_CLIENT_BIN)) {
exitCode = await new Promise((resolve, reject) => {
const child = spawn(process.execPath, ['--import', 'tsx', 'src/modules/mcp/database-restore.test.ts'], {
cwd: new URL('../', import.meta.url), stdio: 'inherit', env: { ...process.env,
@@ -268,6 +277,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 && 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 && 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 && 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')
@@ -283,6 +293,9 @@ try {
else if (result === 0 && operatorPeerMetadata) console.log('Operator peer metadata: actual managed endpoint SQL and genuine localhost license origin; synthetic Q, external metadata and full operator window remain open')
else if (result === 0 && publicRegistryMetadata) console.log('Public registry metadata: actual owned PostgreSQL and same physical lease; no secret/Q/native/write authority or backend-window acceptance')
else if (result === 0 && operatorDockerSql) console.log('Operator Docker SQL: actual owned PostgreSQL with mocked Docker HTTP only; not a real Docker/backend-window acceptance')
else if (result === 0 && ordinaryOperationRetirement) console.log('Ordinary operation retirement: owned actual logs-settings apply and worker')
else if (result === 0 && pollOperationRetirement) console.log('Poll operation retirement: owned authenticated historical fixtures and current reader/GC')
else if (result === 0 && probeOperationRetirement) console.log('Diagnostic retirement: actual owned worker tick, PostgreSQL and product read/replay; no live scheduling')
else if (result === 0 && mcpOperationOrigin) console.log('MCP operation root: owned PostgreSQL ordinary runner provenance; no restore/recovery, global writer activation, CHR or deployment')
else if (result === 0 && mcpSpeedOriginal) console.log('MCP speed: genuine trusted service context with real connection/grant/SQL, host receipt and localhost native; synthetic Q/mapping; not a public MCP runner/action acceptance')
else if (result === 0 && routerConfigurationGuard) console.log('Router configuration guard: dedicated owned databases/roles and localhost native fixture; no application migration, production activation or real CHR')
@@ -1,22 +1,25 @@
import EmbeddedPostgres from 'embedded-postgres'
import { createRequire } from 'node:module'
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 { createServer } from 'node:net'
import { spawn } from 'node:child_process'
const allowed = new Set(['--default-scoped', '--main-only', '--check-only'])
if (process.argv.slice(2).some(arg => !allowed.has(arg)) || new Set(process.argv.slice(2)).size !== process.argv.slice(2).length || process.argv.includes('--default-scoped') && process.argv.includes('--main-only')) throw new Error('Usage: node backend/scripts/test-routing-controller-baseline.mjs [--default-scoped | --main-only] [--check-only]')
const modes = ['--default-scoped', '--main-only', '--scoped-bare-readback']
const allowed = new Set([...modes, '--check-only']), args = process.argv.slice(2)
const selected = modes.filter(flag => args.includes(flag))
if (args.some(arg => !allowed.has(arg)) || new Set(args).size !== args.length || selected.length > 1) throw new Error('Usage: node backend/scripts/test-routing-controller-baseline.mjs [--default-scoped | --main-only | --scoped-bare-readback] [--check-only]')
const root = resolve(import.meta.dirname, '../..'), require = createRequire(import.meta.url), ts = require('typescript')
const mode = process.argv.includes('--default-scoped') ? 'default-scoped' : process.argv.includes('--main-only') ? 'main-only' : 'main-and-forward-rounds'
const mode = selected[0]?.slice(2) ?? 'main-and-forward-rounds'
const target = join(root, 'backend/src/modules/mcp/routing-controller-active-writer.ts')
await readFile(target)
await readFile(join(root, 'backend/src/modules/mcp/routing-main-transition.ts'))
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) }
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')
async function compile(sourceDirectory) {
for (const entry of await readdir(sourceDirectory, { withFileTypes: true })) {
@@ -59,7 +62,7 @@ try {
// embedded-postgres waits for a new exit event; avoid waiting again if its
// owned child has already exited. This never targets another database.
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 })
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 (['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]
if (Number.isSafeInteger(value.wireguardPeerTelemetryTicks) && value.wireguardPeerTelemetryTicks > 0 && value.wireguardPeerTelemetryTicks <= 10000) row.wireguardPeerTelemetryTicks = value.wireguardPeerTelemetryTicks
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 (!Object.keys(row).length) { omittedLines++; continue }
@@ -47,10 +48,10 @@ export function projectVrfDiagnostics(log, privateValues = [], expectedSlice = n
}
export function vrfSliceDiagnostic(result, log, childBudgetMs, privateValues = []) {
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.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.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 }
@@ -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 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]]))
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
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)
assert(rel && !rel.startsWith('..') && !isAbsolute(rel)); await mkdir(cwd)
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)
})
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))
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 }
lastSliceResult = result; currentChildExitConfirmed = exited
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 }))
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 }
await persist('terminal.json', { version: 1, status: 'terminal', ...receipt, requiredCases: 68, completedCases: results.reduce((sum, value) => sum + value.caseCount, 0), diagnosticDirectory: diagnosticPath })
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: 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))
return receipt
} 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)
})
+2 -2
View File
@@ -7,7 +7,7 @@ export function approvedDefinition(value:unknown){
assert(value&&typeof value==='object');const v=value as Record<string,unknown>
assert.deepEqual(Object.keys(v).sort(),['format','provenance','template']);assert.equal(v.format,'application-writer-source44-definition-approval-v1')
assert.equal(canonical(v.provenance),canonical(BUILD44_PROVENANCE));assert.equal(canonical(v.template),canonical(FIXED44_ROLE_TEMPLATE))
return BUILD44_PROVENANCE.templateSha256
return Object.freeze({definitionSha256:BUILD44_PROVENANCE.templateSha256,manifestSha256:BUILD44_PROVENANCE.manifestSha256})
}
/** Real incremental DATA verifier on caller's retained transaction. No pool or attestor. */
export async function collectSource44Catalogue(s:{query:(sql:string,values?:unknown[])=>Promise<{rows:Record<string,unknown>[]} >},installation:{roleBindings:ConstructorParameters<typeof Fixed44CatalogueValueBridgeData>[0]}){
@@ -21,7 +21,7 @@ export async function collectSource44Catalogue(s:{query:(sql:string,values?:unkn
await s.query('DECLARE mm_publisher44 NO SCROLL CURSOR WITHOUT HOLD FOR '+FIXED44_CATALOGUE_BUILD_SQL,values);cursor=true
for(;;){const page=(await s.query('FETCH FORWARD 128 FROM mm_publisher44')).rows;bridge.acceptPage(page);if(page.length===0)break}
const result=bridge.finish();await s.query('CLOSE mm_publisher44');cursor=false
return {catalogSha256:sha(canonical({values:result.values.snapshotSha256,catalogue:result.catalogue})),definitionSha256:result.catalogue.definitionSha256,leafCount:result.catalogue.leafRelations}
return {catalogSha256:sha(canonical({values:result.values.snapshotSha256,catalogue:result.catalogue})),definitionSha256:result.catalogue.definitionSha256,manifestSha256:result.catalogue.manifestSha256,leafCount:result.catalogue.leafRelations}
}catch(error){primary=error;failed=true;throw error}
finally{
try{if(cursor)await s.query('CLOSE mm_publisher44');await s.query("SELECT pg_catalog.set_config('search_path',$1,true),pg_catalog.set_config('TimeZone',$2,true),pg_catalog.set_config('DateStyle',$3,true)",[settings[0].path,settings[0].zone,settings[0].dates])}
@@ -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}
})
}
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict'
import {X509Certificate} from 'node:crypto'
import {checkServerIdentity} from 'node:tls'
import {checkServerIdentity,TLSSocket} from 'node:tls'
import {sha} from './operator-files.mjs'
export const roleKeys=Object.freeze(['runtime','operator','capture','publisher','control'])
export const STARTUP_OPTIONS='-c statement_timeout=10000 -c lock_timeout=5000 -c client_encoding=UTF8'
@@ -18,15 +18,29 @@ export function approval(value){
}
// Pinned historical inspection data only; never publication authorization.
export function reconciliationApproval(value){return parsedApproval(value)}
function peerError(pin,host,cert){
const failed=checkServerIdentity(pin.serverName,cert);if(failed)return failed
try{if(host!==pin.serverName||sha(new X509Certificate(cert.raw).publicKey.export({format:'der',type:'spki'}))!==pin.spkiSha256)return Error('Publisher TLS SPKI refused')}catch{return Error('Publisher TLS certificate refused')}
}
// pg replaces connection.stream with the negotiated TLS socket. Hold its
// identity on the retained client; handshake callbacks are not session evidence.
export function retainPublisherTlsPeer(client,pin){
const socket=client.connection?.stream
const assertCurrent=()=>{
assert(socket instanceof TLSSocket&&client.connection?.stream===socket&&socket.encrypted&&socket.authorized&&!socket.destroyed&&socket.writable,'Publisher retained TLS peer unavailable')
}
assertCurrent()
const failed=peerError(pin,socket.servername,socket.getPeerCertificate());if(failed)throw failed
socket.disableRenegotiation()
return assertCurrent
}
export function poolOptions(transport,request,ca){
assert(exact(transport,['host','port','user','password','database']))
assert(typeof transport.host==='string'&&transport.host===request.tls.serverName&&Number.isSafeInteger(transport.port)&&transport.port>0&&transport.port<=65535&&transport.user===request.roles.publisher.name&&typeof transport.password==='string'&&transport.password.length>0&&transport.password.length<=4096&&transport.database===request.source.databaseName)
assert(sha(ca)===request.tls.caSha256);new X509Certificate(ca)
return {host:transport.host,port:transport.port,user:transport.user,password:transport.password,database:transport.database,max:1,connectionTimeoutMillis:5000,idleTimeoutMillis:5000,statement_timeout:10000,lock_timeout:5000,options:STARTUP_OPTIONS,application_name:'mmapp-writer-publisher',client_encoding:'UTF8',
ssl:{ca,servername:request.tls.serverName,rejectUnauthorized:true,checkServerIdentity(host,cert){
const failed=checkServerIdentity(request.tls.serverName,cert);if(failed)return failed
try{if(host!==request.tls.serverName||sha(new X509Certificate(cert.raw).publicKey.export({format:'der',type:'spki'}))!==request.tls.spkiSha256)return Error('Publisher TLS SPKI refused')}catch{return Error('Publisher TLS certificate refused')}
}}}
ssl:{ca,servername:request.tls.serverName,rejectUnauthorized:true,checkServerIdentity:(host,cert)=>peerError(request.tls,host,cert)}}
}
export function assertPublicationApproval(current,catalogue,structure,pinned,manifestSha256){
assert(pinned.expiresAt>Date.now()&&pinned.manifestSha256===manifestSha256&&pinned.definitionSha256===catalogue.definitionSha256)
@@ -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)
}))
+42 -20
View File
@@ -3,6 +3,7 @@ import pg from 'pg'
import {createPrivateKey,createPublicKey,sign} from 'node:crypto'
import {open,lstat,realpath,unlink} from 'node:fs/promises'
import {writeFile,withJournalLease} from './operator-files.mjs'
import {retainPublisherTlsPeer} from './operator-contract.mjs'
import path from 'node:path'
import {captureDatabaseSourceObservation,assertDatabaseSourceObservationCurrent} from '../../src/db/database-source-observation.js'
import {MIGRATION_MANIFEST,migrationManifestDigest} from '../../src/db/migration-manifest.js'
@@ -10,7 +11,13 @@ import {MIGRATION_MANIFEST,migrationManifestDigest} from '../../src/db/migration
import {ownedContext,sameOwned,sourceCoordinates,protectedStructure,readCurrent,HISTORY,CURRENT} from './context.mjs'
import {bytes,canonical,sha,nextEpoch,POLICY_DOMAIN,HEAD_DOMAIN,KEY,liveValidity,policySchema,headSchema,publicKey} from '../../src/db/application-writer-profile-format.js'
export function createConnectedPublisher(installation,collectFixedCatalogue,sourceVerifier,approval){
export function requireCurrentCatalogueDefinition(approval,definition){
const manifestSha256=migrationManifestDigest()
if(approval.manifestSha256!==manifestSha256||definition.manifestSha256!==manifestSha256||definition.definitionSha256!==approval.definitionSha256)throw Error('Approved catalogue definition is not bound to this build migration manifest')
return manifestSha256
}
export function createConnectedPublisher(installation,collectFixedCatalogue,sourceVerifier,approval,definition){
const PUBLISHER=installation.roleBindings.publisher.name
class ProfilePublicationError extends Error{
automaticReplay=false
@@ -29,13 +36,18 @@ async function privateKey(filename){
// detached application callbacks or cleanup/COMMIT races.
async function session(pool,deadlineAt,work){
if(!(pool instanceof pg.Pool)||!Number.isSafeInteger(deadlineAt)||deadlineAt<=Date.now()||deadlineAt-Date.now()>120000)throw Error('Bounded genuine publisher pool/lifetime required')
let client,retired=false,released=false,locked=false,begun=false,commitSent=false,committed=false,confirmedValue,proposal,phase='checkout',stopReject,cleanupUnconfirmed=false
let client,assertTransport,retired=false,released=false,locked=false,begun=false,commitSent=false,committed=false,confirmedValue,proposal,phase='checkout',stopReject,cleanupUnconfirmed=false
const stopped=new Promise((_,reject)=>{stopReject=reject});void stopped.catch(()=>{})
const release=bad=>{if(!client||released)return;released=true;client.off('error',fault);client.off('end',fault);try{client.release(bad)}catch{cleanupUnconfirmed=true}}
const fault=()=>{if(retired)return;retired=true;cleanupUnconfirmed=true;release(true);stopReject(Error('Publisher connection/lifetime ended'))}
const timer=setTimeout(fault,Math.max(1,deadlineAt-Date.now())),bounded=p=>Promise.race([p,stopped])
const query=async(text,values)=>{if(retired||Date.now()>=deadlineAt)throw Error('Publisher lifetime closed');return bounded(client.query(text,values))}
const fence=()=>{
if(retired||Date.now()>=deadlineAt)throw Error('Publisher lifetime closed')
try{assertTransport()}catch(error){fault();throw error}
}
const query=async(text,values)=>{fence();return bounded(client.query(text,values))}
const state={query,phase:value=>{phase=value},proposal:value=>{proposal=Object.freeze(value)},begun:value=>{begun=value},commit:async value=>{
fence()
confirmedValue=Object.freeze({...value})
commitSent=true
const response=await bounded(client.query('COMMIT').then(result=>{if(result.command==='COMMIT'){committed=true;begun=false}return result}))
@@ -45,6 +57,7 @@ async function session(pool,deadlineAt,work){
try{
const pending=pool.connect().then(value=>{client=value;if(retired){release(true);return value}client.on('error',fault);client.on('end',fault);return value});void pending.catch(()=>{})
await bounded(pending);if(retired)throw Error('Late publisher checkout')
try{assertTransport=retainPublisherTlsPeer(client,installation.tls)}catch(error){retired=true;release(true);throw error}
await query("SET statement_timeout='10s'");await query("SET lock_timeout='5s'")
phase='exclusive-admission';await query('SELECT pg_catalog.pg_advisory_lock($1::bigint)',[KEY]);locked=true
value=await work(state)
@@ -62,7 +75,16 @@ async function session(pool,deadlineAt,work){
if(failed||cleanupUnconfirmed||retired)throw new ProfilePublicationError(commitSent||begun?'unknown':'not-committed',phase,proposal,cleanupUnconfirmed||retired,primary)
return value
}
async function publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256,journalDirectory,validityMs=86400000,deadlineAt=Date.now()+120000}){
async function observeCurrentCatalogue(s,manifestSha256){
const security=await protectedStructure(s,installation),catalogue=await collectFixedCatalogue(s)
if(catalogue.manifestSha256!==manifestSha256||catalogue.definitionSha256!==definition.definitionSha256)throw Error('Catalogue evidence differs from the approved migration manifest/definition')
// Inspect one extra row so an unexpected marker cannot hide after the expected prefix.
const expected=MIGRATION_MANIFEST.map(pin=>pin.file.slice(0,-4))
const markers=(await s.query('SELECT id FROM public.schema_migrations ORDER BY id COLLATE "C" LIMIT $1',[expected.length+1])).rows.map(row=>row.id)
if(canonical(markers)!==canonical(expected))throw Error('Exact current migration marker metadata required')
return {security,catalogue,catalogSha256:sha(canonical({base:catalogue.catalogSha256,security:security.sha256}))}
}
async function publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256,journalDirectory,validityMs=86400000,deadlineAt=Date.now()+120000},manifestSha256){
if(!Number.isSafeInteger(validityMs)||validityMs<1||validityMs>86400000||expectedHeadSha256!==null&&!(/^[a-f0-9]{64}$/).test(expectedHeadSha256))throw Error('Explicit previous head and bounded validity required')
const key=await privateKey(privateKeyFile),directory=await realpath(journalDirectory),pendingFile=path.join(directory,'pending-publication.json')
try{await lstat(pendingFile);throw Error('Pending publication must be reconciled before signing')}catch(error){if(error.code!=='ENOENT')throw error}
@@ -73,18 +95,15 @@ async function publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256,jou
const observation=await captureDatabaseSourceObservation(facade,options)
await sourceVerifier.verify(facade,observation,options)
if(canonical(sourceCoordinates(first))!==canonical(installation.expectedSource))throw Error('Pinned source mismatch')
const security=await protectedStructure(s,installation),catalogue=await collectFixedCatalogue(s)
const markers=(await s.query('SELECT id FROM public.schema_migrations ORDER BY id LIMIT 45')).rows.map(row=>row.id)
if(canonical(markers)!==canonical(MIGRATION_MANIFEST.map(pin=>pin.file.slice(0,-4))))throw Error('Exact current migration marker metadata required')
const {security,catalogue,catalogSha256}=await observeCurrentCatalogue(s,manifestSha256)
const old=await readCurrent(s,installation,{allowEmpty:true,validNow:false})
if((old?.headSha256??null)!==expectedHeadSha256)throw Error('Expected protected current head mismatch')
const catalogSha256=sha(canonical({base:catalogue.catalogSha256,security:security.sha256}))
if(approval.expiresAt<=Date.now()||approval.manifestSha256!==migrationManifestDigest()||approval.definitionSha256!==catalogue.definitionSha256||approval.catalogSha256!==catalogSha256)throw Error('Independently approved policy differs from actual source44 catalogue')
if(old&&(canonical(old.policy.source)!==canonical(sourceCoordinates(first))||canonical(old.policy.roles)!==canonical(security.roles)||old.policy.manifestSha256!==migrationManifestDigest()||old.policy.definitionSha256!==catalogue.definitionSha256||old.policy.catalogSha256!==catalogSha256))throw Error('Renewal cannot adopt a changed source/subject/policy')
if(approval.expiresAt<=Date.now()||approval.catalogSha256!==catalogSha256)throw Error('Independently approved policy differs from actual catalogue')
if(old&&(canonical(old.policy.source)!==canonical(sourceCoordinates(first))||canonical(old.policy.roles)!==canonical(security.roles)||old.policy.manifestSha256!==manifestSha256||old.policy.definitionSha256!==catalogue.definitionSha256||old.policy.catalogSha256!==catalogSha256))throw Error('Renewal cannot adopt a changed source/subject/policy')
await sourceVerifier.verify(facade,observation,options);await assertDatabaseSourceObservationCurrent(facade,observation,options);sameOwned(first,await ownedContext(s,installation,PUBLISHER,'publisher-read'))
await s.query('COMMIT');s.begun(false)
const number=old?nextEpoch(old.epoch):'1',now=Date.now()
const body=policySchema.parse({format:'application-writer-policy-v1',installationId:installation.installationId,epoch:number,issuedAt:now,notBefore:now,notAfter:now+validityMs,source:sourceCoordinates(first),sourceObservationSha256:sha(canonical(observation)),roles:security.roles,manifestSha256:migrationManifestDigest(),definitionSha256:catalogue.definitionSha256,catalogSha256,futureLeaves:'fixed12-native-policy-v1'})
const body=policySchema.parse({format:'application-writer-policy-v1',installationId:installation.installationId,epoch:number,issuedAt:now,notBefore:now,notAfter:now+validityMs,source:sourceCoordinates(first),sourceObservationSha256:sha(canonical(observation)),roles:security.roles,manifestSha256,definitionSha256:catalogue.definitionSha256,catalogSha256,futureLeaves:'fixed12-native-policy-v1'})
const envelope=(domain,body)=>bytes({body,signature:sign(null,Buffer.from(domain+canonical(body)),key).toString('base64url')})
const policyBytes=envelope(POLICY_DOMAIN,body),policySha256=sha(policyBytes)
const head=headSchema.parse({format:'application-writer-head-v1',installationId:installation.installationId,epoch:number,policySha256,previousHeadSha256:old?.headSha256??null})
@@ -101,8 +120,8 @@ async function publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256,jou
await s.query(`SELECT id FROM mmapp_control.${CURRENT} WHERE id=1 FOR UPDATE`)
const current=await readCurrent(s,installation,{allowEmpty:true,validNow:false})
if((current?.headSha256??null)!==expectedHeadSha256)throw Error('CAS predecessor changed')
const againSecurity=await protectedStructure(s,installation),againCatalogue=await collectFixedCatalogue(s)
if(sha(canonical({base:againCatalogue.catalogSha256,security:againSecurity.sha256}))!==catalogSha256)throw Error('Policy drift during retained exclusive publication')
const again=await observeCurrentCatalogue(s,manifestSha256)
if(again.catalogSha256!==catalogSha256)throw Error('Policy drift during retained exclusive publication')
if(Date.now()>=body.notAfter||Date.now()>=approval.expiresAt)throw Error('Policy or approval expired before publication dispatch')
await s.query(`INSERT INTO mmapp_control.${HISTORY}(installation_id,epoch,head_sha256,previous_installation_id,previous_epoch,previous_head_sha256,policy_sha256,policy_bytes,head_bytes) VALUES($1::uuid,$2::bigint,$3,$4::uuid,$5::bigint,$6,$7,$8::bytea,$9::bytea)`,[installation.installationId,number,headSha256,old?installation.installationId:null,old?.epoch??null,old?.headSha256??null,policySha256,policyBytes,headBytes])
const changed=old?await s.query(`UPDATE mmapp_control.${CURRENT} SET epoch=$1::bigint,head_sha256=$2 WHERE id=1 AND installation_id=$3::uuid AND epoch=$4::bigint AND head_sha256=$5 RETURNING id`,[number,headSha256,installation.installationId,old.epoch,old.headSha256]):await s.query(`INSERT INTO mmapp_control.${CURRENT}(id,installation_id,epoch,head_sha256) VALUES(1,$1::uuid,$2::bigint,$3) RETURNING id`,[installation.installationId,number,headSha256])
@@ -136,19 +155,19 @@ async function inspectPublication(pool,proposal,{deadlineAt=Date.now()+120000}={
// Read-only diagnostics on the actual protected publisher session. This does
// not admit application/operator writes, bootstrap, restore, or READY.
async function preflightCurrentProfile(pool,{deadlineAt=Date.now()+120000}={}){
const manifestSha256=requireCurrentCatalogueDefinition(approval,definition)
return session(pool,deadlineAt,async s=>{
s.phase('profile-preflight');await s.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');s.begun(true)
const first=await ownedContext(s,installation,PUBLISHER,'publisher-read')
if(canonical(sourceCoordinates(first))!==canonical(installation.expectedSource))throw Error('Pinned preflight source mismatch')
const facade=Object.freeze({query:(sql,values)=>s.query(sql,values)}),options={deadlineAt}
const observation=await captureDatabaseSourceObservation(facade,options);await sourceVerifier.verify(facade,observation,options)
const security=await protectedStructure(s,installation),catalogue=await collectFixedCatalogue(s)
const observation=await captureDatabaseSourceObservation(facade,options),initialSource=await sourceVerifier.verify(facade,observation,options)
const {security,catalogue,catalogSha256}=await observeCurrentCatalogue(s,manifestSha256)
const current=await readCurrent(s,installation)
if(!current)throw Error('Current signed policy required for preflight')
const catalogSha256=sha(canonical({base:catalogue.catalogSha256,security:security.sha256})),manifestSha256=migrationManifestDigest()
if(approval.expiresAt<=Date.now()||approval.manifestSha256!==manifestSha256||approval.definitionSha256!==catalogue.definitionSha256||approval.catalogSha256!==catalogSha256)throw Error('Independent preflight approval differs from actual source44 catalogue')
if(approval.expiresAt<=Date.now()||approval.catalogSha256!==catalogSha256)throw Error('Independent preflight approval differs from actual catalogue')
if(canonical(current.policy.source)!==canonical(sourceCoordinates(first))||canonical(current.policy.roles)!==canonical(security.roles)||current.policy.manifestSha256!==manifestSha256||current.policy.definitionSha256!==catalogue.definitionSha256||current.policy.catalogSha256!==catalogSha256)throw Error('Current signed policy differs from actual source/roles/catalogue')
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'))
// 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
@@ -157,7 +176,7 @@ async function preflightCurrentProfile(pool,{deadlineAt=Date.now()+120000}={}){
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)
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){
@@ -173,7 +192,10 @@ async function reconcilePendingPublication(pool,{journalDirectory,deadlineAt=Dat
}
return Object.freeze({
preflightCurrentProfile,
publishCurrentProfile:(pool,options)=>withJournalLease(options.journalDirectory,()=>publishCurrentProfile(pool,options)),
publishCurrentProfile:async(pool,options)=>{
const manifestSha256=requireCurrentCatalogueDefinition(approval,definition)
return withJournalLease(options.journalDirectory,()=>publishCurrentProfile(pool,options,manifestSha256))
},
inspectPublication,
reconcilePendingPublication:(pool,options)=>withJournalLease(options.journalDirectory,()=>reconcilePendingPublication(pool,options)),
})
@@ -0,0 +1,150 @@
/** Deployment operator state. Store outside PGDATA, backups and runtime mounts. */
import assert from 'node:assert/strict'
import {constants} from 'node:fs'
import {lstat,open,rename} from 'node:fs/promises'
import path from 'node:path'
import {z} from 'zod'
import {sourceBindingBytes,sourceBindingCanonical,type DatabaseSourceBindingBody} from '../../src/db/database-source-binding.js'
import {protectedDirectory,readProtected,sha,withJournalLease} from './operator-files.mjs'
import {readProtectedDatabaseSourceBindingRegistry, type createProtectedDatabaseSourceBindingVerifier} from './protected-source-registry.mjs'
const digest=z.string().regex(/^[a-f0-9]{64}$/),uuid=z.string().uuid()
const sourcePin=z.object({registryDirectory:z.string().min(1).max(4096),installationId:uuid,publicKeySpkiSha256:digest,currentHeadSha256:digest}).strict()
const directoryPath=z.string().min(1).max(4096)
const approval=z.object({format:z.literal('database-source-activation-approval-v1'),activationDirectory:directoryPath,sourcePin,tlsSpkiSha256:digest,
expectedActivationSha256:digest.nullable()}).strict()
const generation=z.object({generation:uuid,headSha256:digest,bindingSha256:digest,tlsSpkiSha256:digest}).strict()
const stateSchema=z.object({format:z.literal('database-source-activation-v1'),activationDirectory:directoryPath,installationId:uuid,sourceIdentityId:uuid,
publicKeySpkiSha256:digest,status:z.enum(['active','revoked']),generations:z.array(generation).min(1).max(256)}).strict()
const maximum=131072, filename='source-activation.json'
type State=z.infer<typeof stateSchema>
const parse=(bytes:Buffer):unknown=>JSON.parse(new TextDecoder('utf8',{fatal:true}).decode(bytes))
async function present(file:string){
try{await lstat(file);return true}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT')return false;throw error}
}
// Caller holds the same protected lease used by all activation writers.
async function current(directory:string){
const file=path.join(directory,filename)
assert(!await present(file+'.pending'),'Unresolved activation transition')
if(!await present(file))return undefined
const bytes=await readProtected(file,maximum),state=stateSchema.parse(parse(bytes))
assert.equal(state.activationDirectory,directory,'Activation record belongs to another directory')
assert(sourceBindingBytes(state).equals(bytes),'Noncanonical activation record')
for(const field of ['generation','headSha256','tlsSpkiSha256'] as const){
assert.equal(new Set(state.generations.map(item=>item[field].toLowerCase())).size,state.generations.length,'Reused activation identity')
}
return {state,activationSha256:sha(bytes)}
}
async function persist(directory:string,state:State){
await protectedDirectory(directory)
const bytes=sourceBindingBytes(stateSchema.parse(state))
assert(bytes.length<=maximum)
const file=path.join(directory,filename),pending=file+'.pending'
const parent=await open(directory,constants.O_RDONLY|constants.O_DIRECTORY|constants.O_NOFOLLOW)
let confirmed=false,cleanupUnconfirmed=false
try{
const handle=await open(pending,constants.O_WRONLY|constants.O_CREAT|constants.O_EXCL|constants.O_NOFOLLOW,0o600)
try{await handle.writeFile(bytes);await handle.sync()}finally{await handle.close()}
try{await rename(pending,file);await parent.sync()}
catch{throw Object.assign(Error('Activation terminal requires inspection'),{code:'SOURCE_ACTIVATION_TERMINAL_UNKNOWN'})}
confirmed=true
}finally{try{await parent.close()}catch{cleanupUnconfirmed=confirmed}}
return {activationSha256:sha(bytes),cleanupUnconfirmed}
}
function receipt(state:State,activationSha256:string,outcome:'committed'|'observed'){
return {event:'database-source-activation',outcome,activationSha256,status:state.status,
ordinal:state.generations.length,...state.generations.at(-1),generationAuthenticated:false,readyAuthorized:false,automaticReplay:false}
}
type SourceVerifier=ReturnType<typeof createProtectedDatabaseSourceBindingVerifier>
/** Keep revocation and generation changes fenced throughout one read-only SQL preflight. */
export async function withActiveSourcePreflight<T>(input:{directory:string,expectedSha256:string,sourcePin:z.infer<typeof sourcePin>,expectedSource:DatabaseSourceBindingBody['source'],tlsSpkiSha256:string,deadlineAt:number},
verifier:SourceVerifier,work:(guardedVerifier:SourceVerifier)=>Promise<T>){
const {directory,deadlineAt}=input
const expectedSha256=digest.parse(input.expectedSha256),tlsSpkiSha256=digest.parse(input.tlsSpkiSha256)
const pin=sourcePin.parse(input.sourcePin)
return withJournalLease(directory,async()=>{
async function readActive(){
assert(Date.now()<deadlineAt,'Activation preflight expired')
const active=await current(directory)
assert(active?.state.status==='active'&&active.activationSha256===expectedSha256,'Active activation digest changed')
assert(Date.now()<deadlineAt,'Activation preflight expired')
return active.state
}
const state=await readActive(),generation=state.generations.at(-1)!
const registry=await readProtectedDatabaseSourceBindingRegistry(pin,{deadlineAt})
assert(state.publicKeySpkiSha256===registry.publicKeySpkiSha256&&generation.tlsSpkiSha256===tlsSpkiSha256,'Activation key differs from preflight')
function assertGeneration(evidence:{installationId:string,sourceIdentityId:string,generation:string,ordinal:number,headSha256:string,bindingSha256:string}){
assert(evidence.installationId===state.installationId&&evidence.sourceIdentityId===state.sourceIdentityId&&
evidence.generation===generation.generation&&evidence.ordinal===state.generations.length&&
evidence.headSha256===generation.headSha256&&evidence.bindingSha256===generation.bindingSha256,'Activation differs from signed source generation')
}
assertGeneration({...registry.head,headSha256:registry.headSha256})
assert.equal(sourceBindingCanonical(registry.body.source),sourceBindingCanonical(input.expectedSource),'Activation source differs from reviewed configuration')
await readActive()
let sourceChecks=0
const guardedVerifier:SourceVerifier=Object.freeze({async verify(...args:Parameters<SourceVerifier['verify']>){
await readActive()
const evidence=await verifier.verify(...args)
assert(evidence.platformProtectionVerified&&evidence.operatorSignatureVerified&&evidence.currentHeadPinVerified&&evidence.genuineSourceFactsMatched,'Unprotected activation source evidence')
assertGeneration(evidence)
await readActive()
sourceChecks++
return evidence
}})
const result=await work(guardedVerifier)
assert.equal(sourceChecks,2,'Initial and terminal source evidence required')
await readActive()
return result
})
}
export async function sourceActivationMain(args:string[]){
const command=args[0], flags=command==='source-activate'?['directory','approval','approval-sha256']:
command==='source-revoke'?['directory','expected-sha256']:['directory']
assert(['source-activate','source-revoke','source-status'].includes(command)&&args.length===1+2*flags.length)
const values=new Map<string,string>()
for(let i=1;i<args.length;i+=2){
const name=args[i].slice(2)
assert(args[i].startsWith('--')&&flags.includes(name)&&!values.has(name)&&args[i+1]);values.set(name,args[i+1])
}
const directory=values.get('directory')!
return withJournalLease(directory,async()=>{
const before=await current(directory)
if(command==='source-status'){
assert(before,'Activation record absent')
return receipt(before.state,before.activationSha256,'observed')
}
if(command==='source-revoke'){
const expected=digest.parse(values.get('expected-sha256'))
assert(before?.activationSha256===expected&&before.state.status==='active','Active activation digest changed')
const revoked={...before.state,status:'revoked' as const}
const saved=await persist(directory,revoked)
return {...receipt(revoked,saved.activationSha256,'committed'),cleanupUnconfirmed:saved.cleanupUnconfirmed}
}
const request=approval.parse(parse(await readProtected(values.get('approval')!,16384,digest.parse(values.get('approval-sha256')))))
assert.equal(request.activationDirectory,directory,'Approval belongs to another activation directory')
assert.equal(before?.activationSha256??null,request.expectedActivationSha256,'Activation predecessor changed')
const registry=await readProtectedDatabaseSourceBindingRegistry(request.sourcePin,{deadlineAt:Date.now()+120000})
const previous=before?.state.generations??[]
assert.equal(registry.head.ordinal,previous.length+1,'Source ordinal changed')
assert.equal(registry.head.previousHeadSha256,previous.at(-1)?.headSha256??null,'Signed predecessor differs')
if(before){
assert.equal(registry.head.installationId,before.state.installationId)
assert.equal(registry.head.sourceIdentityId,before.state.sourceIdentityId)
assert.equal(registry.publicKeySpkiSha256,before.state.publicKeySpkiSha256)
}
assert(!previous.some(item=>item.generation.toLowerCase()===registry.head.generation.toLowerCase()||item.tlsSpkiSha256===request.tlsSpkiSha256),'Generation or TLS key was already used')
const next:State={format:'database-source-activation-v1',activationDirectory:directory,installationId:registry.head.installationId,
sourceIdentityId:registry.head.sourceIdentityId,publicKeySpkiSha256:registry.publicKeySpkiSha256,status:'active',
generations:[...previous,{generation:registry.head.generation,headSha256:registry.headSha256,
bindingSha256:registry.head.bindingSha256,tlsSpkiSha256:request.tlsSpkiSha256}]}
const saved=await persist(directory,next)
return {...receipt(next,saved.activationSha256,'committed'),cleanupUnconfirmed:saved.cleanupUnconfirmed}
})
}
@@ -0,0 +1,134 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import os from 'node:os'
import {createHash,generateKeyPairSync,randomUUID} from 'node:crypto'
import {createRequire} from 'node:module'
import {runInNewContext} from 'node:vm'
import {provisionDatabaseSourceBindingOffline} from '../database-source-binding-operator.ts'
import {readDatabaseSourceBindingRegistry,sourceBindingBytes,sourceBindingCanonical,type DatabaseSourceDeploymentPin} from '../../src/db/database-source-binding.ts'
import type * as Activation from './source-activation.mts'
const require=createRequire(import.meta.url),ts=require('typescript')
const source=await fs.readFile(new URL('./source-activation.mts',import.meta.url),'utf8')
const sha=(value:Buffer)=>createHash('sha256').update(value).digest('hex')
test('protected activation transition model preserves revocation and refuses concurrent, stale or reused generations',async()=>{
const root=await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(),'mm-source-activation-test-')))
try{
// Genuine issuer signatures and filesystem files. Windows models the
// protection/lease boundary and directory fsync used on the POSIX target.
const lock=path.join(root,'test-lease'),state=path.join(root,'source-activation.json')
const files={sha,protectedDirectory:async(directory:string)=>{assert.equal(directory,root)},
readProtected:async(filename:string,maximum:number,expected?:string)=>{
assert.equal(path.dirname(filename),root)
const bytes=await fs.readFile(filename);assert(bytes.length<=maximum)
if(expected)assert.equal(sha(bytes),expected)
return bytes
},async withJournalLease(directory:string,work:()=>Promise<unknown>){
assert.equal(directory,root);const handle=await fs.open(lock,'wx')
try{return await work()}finally{await handle.close();await fs.unlink(lock)}
}}
let parentFailure=''
const modelFs={...fs,open:async(filename:string,flags:number,mode?:number)=>filename===root?
{sync:async()=>{if(parentFailure==='sync')throw Error('fixture directory sync failure')},
close:async()=>{if(parentFailure==='close')throw Error('fixture directory close failure')}}:fs.open(filename,flags,mode)}
const imports:Record<string,unknown>={'node:fs/promises':modelFs,'./operator-files.mjs':files,
'./protected-source-registry.mjs':{readProtectedDatabaseSourceBindingRegistry:readDatabaseSourceBindingRegistry},
'../../src/db/database-source-binding.js':{sourceBindingBytes,sourceBindingCanonical}}
const load=()=>{
const exports={}
const compiled=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText
runInNewContext(compiled,{exports,require:(name:string)=>imports[name]??require(name),Buffer,TextDecoder})
return exports as typeof Activation
}
const {withActiveSourcePreflight}=load()
let {sourceActivationMain:run}=load()
const privateKeyFile=path.join(root,'issuer.pem'),key=generateKeyPairSync('ed25519').privateKey
await fs.writeFile(privateKeyFile,key.export({format:'pem',type:'pkcs8'}),{mode:0o600})
const installationId=randomUUID(),sourceIdentityId=randomUUID()
const issue=async(previousPin?:DatabaseSourceDeploymentPin,generation=randomUUID())=>{
return (await provisionDatabaseSourceBindingOffline({privateKeyFile,previousPin,outputDirectory:path.join(root,randomUUID()),
request:{installationId,sourceIdentityId,generation,source:{systemIdentifier:'123',databaseOid:16384,databaseName:'synthetic',
sessionUser:'publisher',currentUser:'publisher',inRecovery:false,serverVersionNum:180004}}})).deploymentPin
}
const status=['source-status','--directory',root]
const revoke=(expected:string)=>['source-revoke','--directory',root,'--expected-sha256',expected]
const activate=async(pin:DatabaseSourceDeploymentPin,tlsSpkiSha256:string,expectedActivationSha256:string|null,activationDirectory=root)=>{
const bytes=sourceBindingBytes({format:'database-source-activation-approval-v1',activationDirectory,sourcePin:pin,tlsSpkiSha256,expectedActivationSha256})
const file=path.join(root,randomUUID()+'.json');await fs.writeFile(file,bytes,{mode:0o600,flag:'wx'})
return ['source-activate','--directory',root,'--approval',file,'--approval-sha256',sha(bytes)]
}
const g1=await issue(undefined,'5ac5e8bd-0eb2-498b-901a-dd2010cd4b31'),g2=await issue(g1),g3=await issue(g2)
await assert.rejects(run(await activate(g1,'1'.repeat(64),null,path.join(root,'other'))),/another activation directory/)
const start=await activate(g1,'1'.repeat(64),null)
const races=await Promise.allSettled([run(start),run(start)])
assert.deepEqual(races.map(value=>value.status).sort(),['fulfilled','rejected'],races.map(value=>value.status==='rejected'?String(value.reason):value.status).join(', '))
const first=await run(status)
assert.equal(first.ordinal,1);assert.equal(first.headSha256,g1.currentHeadSha256)
assert.equal(first.generationAuthenticated,false)
const registry=await readDatabaseSourceBindingRegistry(g1)
const evidence={format:'database-source-binding-evidence-v1' as const,
installationId:registry.head.installationId,sourceIdentityId:registry.head.sourceIdentityId,generation:registry.head.generation,
ordinal:registry.head.ordinal,bindingSha256:registry.head.bindingSha256,headSha256:registry.headSha256,
operatorSignatureVerified:true as const,currentHeadPinVerified:true as const,
genuineSourceFactsMatched:true as const,platformProtectionVerified:true as const,generationAuthenticated:false as const,
allWriterDrainAuthenticated:false as const,readyAuthorized:false as const}
// The actual preflight exercises the checked-out PostgreSQL client. The guard fixture models its source verifier.
type Verifier=Parameters<typeof withActiveSourcePreflight>[1]
const verifierArgs=[] as unknown as Parameters<Verifier['verify']>
const preflight={directory:root,sourcePin:g1,expectedSource:registry.body.source,expectedSha256:first.activationSha256,tlsSpkiSha256:'1'.repeat(64),deadlineAt:Date.now()+30000}
const refuseConnect=()=>{throw Error('Reached SQL connection before activation was trusted')}
const verifier={verify:async()=>evidence}
await assert.rejects(withActiveSourcePreflight({...preflight,expectedSha256:'0'.repeat(64)},verifier,refuseConnect),/activation digest changed/)
await assert.rejects(withActiveSourcePreflight({...preflight,tlsSpkiSha256:'2'.repeat(64)},verifier,refuseConnect),/Activation key differs/)
await assert.rejects(withActiveSourcePreflight({...preflight,sourcePin:g2},verifier,refuseConnect),/differs from signed source generation/)
await assert.rejects(withActiveSourcePreflight({...preflight,expectedSource:{...preflight.expectedSource,currentUser:'different'}},verifier,refuseConnect),/differs from reviewed configuration/)
let verifications=0
const result=await withActiveSourcePreflight(preflight,{verify:async()=>{verifications++;return evidence}},async guarded=>{
assert.equal(await guarded.verify(...verifierArgs),evidence)
await assert.rejects(run(revoke(first.activationSha256)),{code:'EEXIST'})
assert.equal(await guarded.verify(...verifierArgs),evidence)
return 'matched'
})
assert.equal(result,'matched');assert.equal(verifications,2)
await assert.rejects(fs.access(lock),{code:'ENOENT'})
await assert.rejects(withActiveSourcePreflight(preflight,verifier,async guarded=>{
await guarded.verify(...verifierArgs)
}),/Initial and terminal source evidence required/)
let reads=0
await assert.rejects(withActiveSourcePreflight(preflight,{verify:async()=>++reads===1?evidence:{...evidence,bindingSha256:'0'.repeat(64)}},async guarded=>{
await guarded.verify(...verifierArgs)
await guarded.verify(...verifierArgs)
}),/differs from signed source generation/)
await fs.writeFile(state+'.pending','interrupted',{flag:'wx'})
await assert.rejects(withActiveSourcePreflight(preflight,verifier,refuseConnect),/Unresolved activation transition/)
await fs.unlink(state+'.pending')
await assert.rejects(run(start),/predecessor changed/)
const revoked=await run(revoke(first.activationSha256))
await assert.rejects(withActiveSourcePreflight({...preflight,expectedSha256:revoked.activationSha256},verifier,refuseConnect),/activation digest changed/)
run=load().sourceActivationMain
assert.equal((await run(status)).status,'revoked')
await assert.rejects(run(revoke(first.activationSha256)),/digest changed/)
await assert.rejects(run(await activate(g3,'3'.repeat(64),revoked.activationSha256)),/ordinal changed/)
const second=await run(await activate(g2,'2'.repeat(64),revoked.activationSha256))
assert.equal(second.ordinal,2);assert.equal(second.status,'active')
await assert.rejects(run(await activate(g3,'1'.repeat(64),second.activationSha256)),/already used/)
const repeatedGeneration=await issue(g2,first.generation!.toUpperCase())
await assert.rejects(run(await activate(repeatedGeneration,'3'.repeat(64),second.activationSha256)),/already used/)
await assert.rejects(run(await activate({...g3,currentHeadSha256:'0'.repeat(64)},'3'.repeat(64),second.activationSha256)))
assert.equal((await run(status)).activationSha256,second.activationSha256)
parentFailure='sync'
await assert.rejects(run(revoke(second.activationSha256)),{code:'SOURCE_ACTIVATION_TERMINAL_UNKNOWN'})
parentFailure='close'
const observed=await run(status)
assert.equal(observed.status,'revoked')
const third=await run(await activate(g3,'3'.repeat(64),observed.activationSha256))
assert.equal(third.outcome,'committed')
assert('cleanupUnconfirmed' in third && third.cleanupUnconfirmed===true)
await fs.writeFile(state+'.pending','interrupted',{flag:'wx'})
await assert.rejects(run(status),/Unresolved activation transition/)
await assert.rejects(run(revoke(third.activationSha256)),/Unresolved activation transition/)
}finally{await fs.rm(root,{recursive:true,force:true})}
})
+5
View File
@@ -3,10 +3,14 @@ import { applySqlMigrations } from "./migrate.js"
import { dropExpiredPartitions, ensurePartitionsAround } from "./partitions.js"
import { importSqliteToPostgres, shouldImportSqlite, sqliteFileLooksPresent } from "./sqlite-import.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
export async function initDatabase(): Promise<void> {
await assertDatabaseMaintenanceStartup(mcpStateDirectory(), env.DATABASE_URL)
await pool.query("SELECT 1")
await applySqlMigrations(pool)
await ensurePartitionsAround(pool)
@@ -39,4 +43,5 @@ export async function initDatabase(): Promise<void> {
}
await dropExpiredPartitions(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"
return () => analyzeMigration(file, bytes, { file, sha256: createHash("sha256").update(bytes).digest("hex"), transaction: "owner-transaction", selfMarker: false, opaqueBodies: 0, bodyReview: [], effects: ["Explicit inert fixture review"], ...extra })
}
await test("all44 exact file pins and every top-level schema operation are inventoried", () => {
assert.equal(inventory.length, 44); assert.equal(MIGRATION_MANIFEST.length, 44)
await test("all 56 exact file pins and every top-level schema operation are inventoried", () => {
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 === "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))
let tables = new Set<string>()
for (const file of inventory) {
@@ -32,8 +32,8 @@ await test("DO BEGIN and stored function BEGIN are never transaction boundaries"
assert.equal(endpoint.topLevelTransactionControls.length, 2); assert.equal(endpoint.opaqueBodies.length, 2)
assert.deepEqual(endpoint.opaqueBodies.map(body => body.kind), ["stored-function-body", "executed-do-body"])
})
await test("0035 through0044 have exact atomic file-owned terminal markers", () => {
assert.deepEqual(inventory.filter(file => file.selfMarker).map(file => file.id), ["0035_server_probe_journal", "0036_diagnostic_cancellations", "0037_server_probe_storage_v2", "0038_server_poll_journal_and_writer_guards", "0039_server_poll_storage", "0040_mcp_routing_reservations", "0041_server_poll_terminal_archives", "0042_server_poll_settings_epoch", "0043_mcp_routing_forward_charges", "0044_mcp_routing_manual_inverse_charges"])
await test("0035 through 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", "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) }
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_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))
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.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)
@@ -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"))
})
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`)
+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.",
"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.
// This is a necessary presence check, not a substitute for exact live column,
// constraint/index/function/trigger comparison by the control protocol.
export const CURRENT_REQUIRED_TABLES: readonly string[] = Object.freeze(["acme_settings","alert_engine_confirm_pending","alert_engine_cursor","alert_engine_prev_live","alert_engine_state","alert_groups","alert_history","alert_outbox","alert_rule_conditions","alert_rule_targets","alert_rules","alert_telegram_settings","app_users","backup_entries","backup_schedule_settings","backup_storage_settings","certificate_issue_jobs","certificate_renew_settings","config_revisions","data_migration","events","evobgp_settings","filter_rules","flow_asn_meta","flow_buckets","flow_daily_dims","flow_daily_facts","flow_hour_facts","flow_ip_meta","flow_minute_dims","flow_minute_stats","flow_settings_intents","flow_write_receipts","geoip_settings","internet_path_settings","internet_path_snapshots","mcp_artifacts","mcp_audit","mcp_budget_buckets","mcp_connections","mcp_credentials","mcp_diagnostic_cancellations","mcp_drift","mcp_grants","mcp_management_channels","mcp_oauth_clients","mcp_oauth_requests","mcp_operation_checkpoints","mcp_operation_steps","mcp_operations","mcp_owner_revocations","mcp_path_checks","mcp_plans","mcp_portal_events","mcp_protections","mcp_read_leases","mcp_recovery_intents","mcp_requests","mcp_routing_forward_charges","mcp_routing_manual_inverse_charges","mcp_routing_observations","mcp_routing_profiles","mcp_routing_reservation_slots","mcp_routing_reservations","mcp_scoped_recovery_intents","mcp_secrets","mcp_settings","mcp_unauthorized_buckets","recursive_routes","router_log_collection_state","router_log_entries","router_log_settings","scheduler_runs","schema_migrations","server_poll_acks","server_poll_archives","server_poll_executions","server_poll_intents","server_poll_quarantines","server_poll_receipts","server_poll_storage_admissions","server_poll_storage_usage","server_probe_execution_payloads","server_probe_execution_quarantines","server_probe_execution_stages","server_probe_executions","server_probe_storage_admissions","server_probe_storage_usage","server_snapshots","servers","servers_api_ping_settings","servers_rest_ping_samples","traffic_collection_state","traffic_flow_settings","traffic_samples","traffic_settings","uptime_probe_samples","uptime_probes","uptime_resource_samples","uptime_settings","uptime_speed_probes","uptime_speed_test_runs","user_interface_bindings","vrf_production_tickets"])
export const CURRENT_REQUIRED_TABLES: readonly string[] = Object.freeze(["acme_settings","alert_engine_confirm_pending","alert_engine_cursor","alert_engine_prev_live","alert_engine_state","alert_groups","alert_history","alert_outbox","alert_rule_conditions","alert_rule_targets","alert_rules","alert_telegram_settings","app_users","backup_entries","backup_schedule_settings","backup_storage_settings","certificate_issue_jobs","certificate_renew_settings","config_revisions","data_migration","events","evobgp_settings","filter_rules","flow_asn_meta","flow_buckets","flow_daily_dims","flow_daily_facts","flow_hour_facts","flow_ip_meta","flow_minute_dims","flow_minute_stats","flow_settings_intents","flow_write_receipts","geoip_settings","internet_path_settings","internet_path_snapshots","mcp_artifacts","mcp_audit","mcp_budget_buckets","mcp_connections","mcp_credentials","mcp_diagnostic_cancellations","mcp_drift","mcp_grants","mcp_management_channels","mcp_oauth_clients","mcp_oauth_requests","mcp_operation_checkpoints","mcp_operation_replay_tombstones","mcp_operation_steps","mcp_operations","mcp_ordinary_retirement_claims","mcp_owner_revocations","mcp_path_checks","mcp_plans","mcp_portal_events","mcp_protections","mcp_read_leases","mcp_recovery_intents","mcp_requests","mcp_routing_forward_charges","mcp_routing_manual_inverse_charges","mcp_routing_observations","mcp_routing_profiles","mcp_routing_reservation_slots","mcp_routing_reservations","mcp_scoped_recovery_intents","mcp_secrets","mcp_settings","mcp_unauthorized_buckets","recursive_routes","router_log_collection_state","router_log_entries","router_log_settings","scheduler_runs","schema_migrations","server_poll_acks","server_poll_archives","server_poll_executions","server_poll_intents","server_poll_quarantines","server_poll_receipts","server_poll_retirement_claims","server_poll_storage_admissions","server_poll_storage_usage","server_probe_execution_payloads","server_probe_execution_quarantines","server_probe_execution_stages","server_probe_executions","server_probe_retired_sources","server_probe_retirement_claims","server_probe_storage_admissions","server_probe_storage_usage","server_probe_terminal_proofs","server_snapshots","servers","servers_api_ping_settings","servers_rest_ping_samples","traffic_collection_state","traffic_flow_settings","traffic_samples","traffic_settings","uptime_probe_samples","uptime_probes","uptime_resource_samples","uptime_settings","uptime_speed_probes","uptime_speed_test_runs","user_interface_bindings","vrf_production_tickets"])
export interface LegacyBaselineEvidence {
inventoryComplete: boolean; databaseGeneration: string; unresolvedAttemptIds: readonly string[]
@@ -53,7 +53,7 @@ try {
assert(baseline.sections.constraints.length > 100)
assert(baseline.sections.functions.length > 0)
assert.deepEqual(compareMigrationSchemas(baseline, await capture(reference.connection)), { equal: true, evidenceOnly: true, enrollmentAuthorized: false, sha256: baseline.sha256 })
cases.push("all36 pinned migrations produce repeatable complete PG18 catalog evidence on an actual checked-out read-only backend")
cases.push(`all ${inventory.length} pinned migrations produce repeatable complete PG18 catalog evidence on an actual checked-out read-only backend`)
} finally { await reference.connection.end() }
const independent = await database("independent")
try {
+4
View File
@@ -4,6 +4,8 @@ import { serializerCompiler, validatorCompiler } from "@fastify/type-provider-zo
import { monitorEventLoopDelay } from "node:perf_hooks"
import { env } from "./config.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 authPlugin, { requireApiAuth } from "./plugins/auth.js"
import { installDatabaseRequestGate } from "./plugins/database-request-gate.js"
@@ -186,6 +188,8 @@ const isMain =
if (isMain) {
try {
// Resolve external maintenance before init/migrations or consumer startup.
await assertDatabaseMaintenanceStartup(mcpStateDirectory(), env.DATABASE_URL)
await initDatabase()
const app = await buildApp()
let shuttingDown = false
+3 -3
View File
@@ -5,7 +5,7 @@ import type { ConnectionRow, McpPrincipal } from "./store.js"
import type { RosSnapshot, RosStep } from "./resources.js"
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. */
persistentRoutingProfile?: { id: string; version: number }
/** Available only during target resolution, never retained by the read/prepare handler. */
@@ -26,11 +26,11 @@ export interface ChangeEvidence {
requiresHuman?: boolean;
verificationScope?: "configuration" | "acknowledgement";
validUntil?: string;
observationChecks?: Array<{ id: string; srcServerId: number; dstServerId: number; srcInterface: string; family: string; table: string; fingerprint: string; validUntil: string }>;
observationChecks?: Array<{ id: string; srcServerId: number; dstServerId: number; srcInterface: string; dstInterface?: string; metric?: "pingAt" | "bandwidthAt"; family: string; table: string; fingerprint: string; validUntil: string }>;
ospfOwnerGraph?: { family: "ipv4" | "ipv6"; sourceSecretProofs: Array<{ serverId: number; proof: string }>; instances: Array<{ serverId: number; fingerprint: string }> };
managementChannels?: Array<{ channelId: string; serverId: number; expiresAt: string }>;
recovery?: { step: ChangeStep; expected: unknown; validUntil: string; authorityProfileId: string };
healthChecks?: Array<{ serverId: number; resource: string; id: string; field: "active" | "resolved" | "running" | "state" | "bfd-up"; phase?: "after"; identity?: Record<string, string> }>;
healthChecks?: Array<{ serverId: number; resource: string; id: string; field: "active" | "resolved" | "running" | "state" | "bfd-up"; phase?: "after"; identity?: Record<string, string>; nextHopInterface?: { name: string; id: string } }>;
warnings: string[];
}
export interface McpAction {
+104 -21
View File
@@ -9,14 +9,17 @@ import { registerIpsecExistingCertificateExportActions } from "./ipsec-existing-
import { registerIpsecUserUpdateActions } from "./ipsec-user-update-actions.js"
import { prepareIpsecNativeWrite } from "./ipsec-native-write-actions.js"
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 { logFiltersSchema, logSettingsPatchSchema } from "@mmapp/contracts"
import { appUserCreateSchema, appUserUpdateSchema, appUserRoleSchema, sectionPermSchema, serverPermSchema, userBindingCreateSchema } from "@mmapp/contracts/users"
import { appUserCreateSchema, appUserUpdateSchema, appUserRoleSchema, interfaceCatalogStateSchema, sectionPermSchema, serverPermSchema, userBindingCreateSchema } from "@mmapp/contracts/users"
import type { McpOperationDescriptor } from "@mmapp/contracts/mcp"
import { hasPermission } from "../../lib/permissions.js"
import { assertOperationReplayScope, findOperationReplayTombstone, operationHistoryExpired } from "./operation-replay-tombstones.js"
import { pool } from "../../db/index.js"
import { getServerReadById } from "../servers/service/servers-service.js"
import { listUsers, getUserById, listInterfaceCatalog, getInterfaceCatalogState } from "../users/service/users-service.js"
import { listUsers, getUserById, getInterfaceCatalogState, projectLegacyInterfaceCatalog, validateUserBindingCreate, UsersServiceError } from "../users/service/users-service.js"
import { getLogSettings } from "../../services/router-logs-collector.js"
import { readLogPage, readLogSummary, readLogContext, readLogEntry } from "../../services/router-logs-query.js"
import { logWhere } from "../../services/router-logs-model.js"
@@ -28,7 +31,7 @@ import { readLogStream } from "../../services/router-logs-query.js"
import { subscribeLogDevice } from "../../services/router-logs-collector.js"
import { rosResources, readRosSnapshot, resourceById, routerClient, validateRosStep, splitOspfPassiveUpdate } from "./resources.js"
import { registerMcpOperation, listMcpOperations } from "./registry.js"
import { accessGeneration, decryptSecret, encryptSecret, fingerprint, McpError, redact } from "./security.js"
import { accessGeneration, decryptSecret, encryptSecret, fingerprint, MAX_MCP_PUBLIC_ARRAY_ITEMS, MAX_MCP_PUBLIC_STRING_LENGTH, McpError, redact } from "./security.js"
import { validatePrincipal } from "./store.js"
import type { McpAction, ActionContext, ChangeEvidence } from "./action-types.js"
import { registerRoutingActions } from "./routing-actions.js"
@@ -76,7 +79,7 @@ import { prepareGreNativeChange, registerGreNativeServices } from "./gre-native-
import { registerVpnCatalogActions } from "./vpn-catalog-actions.js"
import { registerEvoBgpActions } from "./evobgp-actions.js"
import { registerRoutingProfileActions } from "./routing-profiles.js"
import { registerRoutingControls } from "./routing-controls.js"
import { readRpfDependencies, registerRoutingControls } from "./routing-controls.js"
import { registerLifecycleActions } from "./lifecycle-actions.js"
import { registerRecoveryActions } from "./recovery-actions.js"
import { registerPathActions } from "./path-verification.js"
@@ -103,6 +106,7 @@ import { registerRecursiveRecovery } from "./recursive-recovery.js"
import { registerNetworkHostActions } from "./network-host-actions.js"
import { registerBgpFilterBindingActions } from "./bgp-filter-binding-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 rowId = z.string().trim().min(1).max(200)
@@ -126,6 +130,7 @@ registerRecursiveRecovery(add)
registerNetworkHostActions(add)
registerBgpFilterBindingActions(add)
registerBgpCommunityPolicyActions(add)
registerBridgeVlanActions(add)
registerTrafficHistoryActions(add)
registerSidebarCountsActions(add)
registerLogArtifactActions(add)
@@ -151,9 +156,9 @@ for (const resource of rosResources) {
? z.object({ serverId, fields: resource.fields.default({}), ...(resource.id === "vrf" ? { productionAdmission: z.literal(true).optional() } : {}) }).strict()
: z.object({ serverId, id: rowId.optional(), fields: resource.fields.default({}), ...(resource.id === "vrf" ? { productionAdmission: z.literal(true).optional() } : {}) }).strict()
const delegated = resource.id === "wireguard" && action !== "create" || resource.id === "wireguard_peer" || resource.id === "gre" && action !== "create" || resource.id === "ipsec_peer" && action !== "create" || resource.id === "certificate" && action === "delete" || resource.id === "container" && action === "delete"
const previousVersion = action === "create" ? (["ospf_template", "wireguard_peer", "wireguard"].includes(resource.id) ? 3 : 2) : resource.id === "ipsec_peer" || resource.id === "certificate" && action === "delete" ? 2 : resource.id === "wireguard" && action === "update" ? 3 : resource.id === "ospf_template" && action !== "delete" || resource.id === "wireguard_peer" || resource.id === "wireguard" ? 2 : 1
const previousVersion = action === "create" ? (["ospf_template", "wireguard_peer", "wireguard"].includes(resource.id) ? 3 : 2) : resource.id === "ipsec_peer" || resource.id === "certificate" && action === "delete" ? 2 : resource.id === "wireguard" && action === "update" ? 3 : resource.id === "ospf_template" && action !== "delete" || resource.id === "wireguard_peer" || resource.id === "wireguard" || resource.id === "ip_settings" ? 2 : 1
add(`ros.${resource.id}.${action}`, `mm_${resource.id}_${action}_plan`, `${resource.title}: ${action}`, resource.permission.split(":")[1], resource.id === "certificate" && action === "update" ? ["mm:certificates:write", "mm:network:write"] : resource.permission + ":write", "write", "servers", schema, {
version: resource.id === "ipsec_peer" ? (action === "create" ? 5 : 3) : resource.id === "certificate" ? (action === "update" ? 4 : 3) : previousVersion + (delegated ? 0 : 1) + (resource.id === "ospf_template" ? 1 : 0) + (resource.id === "vrf" ? 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) => {
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
@@ -190,6 +195,8 @@ for (const resource of rosResources) {
const references: ChangeEvidence["objects"] = [{ kind: resource.id, id: id ?? String(fields.name ?? "new"), serverId: sid }]
if (resource.id.endsWith("settings")) references.push({ kind: resource.id, id: "singleton", serverId: sid })
const snapshots = [before]
const rpf = resource.id === "ip_settings" && fields["rp-filter"] ? await readRpfDependencies(sid, fields["rp-filter"], before.sourceProof) : undefined
if (rpf) snapshots.push(...rpf.snapshots)
if (["ospf_instance", "ospf_area", "ospf_template", "bfd_configuration", "bgp_instance", "bgp_connection", "vrf"].includes(resource.id)) await addRoutingDependencies(sid, resource.id, action, current, fields, snapshots, references)
if (["ipv4_route", "ipv6_route", "routing_rule", "bgp_connection"].includes(resource.id)) {
const tables = await readRosSnapshot(sid, "routing_table")
@@ -249,13 +256,24 @@ for (const resource of rosResources) {
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
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,
...(resource.id === "vrf" && input.productionAdmission === true ? { vrfProductionAdmission: true as const } : {}),
steps: splitOspfPassiveUpdate({ serverId: sid, resource: resource.id, action, id, fields, ...(typeof placeBeforeId === "string" ? { placeBeforeId } : {}) }).map(step => ({ kind: "ros" as const, step })),
requiresHuman: true,
warnings: ["Нативное изменение может затронуть управление устройством и требует подтверждения человеком.", "Автоматическое каскадное изменение зависимостей не выполняется.", ...(placeBeforeId ? [`VRF создаётся перед записью ${placeBeforeId}; порядок определяет принадлежность интерфейсов.`] : [])],
warnings: ["Нативное изменение может затронуть управление устройством и требует подтверждения человеком.", "Автоматическое каскадное изменение зависимостей не выполняется.", ...(rpf?.warnings ?? []), ...(resource.id === "vrf" && action === "delete" ? ["При удалении VRF RouterOS может удалить автоматический IPv6 link-local и его connected-маршрут пустого bridge; статические адреса сохраняются."] : []), ...(placeBeforeId ? [`VRF создаётся перед записью ${placeBeforeId}; порядок определяет принадлежность интерфейсов.`] : [])],
}
},
})
@@ -267,7 +285,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"] },
] 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(), {
version: definition.id === "interfaces.update" ? 4 : 2,
version: definition.id === "interfaces.update" ? 5 : 2,
prepare: async (input, context) => {
const resource = resourceById(String(input.resource)), fields = resource.fields.safeParse(input.fields)
if (!fields.success) throw new McpError("INVALID_INPUT", "Поля не соответствуют типизированной схеме выбранного объекта", 400)
@@ -300,11 +318,33 @@ add("users.list", "mm_users_list", "Пользователи", "users", "mm:user
add("users.get", "mm_users_get", "Пользователь по ID", "users", "mm:users:read", "read", "global", z.object({ id: rowId }).strict(), {
read: async (input, context) => ({ user: scopedUser(await getUserById(String(input.id)), context) }),
})
const invalidInterfaceCatalog = () => new McpError("STATE_UNAVAILABLE", "Не подтверждён контекст каталога интерфейсов.", 502)
async function readMcpInterfaceCatalog(serverId: number) {
const state = await getInterfaceCatalogState(serverId).catch(error => {
if (error instanceof UsersServiceError) {
if (error.status === 404) throw new McpError("NOT_FOUND", "Сервер не найден", 404)
if (error.status === 502) throw invalidInterfaceCatalog()
}
throw error
})
const parsed = interfaceCatalogStateSchema.safeParse(state)
if (!parsed.success || parsed.data.serverId !== serverId) throw invalidInterfaceCatalog()
return parsed.data
}
function catalogueExceedsPublicLimits(value: unknown): boolean {
if (typeof value === "string") return value.length > MAX_MCP_PUBLIC_STRING_LENGTH
if (Array.isArray(value)) return value.length > MAX_MCP_PUBLIC_ARRAY_ITEMS || value.some(catalogueExceedsPublicLimits)
return value !== null && typeof value === "object" && Object.values(value).some(catalogueExceedsPublicLimits)
}
function intactInterfaceCatalog<T>(result: T): T {
if (catalogueExceedsPublicLimits(result)) throw new McpError("BUDGET_EXCEEDED", "Каталог интерфейсов превышает предел ответа MCP.", 413)
return result
}
add("users.interfaces.read", "mm_user_interfaces_list", "Каталог интерфейсов для привязок пользователей", "users", "mm:users:read", "read", "servers", z.object({ serverId }).strict(), {
read: async input => ({ interfaces: await listInterfaceCatalog(Number(input.serverId)) }),
read: async input => intactInterfaceCatalog({ interfaces: projectLegacyInterfaceCatalog(await readMcpInterfaceCatalog(Number(input.serverId))) }),
})
add("users.interfaces.state", "mm_user_interfaces_state_get", "Состояние каталога интерфейсов для привязок", "users", "mm:users:read", "read", "servers", z.object({ serverId }).strict(), {
read: async input => getInterfaceCatalogState(Number(input.serverId)),
read: async input => intactInterfaceCatalog(await readMcpInterfaceCatalog(Number(input.serverId))),
})
for (const action of ["create", "update", "delete", "binding_add", "binding_remove"] as const) {
const schema = action === "create" ? z.object({ user: appUserCreateSchema.omit({ role: true, sections: true, servers: true }).strict() }).strict()
@@ -321,6 +361,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] : []),
...(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 (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,
steps: [{ kind: "service", service: `users.${action}`, input }], warnings: ["Изменение /users не меняет JWT-права портала."] }
},
@@ -349,16 +404,17 @@ function logFilters(input: Record<string, unknown>, context: ActionContext) {
}
add("logs.query", "mm_logs_query", "История логов", "logs", "mm:servers:read", "read", "servers", logInput, {
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
if (input.cursor) {
try {
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")
cursor = state.cursor; watermark = state.watermark
} catch { throw new McpError("RESET_REQUIRED", "Курсор устарел или относится к другим фильтрам", 409) }
}
} else filters = logFilters(input, context)
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 }
},
@@ -373,30 +429,41 @@ for (const context of [false, true]) add(context ? "logs.context" : "logs.get",
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) => {
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
assertActive()
const release = subscribeLogDevice(Number(input.serverId)), until = Date.now() + Number(input.waitSeconds) * 1000
try {
const filters = logFilters(input, context), generation = await accessGeneration()
const generation = await accessGeneration()
let filters: ReturnType<typeof logFilters>
let cursor = "0"
if (input.cursor) {
try {
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")
cursor = saved.cursor
} catch { throw new McpError("RESET_REQUIRED", "Курсор follow устарел или относится к другим фильтрам", 409) }
} 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
}
while (true) {
assertActive()
const current = await validatePrincipal(context.principal)
assertActive()
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
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 })) }
await new Promise(resolve => setTimeout(resolve, 1000))
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 sleep(1000, undefined, { signal: waitSignal })
}
} finally { release() }
},
@@ -422,7 +489,18 @@ add("logs.settings.update", "mm_logs_settings_update_plan", "Изменение
prepare: async (input, context) => {
const ids = (await pool.query<{ id: number }>("SELECT id FROM servers")).rows.map(row => Number(row.id))
if (ids.some(id => !context.connection.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Глобальная настройка сборщика затрагивает скрытые серверы")
return { target: { scope: "global" }, objects: ids.map(serverId => ({ kind: "server", id: String(serverId), serverId })), snapshots: [], before: await getLogSettings(), steps: [{ kind: "service", service: "logs.settings.update", input }], warnings: ["Отключение сбора и уменьшение retention влияют на последующую доступность истории."] }
const before = await getLogSettings()
const settings = logSettingsPatchSchema.parse(input.settings)
const warnings: string[] = []
if (before.enabled && settings.enabled === false) warnings.push("После применения сборщик прекратит получать новые события.")
if (settings.retentionDays !== undefined && settings.retentionDays < before.retentionDays) {
warnings.push(`Срок хранения сокращается с ${before.retentionDays} до ${settings.retentionDays} суток. При последующей очистке удаляются записи, полученные более ${settings.retentionDays} суток назад.`)
}
return {
target: { scope: "global" },
objects: ids.map(serverId => ({ kind: "server", id: String(serverId), serverId })),
snapshots: [], before, steps: [{ kind: "service", service: "logs.settings.update", input }], warnings,
}
},
})
@@ -473,7 +551,12 @@ add("changes.cancel", "mm_change_cancel", "Отменить свой незап
add("operations.read", "mm_operation_get", "Результат своей операции", "mcp", null, "read", "connection", z.object({ operationId: z.string().uuid() }).strict(), {
read: async (input, context) => {
const { rows: [operation] } = await pool.query("SELECT * FROM mcp_operations WHERE id=$1 AND connection_id=$2", [input.operationId, context.connection.id])
if (!operation) throw new McpError("NOT_FOUND", "Операция не найдена", 404)
if (!operation) {
const tombstone = await findOperationReplayTombstone(pool, context.connection.id, String(input.operationId))
if (!tombstone) throw new McpError("NOT_FOUND", "Операция не найдена", 404)
assertOperationReplayScope(tombstone, context.connection)
operationHistoryExpired()
}
await assertPlanReadScope(await (await import("./runtime.js")).loadPlan(operation.plan_id), context)
const { rows: steps } = await pool.query("SELECT * FROM mcp_operation_steps WHERE operation_id=$1 ORDER BY step_index", [input.operationId])
return { ...operation, steps }
@@ -17,6 +17,7 @@ try{
await pool.query("INSERT INTO backup_storage_settings(id) VALUES(1) ON CONFLICT(id) DO NOTHING")
await updateMcpSettings({enabled:true},actor)
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("../../test/mcp-acme-test.js")).verifyMcpAcmeTest(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 { nativeBgpRows, type NativeBgpRow } from "../../services/bgp-export-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]
}
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[] = []
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))
@@ -1,5 +1,5 @@
import assert from "node:assert/strict"
import { randomUUID, createHmac } from "node:crypto"
import { randomUUID, createHash, createHmac } from "node:crypto"
import { readFile, writeFile } from "node:fs/promises"
import path from "node:path"
import type { PoolClient } from "pg"
@@ -8,7 +8,7 @@ import { pool, closePool } from "../../db/index.js"
import { mcpConnectionCreateSchema } from "@mmapp/contracts/mcp"
import { type AuthUser } from "../../lib/permissions.js"
import { accessGeneration, mcpStateDirectory } from "./security.js"
import { createMcpConnection, authenticateMcpToken, updateMcpSettings } from "./store.js"
import { createMcpConnection, authenticateMcpToken, validatePrincipal, updateMcpSettings } from "./store.js"
import { invokeMcpTool, approvePlan, applyPlan } from "./runtime.js"
import { uploadRestoreArchive, readRestoreReceipt, executeMcpDatabaseRestore } from "./database-restore.js"
import { exportSystemDatabaseBackup, restoreSystemDatabaseBackup } from "../../services/system-database-backup.js"
@@ -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
const originalConnect = pool.connect
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 {
// Every owned restore profile archives inert domain collectors, not only the
// data-equivalence profile. Otherwise restart can launch unrelated downloads
@@ -42,25 +51,39 @@ try {
await updateMcpSettings({ enabled: true }, actor)
await pool.query("CREATE TABLE mcp_restore_acceptance_marker (value text NOT NULL)")
await pool.query("INSERT INTO mcp_restore_acceptance_marker VALUES('before')")
const dump = await exportSystemDatabaseBackup()
assert.equal(dump.buffer.subarray(0, 5).toString(), "PGDMP")
if (verifyData) dataExpectation = await (await import("./database-restore-data-equivalence.sql-helper.js")).prepareOwnedRestoreDataExpectation(dump.buffer)
const revokedOwner = { id: "restore-revocation-after-backup", issuedAt: Date.now() - 60000 }
const portalSecret = "restore-acceptance-only-portal-secret-32", stamp = String(Math.floor(Date.now() / 1000))
const portalEvent = Buffer.from(JSON.stringify({ eventId: randomUUID(), userId: revokedOwner.id, type: "permissions.changed" }))
await acceptPortalRevocation(portalEvent, stamp, "sha256=" + createHmac("sha256", portalSecret).update(stamp + ".").update(portalEvent).digest("hex"), portalSecret)
const source = await uploadRestoreArchive(dump.buffer, actor)
const encrypted = await readFile(path.join(mcpStateDirectory(), "restore-vault", source.archiveRef + ".archive.bin"))
assert.ok(!encrypted.equals(dump.buffer))
assert.ok(!encrypted.subarray(0, 5).equals(Buffer.from("PGDMP")))
await assert.rejects(uploadRestoreArchive(dump.buffer, { ...actor, permissions: ["mm:mcp:admin"] }))
await assert.rejects(uploadRestoreArchive(Buffer.from("invalid"), actor))
const serverIds = (await pool.query("SELECT id FROM servers")).rows.map(row => Number(row.id))
// Combined owned profiles retain their evidence/disabled synthetic servers.
// The human restore fixture grants this exact catalog instead of inheriting
// the ordinary five-server default; production policy guards stay unchanged.
assert(serverIds.length <= 1000)
const issue = (owner = actor) => createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Database restore acceptance", mode: "approval", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:settings:admin"], scope: { allowGlobal: true, serverIds, operationIds: ["database.restore"] }, limits: { serversPerPlan: Math.max(1, serverIds.length) } }), owner)
const cutoff = Date.now() - 1000
const revokedOwner = { ...actor, id: "restore-revocation-after-backup", issuedAt: cutoff - 1 }
const oldAccess = await issue(revokedOwner), oldPrincipal = await authenticateMcpToken(oldAccess.token)
const readHistoricalAccess = async () => (await pool.query<{ status: string; credential_current: boolean; generation: string }>(
"SELECT c.policy->>'status' AS status, k.revoked_at IS NULL AND k.expires_at>now() AS credential_current, c.generation FROM mcp_connections c JOIN mcp_credentials k ON k.connection_id=c.id WHERE c.id=$1 AND k.id=$2",
[oldPrincipal.connectionId, oldPrincipal.credentialId],
)).rows[0]
const beforeDumpAccess = await readHistoricalAccess()
assert.deepEqual(beforeDumpAccess, { status: "active", credential_current: true, generation: await accessGeneration() })
const dump = await exportSystemDatabaseBackup()
assert.equal(dump.buffer.subarray(0, 5).toString(), "PGDMP")
if (verifyData) dataExpectation = await (await import("./database-restore-data-equivalence.sql-helper.js")).prepareOwnedRestoreDataExpectation(dump.buffer)
const portalSecret = "restore-acceptance-only-portal-secret-32", stamp = String(Math.floor(Date.now() / 1000))
const eventId = randomUUID()
const portalEvent = Buffer.from(JSON.stringify({ version: 2, eventId, userId: revokedOwner.id, type: "permissions.changed", eventOccurredAt: cutoff }))
assert.deepEqual(await acceptPortalRevocation(portalEvent, stamp, "sha256=" + createHmac("sha256", portalSecret).update(stamp + ".").update(portalEvent).digest("hex"), portalSecret), { ok: true, duplicate: false })
const cutoffFile = path.join(mcpStateDirectory(), "owner-revocations", createHash("sha256").update(revokedOwner.id).digest("hex") + ".json")
const cutoffBytes = await readFile(cutoffFile)
assert.deepEqual(JSON.parse(cutoffBytes.toString("utf8")), { invalidBefore: cutoff })
assert.deepEqual(await readHistoricalAccess(), { ...beforeDumpAccess, status: "revoked", credential_current: false })
await assert.rejects(validatePrincipal(oldPrincipal), { code: "OWNER_CLAIMS_REVOKED" })
const source = await uploadRestoreArchive(dump.buffer, actor)
const encrypted = await readFile(path.join(mcpStateDirectory(), "restore-vault", source.archiveRef + ".archive.bin"))
assert.ok(!encrypted.equals(dump.buffer))
assert.ok(!encrypted.subarray(0, 5).equals(Buffer.from("PGDMP")))
await assert.rejects(uploadRestoreArchive(dump.buffer, { ...actor, permissions: ["mm:mcp:admin"] }))
await assert.rejects(uploadRestoreArchive(Buffer.from("invalid"), actor))
const issued = await issue(), principal = await authenticateMcpToken(issued.token)
const other = await issue({ ...actor, id: "another-owner" }), otherPrincipal = await authenticateMcpToken(other.token)
await assert.rejects(invokeMcpTool(otherPrincipal, "mm_database_restore_plan", { archiveRef: source.archiveRef, expectedSha256: source.sha256 }))
@@ -135,9 +158,26 @@ try {
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_operations WHERE id=$1", [operation.id])).rows[0].n, 0)
assert.notEqual(await accessGeneration(), generation)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_owner_revocations WHERE owner_id=$1", [revokedOwner.id])).rows[0].n, 0)
await assert.rejects(assertPortalClaimsCurrent(revokedOwner))
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_portal_events WHERE id=$1", [eventId])).rows[0].n, 0)
assert.deepEqual(await readHistoricalAccess(), beforeDumpAccess)
assert.deepEqual(await readFile(cutoffFile), cutoffBytes)
// Both paths must report the external owner cutoff, independently of the
// blanket generation change that also invalidates older connections.
await assert.rejects(assertPortalClaimsCurrent(revokedOwner), { code: "OWNER_CLAIMS_REVOKED" })
await assert.rejects(authenticateMcpToken(oldAccess.token), { code: "OWNER_CLAIMS_REVOKED" })
await assert.rejects(validatePrincipal(oldPrincipal), { code: "OWNER_CLAIMS_REVOKED" })
await assert.rejects(authenticateMcpToken(issued.token))
await assert.rejects(readRestoreReceipt(operation.id, { ...actor, permissions: ["mm:mcp:read"] }))
// The applied receipt is durable before the terminal callback releases its
// last SQL lease/exclusive admission. Await that existing owner, rather than
// racing its cleanup with the stale-callback/no-replay assertion below.
await waitForDatabaseRetirement()
await assert.rejects(issue(revokedOwner), { code: "OWNER_CLAIMS_REVOKED" })
const refreshedOwnerAccess = await issue({ ...revokedOwner, issuedAt: cutoff + 1 })
const refreshedOwner = await validatePrincipal(await authenticateMcpToken(refreshedOwnerAccess.token))
assert.equal(refreshedOwner.owner_id, revokedOwner.id)
assert.equal(refreshedOwner.generation, await accessGeneration())
assert.notEqual(refreshedOwner.generation, beforeDumpAccess.generation)
// A stale callback or process replay cannot invoke pg_restore a second time.
await pool.query("UPDATE mcp_restore_acceptance_marker SET value='after-receipt'")
await executeMcpDatabaseRestore(operation.id, principal)
@@ -146,7 +186,7 @@ try {
if (dataExpectation) {
console.log("Owned restore data negative cases: foreign owner, wrong operation and unconfirmed receipt refused; repeat validation writes no audit; full equality observed before consumers resume")
}
console.log("✓ Terminal restore: real pg_dump/pg_restore transaction, encrypted human upload, approval, global protection, request drain, external receipt, revoked tokens, preserved external owner revocation and no replay")
console.log("✓ Terminal restore: real pg_dump/pg_restore transaction, encrypted human upload, approval, global protection, request drain, external receipt, restored live credential refused by external Portal cutoff, post-cutoff claims admitted and no replay")
// Archive authentication is checked before a terminal attempt can replace data.
await updateMcpSettings({ enabled: true }, actor)
@@ -165,6 +205,7 @@ try {
await delay(100)
}
assert.equal(receipt.state, "blocked", JSON.stringify(receipt))
await waitForDatabaseRetirement()
assert.equal(await accessGeneration(), generationBeforeCorruption)
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")
+24 -12
View File
@@ -5,7 +5,7 @@ import { z } from "zod"
import { pool } from "../../db/index.js"
import { hasPermission, type AuthUser } from "../../lib/permissions.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 { accessGeneration, decryptBinary, encryptBinary, decryptSecret, McpError, mcpStateDirectory } from "./security.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 { withApplicationMutationLock } from "../../services/config-mutation-lock.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
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> }
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) {
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) => {
@@ -91,15 +96,17 @@ export function registerDatabaseRestore(register: typeof add) {
}
/** 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) {
const lease = await pool.connect()
let locked = false, unsafe = false
try {
locked = (await lease.query<{ locked: boolean }>("SELECT pg_try_advisory_lock(hashtextextended($1,0)) AS locked", ["mcp-operation:" + operationId])).rows[0].locked
if (locked) await executeRestore(operationId, principal,originalGrant)
} finally {
if (locked) try { await lease.query("SELECT pg_advisory_unlock(hashtextextended($1,0))", ["mcp-operation:" + operationId]) } catch { unsafe = true }
lease.release(unsafe)
}
return withSystemDatabaseRestoreAdmission(async () => {
const lease = await pool.connect()
let locked = false, unsafe = false
try {
locked = (await lease.query<{ locked: boolean }>("SELECT pg_try_advisory_lock(hashtextextended($1,0)) AS locked", ["mcp-operation:" + operationId])).rows[0].locked
if (locked) await executeRestore(operationId, principal,originalGrant)
} finally {
if (locked) try { await lease.query("SELECT pg_advisory_unlock(hashtextextended($1,0))", ["mcp-operation:" + operationId]) } catch { unsafe = true }
lease.release(unsafe)
}
})
}
async function executeRestore(operationId: string, principal: McpPrincipal,originalGrant?:OriginalServiceGrant) {
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 }
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 }
let attempted = false, stage = "authority"
let attempted = false, stage = "authority", restoreActorId:string|undefined
const assertAuthority = async () => {
await assertOriginalMcpTemporaryGrant(pool,principal.connectionId,principal.credentialId,originalGrant)
const connection = await validatePrincipal(principal)
@@ -124,6 +131,7 @@ async function executeRestore(operationId: string, principal: McpPrincipal,origi
}
try {
const { connection, ids } = await assertAuthority(), source = await archive(payload.archiveRef, connection.owner_id)
restoreActorId=connection.owner_id
stage = "archive-read"
if (source.sha256 !== payload.expectedSha256) throw new McpError("STATE_CONFLICT", "Архив подменён", 409)
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]) }
stage = "receipt-publish"
await durableWrite(filename(operationId, ".receipt.json"), JSON.stringify(receipt))
await audit(pool, connection.owner_id, "database.restore.receipt", null, receipt).catch(() => {})
} catch (error) {
// The isolated destructive acceptance runner can print bounded diagnostics
// 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))
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 { 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"
const proof = async (value: unknown) => createHash("sha256").update(fingerprint(value)).digest("hex")
@@ -25,6 +28,7 @@ function fixture() {
return { rows }
} }
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 }
}
{
@@ -100,4 +104,22 @@ for (const mutate of mutations) {
assert.equal(source.originalContext.tool, "bandwidth"); assert.equal(source.stageBudget, 1)
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 { SERVER_PROBE_OPERATION } from "../../services/server-probe-authority.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 uuid = z.string().uuid()
@@ -113,5 +113,14 @@ export const loadDiagnosticCancellationSource = sourceLoader(productionCrypto)
/** Test-only crypto seam. Never accept these dependencies from tool JSON. */
export const createDiagnosticCancellationSourceForTests = sourceLoader
/** 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
/** 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)
}
+45 -1
View File
@@ -1,11 +1,12 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { analyzeEcmpPreflight, preflightObservation, preflightPaths, type PreflightObservation, type PreflightSource } from "./ecmp-preflight.js"
import { analyzeEcmpPreflight, duplicateEcmpAdjacency, ecmpAdjacency, preflightObservation, preflightPaths, type PreflightObservation, type PreflightSource } from "./ecmp-preflight.js"
function fixture(): Record<PreflightSource, PreflightObservation> {
const result = Object.fromEntries(Object.keys(preflightPaths("ipv4")).map(key => [key, { rows: [], error: null }])) as Record<PreflightSource, PreflightObservation>
result.routes.rows = [1, 2].map(index => ({ ".id": `*${index}`, "dst-address": "192.0.2.0/24", "routing-table": "main", gateway: `192.0.2.${index}`, "immediate-gw": `192.0.2.${index}%gre${index}`, disabled: "false", active: index === 1 ? "true" : "false", distance: String(index * 10) }))
result.settings.rows = [{ "rp-filter": "no", "ipv4-multipath-hash-policy": "l3", "multipath-hash-policy": "l4" }]
result.tables.rows = [{ ".id": "*main", name: "main", fib: "true" }]
result.connectionTracking.rows = [{ enabled: "auto" }]
return result
}
@@ -15,6 +16,23 @@ test("preflight observes unequal/inactive candidates without claiming packet/pat
assert.equal(result.decision, "review-required"); assert.equal(result.independentPaths, "not-proven"); assert.equal(result.networkValidation, "not-performed")
assert.equal(result.hashPolicy, "l3"); assert.deepEqual(result.findings, [])
})
test("preflight flags missing, ambiguous or inactive FIB without accepting unrelated tables", () => {
for (const tables of [
[],
[{ ".id": "*main", name: "main" }],
[{ ".id": "*main", name: "main", fib: "" }],
[{ ".id": "*main", name: "main", fib: "false" }],
[{ ".id": "*main", name: "main", fib: "true", disabled: "true" }],
[{ ".id": "*main", name: "main", fib: "true", invalid: "true" }],
[{ ".id": "*main", name: "main", fib: "true" }, { ".id": "*alias", name: "main", fib: "true" }],
]) {
const sources = fixture()
sources.tables.rows = [...tables, { ".id": "*other", name: "other", fib: "true" }]
const result = analyzeEcmpPreflight("ipv4", ["*1", "*2"], sources)
assert.equal(result.configurationObserved, false)
assert.deepEqual(result.findings.find(item => item.code === "FIB_TABLE_UNCONFIRMED")?.ids, ["*1", "*2"])
}
})
test("NAT, FastTrack, marks, notrack, strict RPF and shared egress require explicit review", () => {
const sources = fixture()
sources.filter.rows = [{ ".id": "*fast", action: "fasttrack-connection", disabled: "false" }, { ".id": "*disabled", action: "fasttrack-connection", disabled: "true" }]
@@ -52,3 +70,29 @@ test("IPv6 uses IPv6 paths/hash policy and explicitly has no IPv4 RPF setting",
assert.deepEqual(result.rpf, { supported: false, mode: null }); assert.equal(result.hashPolicy, "l4")
assert.ok(!result.findings.some(item => item.code.startsWith("RPF")))
})
test("ECMP identifies adjacency by canonical address and egress link", () => {
const duplicate = (first: string, second: string) => duplicateEcmpAdjacency([ecmpAdjacency(first), ecmpAdjacency(second)])
assert.ok(duplicate("fe80::1%gre1@main", "fe80:0:0:0:0:0:0:1%gre1@tenant"))
assert.ok(!duplicate("fe80::1%gre1", "fe80::1%gre2"))
assert.ok(!duplicate("192.0.2.1%gre1", "192.0.2.2%gre1"))
assert.ok(duplicate("192.0.2.1", "192.0.2.1%gre1"))
assert.equal(ecmpAdjacency("fe80::1"), null)
assert.equal(ecmpAdjacency("192.0.2.1%gre1,192.0.2.2%gre2"), null)
assert.equal(ecmpAdjacency("gre1"), null)
assert.deepEqual(ecmpAdjacency("gre1", new Set(["gre1"])), { address: null, interface: "gre1" })
const sources = fixture()
sources.routes.rows[1]["immediate-gw"] = sources.routes.rows[0]["immediate-gw"]
assert.ok(analyzeEcmpPreflight("ipv4", ["*1", "*2"], sources).findings.some(item => item.code === "DUPLICATE_IMMEDIATE_GATEWAY"))
})
test("interface-only ECMP next-hop requires a unique observed name", () => {
const sources = fixture()
sources.routes.rows[0]["immediate-gw"] = "gre2"
const analyze = (rows: Record<string, unknown>[]) => analyzeEcmpPreflight("ipv4", ["*1", "*2"], sources, preflightObservation(rows))
const known = analyze([{ ".id": "*gre", name: "gre2" }])
assert.equal(known.configurationObserved, true)
assert.ok(known.findings.some(item => item.code === "SHARED_EGRESS_INTERFACE"))
const ambiguous = analyze([{ ".id": "*gre", name: "gre2" }, { ".id": "*other", name: "gre2" }])
assert.equal(ambiguous.configurationObserved, false)
assert.ok(ambiguous.findings.some(item => item.code === "EGRESS_INTERFACE_UNKNOWN"))
assert.equal(analyze([{ name: "gre2" }]).sources.interfaces.observed, false)
})
+63 -4
View File
@@ -1,4 +1,16 @@
import { ipv6ToInt } from "../../services/traffic-flow-ip.js"
import { isConfirmedRoutingFibTable } from "../../services/routing-table-fib.js"
import { routeGatewayContext } from "./routing-community-context-model.js"
type Row = Record<string, unknown>
export function confirmedEcmpTable(rows: readonly Row[], name: string) {
const matches = rows.filter(row => row.name === name)
if (matches.length !== 1) return undefined
const table = matches[0], id = table[".id"]
// The /routing/table catalogue can return an empty FIB value for a non-FIB table.
if (typeof id !== "string" || !id.trim() || table.fib === "" || !isConfirmedRoutingFibTable(table)) return undefined
return { id }
}
export type PreflightObservation = { rows: Row[]; error: string | null }
export const preflightPaths = (family: "ipv4" | "ipv6") => {
const base = family === "ipv4" ? "ip" : "ipv6"
@@ -17,12 +29,48 @@ export function preflightObservation(raw: unknown, singleton = false): Preflight
const text = (value: unknown) => typeof value === "string" && value.trim() ? value.trim() : null
const flag = (value: unknown) => value === true || value === "true" || value === "yes" ? true : value === false || value === "false" || value === "no" ? false : null
export function ecmpAdjacency(value: unknown, interfaces?: ReadonlySet<string>) {
if (typeof value !== "string" || !value || /[\r\n,]/.test(value)) return null
try {
const hop = routeGatewayContext(value)
const address = hop.family === 6 ? ipv6ToInt(hop.address) : hop.address
// fe80::/10 needs a confirmed link to identify the adjacency.
if (address === null || (typeof address === "bigint" && address >> 118n === 0x3fan && hop.interface === null)) return null
return { address, interface: hop.interface }
} catch {
return interfaces?.has(value) ? { address: null, interface: value } : null
}
}
export function confirmedEcmpInterfaces(rows: readonly Row[]) {
const names = new Map<string, string>()
const ambiguous = new Set<string>()
for (const row of rows) {
if (typeof row.name !== "string" || !row.name.trim() || typeof row[".id"] !== "string" || !row[".id"].trim()) continue
if (names.has(row.name)) ambiguous.add(row.name)
names.set(row.name, row[".id"])
}
for (const name of ambiguous) names.delete(name)
return names
}
export function confirmedEcmpInterfaceNames(rows: readonly Row[]) {
return new Set(confirmedEcmpInterfaces(rows).keys())
}
export function duplicateEcmpAdjacency(hops: ReturnType<typeof ecmpAdjacency>[]) {
return hops.some((hop, index) => hop && hops.slice(0, index).some(previous =>
previous && previous.address === hop.address
&& (previous.interface === hop.interface || previous.interface === null || hop.interface === null)))
}
/** Configuration evidence only. Rule presence does not prove packet matching,
* physical independence, NAT affinity, return path or traffic distribution. */
export function analyzeEcmpPreflight(family: "ipv4" | "ipv6", routeIds: string[], observed: Record<PreflightSource, PreflightObservation>) {
export function analyzeEcmpPreflight(family: "ipv4" | "ipv6", routeIds: string[], observed: Record<PreflightSource, PreflightObservation>, inventory?: PreflightObservation) {
const findings: { code: string; level: "review" | "unknown"; resource: string; ids: string[]; objectCount: number; idsTruncated: boolean; message: string }[] = []
const add = (code: string, level: "review" | "unknown", resource: string, ids: string[], message: string) => findings.push({ code, level, resource, ids: ids.slice(0, 100), objectCount: ids.length, idsTruncated: ids.length > 100, message })
for (const [resource, observation] of Object.entries(observed)) if (observation.error) add("OBSERVATION_UNAVAILABLE", "unknown", resource, [], observation.error)
const sources = inventory ? { ...observed, interfaces: inventory } : observed
for (const [resource, observation] of Object.entries(sources)) if (observation.error) add("OBSERVATION_UNAVAILABLE", "unknown", resource, [], observation.error)
const candidates = routeIds.map(id => observed.routes.rows.find(row => row[".id"] === id))
const selected = candidates.filter((row): row is Row => !!row)
if (selected.length !== routeIds.length) add("ROUTE_NOT_OBSERVED", "unknown", "routes", routeIds.filter((_id, index) => !candidates[index]), "Не все выбранные маршруты подтверждены")
@@ -30,9 +78,20 @@ export function analyzeEcmpPreflight(family: "ipv4" | "ipv6", routeIds: string[]
if (routeSummary.some(row => !row.prefix || !row.table || !row.gateway || !row.immediateGateway || row.disabled === null)) add("ROUTE_CONTEXT_UNKNOWN", "unknown", "routes", routeSummary.map(row => row.id), "Не подтверждены prefix/table/gateway/disabled; отсутствие свойства не считается значением по умолчанию")
if (routeSummary.some(row => row.disabled === true)) add("DISABLED_ROUTE", "review", "routes", routeSummary.filter(row => row.disabled === true).map(row => row.id), "В наборе есть отключённые маршруты")
if (new Set(routeSummary.map(row => row.prefix).filter(Boolean)).size > 1 || new Set(routeSummary.map(row => row.table).filter(Boolean)).size > 1) add("DIFFERENT_ROUTE_CONTEXTS", "review", "routes", routeSummary.map(row => row.id), "Prefix или routing table выбранных маршрутов различаются")
if (observed.tables.error === null) {
const unconfirmed = new Set<string>()
for (const table of new Set(routeSummary.map(row => row.table))) {
if (table !== null && !confirmedEcmpTable(observed.tables.rows, table)) unconfirmed.add(table)
}
const affected = routeSummary.filter(row => row.table !== null && unconfirmed.has(row.table))
if (affected.length) add("FIB_TABLE_UNCONFIRMED", "unknown", "routes", affected.map(row => row.id), "Для выбранных маршрутов не подтверждена однозначная активная таблица FIB")
}
const knownGateways = routeSummary.map(row => row.gateway).filter(Boolean)
if (new Set(knownGateways).size !== knownGateways.length) add("DUPLICATE_GATEWAY", "review", "routes", routeSummary.map(row => row.id), "Next-hop выбранных маршрутов повторяется")
const interfaces = routeSummary.map(row => row.immediateGateway?.split("%")[1]?.split("@")[0] || null)
const names = inventory?.error === null ? confirmedEcmpInterfaceNames(inventory.rows) : undefined
const hops = selected.map(row => ecmpAdjacency(row["immediate-gw"], names))
if (duplicateEcmpAdjacency(hops)) add("DUPLICATE_IMMEDIATE_GATEWAY", "review", "routes", routeSummary.map(row => row.id), "Выбранные маршруты разрешаются в один и тот же next-hop")
const interfaces = hops.map(hop => hop?.interface ?? null)
if (interfaces.some(iface => iface === null)) add("EGRESS_INTERFACE_UNKNOWN", "unknown", "routes", routeSummary.map(row => row.id), "Не подтверждён интерфейс immediate gateway")
else if (new Set(interfaces).size < interfaces.length) add("SHARED_EGRESS_INTERFACE", "review", "routes", routeSummary.map(row => row.id), "Несколько next-hop используют один интерфейс; независимость путей не подтверждена")
const maybeEnabled = (resource: PreflightSource) => {
@@ -59,5 +118,5 @@ export function analyzeEcmpPreflight(family: "ipv4" | "ipv6", routeIds: string[]
if (!connectionTracking) add("CONNECTION_TRACKING_UNKNOWN", "unknown", "connectionTracking", [], "Режим connection tracking не подтверждён")
else if (connectionTracking === "no" || connectionTracking === "false") add("CONNECTION_TRACKING_DISABLED", "review", "connectionTracking", [], "Connection tracking отключён; проверить зависимости NAT/stateful firewall")
return { family, decision: "review-required", configurationObserved: !findings.some(item => item.level === "unknown"), networkValidation: "not-performed", independentPaths: "not-proven", routeSummary, hashPolicy, rpf: { supported: family === "ipv4", mode: rpf }, connectionTracking,
sources: Object.fromEntries(Object.entries(observed).map(([key, value]) => [key, { observed: value.error === null, count: value.rows.length }])), findings }
sources: Object.fromEntries(Object.entries(sources).map(([key, value]) => [key, { observed: value.error === null, count: value.rows.length }])), findings }
}
@@ -0,0 +1,73 @@
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { test } from "node:test"
import type { McpAction, ActionContext } from "./action-types.js"
import { registerGlobalReconciliationActionsForTests } from "./global-reconciliation-actions.js"
type Register = Parameters<typeof registerGlobalReconciliationActionsForTests>[0]
type Dependencies = Parameters<typeof registerGlobalReconciliationActionsForTests>[1]
function fixture() {
const connectionId = randomUUID(), operationId = randomUUID(), ownerId = "current-retired-owner"
const context = { principal: { connectionId }, connection: { id: connectionId, owner_id: ownerId, owner_permissions: ["mm:servers:write"],
policy: { permissions: ["mm:servers:write"], scope: { allowGlobal: true, operationIds: ["servers.poll", "operations.global.reconcile"], serverIds: [1, 2] } } } } as ActionContext
let action!: Pick<McpAction, "target" | "read">
let history: { affectedServerIds: number[] } | undefined = { affectedServerIds: [1, 2] }
let revoke = false, retiredCalls = 0, live = false
const deps = {
connection: async () => {
if (revoke) throw Error("revoked-current-grant")
return structuredClone(context.connection)
},
query: async (sql: string) => {
if (sql.startsWith("SELECT id FROM servers")) return [{ id: 1 }]
if (live) return [{ id: operationId, state: "applied", operation_id: "servers.poll", evidence: { sealed: "live" } }]
return []
},
operation: (id: string) => ({ id, scope: "global", requiredPermissions: ["mm:servers:write"] }),
service: () => ({ reconcile: async () => ({ matchesDesiredState: true, matchesOriginalState: false }) }),
decrypt: async () => JSON.stringify({ target: { scope: "global" }, objects: [{ serverId: 1 }], steps: [{ kind: "service", service: "servers.poll", input: {} }] }),
retired: async (identity, authorize) => {
retiredCalls++
assert.deepEqual(identity, { operationId, connectionId, ownerId })
await authorize()
return history
},
} satisfies Omit<Dependencies, "operation" | "service"> & Partial<Record<"operation" | "service", unknown>>
registerGlobalReconciliationActionsForTests(((...args: Parameters<Register>) => { action = args[8] }) as Register, deps as unknown as Dependencies)
return { context, operationId, history: (value: typeof history) => { history = value }, live: () => { live = true },
revoke: () => { revoke = true }, calls: () => retiredCalls, read: () => action.read!({ operationId }, context),
target: () => action.target({ operationId }, context) }
}
test("global action reconciles retired poll only under current ownership and returns no archived material", async () => {
const f = fixture()
assert.deepEqual(await f.target(), { scope: "global" })
const result = await f.read() as { observedAt: string }
const { observedAt, ...rest } = result
assert(Number.isFinite(Date.parse(observedAt)))
assert.deepEqual(rest, { operationId: f.operationId, state: "applied", automaticRetry: false,
results: [{ stepIndex: 0, service: "servers.poll", matchesDesiredState: true, matchesOriginalState: false }] })
assert.equal(f.calls(), 2)
})
test("global action refuses missing or inaccessible retired history", async () => {
const f = fixture()
f.history(undefined)
await assert.rejects(f.read(), { code: "NOT_FOUND" })
f.history({ affectedServerIds: [1, 2] })
f.context.connection.policy.scope.serverIds = [1]
await assert.rejects(f.read(), { code: "SERVER_DENIED" })
f.context.connection.policy.scope.serverIds = [1, 2]
f.context.connection.policy.scope.operationIds = ["operations.global.reconcile"]
await assert.rejects(f.read(), { code: "PERMISSION_DENIED" })
f.revoke()
await assert.rejects(f.read(), /revoked-current-grant/)
})
test("global action uses the live reconciliation path while original source exists", async () => {
const f = fixture()
f.live()
await f.read()
assert.equal(f.calls(), 0)
})
@@ -1,5 +1,6 @@
import { z } from "zod"
import { pool } from "../../db/index.js"
import { readRetiredServerPollHistory } from "../../services/server-poll-history-reader.js"
import { hasPermission } from "../../lib/permissions.js"
import { effectiveMcpConnection } from "./store.js"
import { decryptSecret, fingerprint, McpError } from "./security.js"
@@ -7,40 +8,84 @@ import { findMcpOperation } from "./registry.js"
import { findMcpService } from "./service-registry.js"
import type { add } from "./actions.js"
import type { ActionContext, ChangeEvidence } from "./action-types.js"
import type { ConnectionRow } from "./store.js"
async function authorizedSource(operationId: string, context: ActionContext) {
const production = {
connection: effectiveMcpConnection,
query: async (sql: string, values: unknown[] = []) => (await pool.query(sql, values)).rows,
operation: findMcpOperation,
service: findMcpService,
decrypt: decryptSecret,
retired: readRetiredServerPollHistory,
}
type Dependencies = typeof production
const notFound = () => new McpError("NOT_FOUND", "Операция не найдена", 404)
async function currentAccess(context: ActionContext, deps: Dependencies) {
context.signal?.throwIfAborted()
const current = await effectiveMcpConnection(context.principal)
const current = await deps.connection(context.principal)
if (!current.policy.scope.allowGlobal || fingerprint(current.policy) !== fingerprint(context.connection.policy) || fingerprint(current.owner_permissions) !== fingerprint(context.connection.owner_permissions)) throw new McpError("PERMISSION_DENIED", "Глобальный допуск сверки изменился", 403)
const ids = (await pool.query("SELECT id FROM servers ORDER BY id LIMIT 1001")).rows.map(row => Number(row.id))
const ids = (await deps.query("SELECT id FROM servers ORDER BY id LIMIT 1001")).map(row => Number(row.id))
if (ids.length > 1000) throw new McpError("LIMIT_EXCEEDED", "Каталог глобальной сверки превышает лимит", 409)
if (ids.some(id => !current.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Глобальная сверка требует доступа ко всему каталогу серверов", 403)
const row = (await pool.query("SELECT o.id,o.state,p.operation_id,p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id WHERE o.id=$1 AND o.connection_id=$2", [operationId, current.id])).rows[0]
if (!row) throw new McpError("NOT_FOUND", "Операция не найдена", 404)
const original = findMcpOperation(row.operation_id)
return current
}
function requireOriginal(actionId: string, current: ConnectionRow, deps: Dependencies) {
const original = deps.operation(actionId)
if (!original || original.scope !== "global" || !current.policy.scope.operationIds.includes(original.id) || original.requiredPermissions.some(permission => !hasPermission(current.policy.permissions, permission) || !hasPermission(current.owner_permissions, permission))) throw new McpError("PERMISSION_DENIED", "Утрачено право на исходную глобальную операцию", 403)
const evidence = JSON.parse(await decryptSecret(row.evidence.sealed)) as ChangeEvidence
if (evidence.target.scope !== "global" || !evidence.steps.length || evidence.steps.some(step => step.kind !== "service" || !findMcpService(step.service)?.reconcile)) throw new McpError("UNSUPPORTED_CAPABILITY", "Для глобальной операции нет безопасной read-only сверки", 409)
const declared = evidence.objects.flatMap(object => object.serverId === undefined ? [] : [object.serverId])
}
function requireServerScope(declared: number[], current: ConnectionRow) {
if (declared.some(id => !current.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Зависимость операции вне текущего глобального допуска", 403)
}
async function authorizedSource(operationId: string, context: ActionContext, deps: Dependencies) {
const current = await currentAccess(context, deps)
const row = (await deps.query("SELECT o.id,o.state,p.operation_id,p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id WHERE o.id=$1 AND o.connection_id=$2", [operationId, current.id]))[0]
if (!row) {
const authorize = async () => {
const fresh = await currentAccess(context, deps)
if (fresh.id !== current.id || fresh.owner_id !== current.owner_id) throw notFound()
}
const archived = await deps.retired({ operationId, connectionId: current.id, ownerId: current.owner_id }, authorize,
{ deadlineAt: Date.now() + 10000, signal: context.signal })
context.signal?.throwIfAborted()
if (!archived) throw notFound()
requireOriginal("servers.poll", current, deps)
requireServerScope(archived.affectedServerIds, current)
return { kind: "retired-poll" as const }
}
requireOriginal(row.operation_id, current, deps)
const evidence = JSON.parse(await deps.decrypt(row.evidence.sealed)) as ChangeEvidence
if (evidence.target.scope !== "global" || !evidence.steps.length || evidence.steps.some(step => step.kind !== "service" || !deps.service(step.service)?.reconcile)) throw new McpError("UNSUPPORTED_CAPABILITY", "Для глобальной операции нет безопасной read-only сверки", 409)
requireServerScope(evidence.objects.flatMap(object => object.serverId === undefined ? [] : [object.serverId]), current)
if (["pending", "running"].includes(row.state)) throw new McpError("STATE_CONFLICT", "Операция ещё выполняется", 409)
context.signal?.throwIfAborted()
return { row, evidence }
return { kind: "live" as const, row, evidence }
}
export function registerGlobalReconciliationActions(register: typeof add) {
function registerActions(register: typeof add, deps: Dependencies) {
register("operations.global.reconcile", "mm_global_operation_reconcile", "Сверить глобальную сервисную операцию без повторной записи", "mcp", null, "read", "global", z.object({ operationId: z.string().uuid() }).strict(), {
target: async (input, context) => { await authorizedSource(String(input.operationId), context); return { scope: "global" } },
target: async (input, context) => { await authorizedSource(String(input.operationId), context, deps); return { scope: "global" } },
read: async (input, context) => {
const { row, evidence } = await authorizedSource(String(input.operationId), context)
const authorize = async () => { await authorizedSource(String(input.operationId), context) }
const operationId = String(input.operationId)
const source = await authorizedSource(operationId, context, deps)
if (source.kind === "retired-poll") return { operationId, state: "applied", observedAt: new Date().toISOString(),
results: [{ stepIndex: 0, service: "servers.poll", matchesDesiredState: true, matchesOriginalState: false }], automaticRetry: false }
const { row, evidence } = source
const authorize = async () => { if ((await authorizedSource(operationId, context, deps)).kind !== "live") throw notFound() }
const results = []
for (const [index, step] of evidence.steps.entries()) {
if (step.kind !== "service") throw new McpError("UNSUPPORTED_CAPABILITY", "Нативная глобальная сверка недоступна", 409)
await authorize()
results.push({ stepIndex: index, service: step.service, ...await findMcpService(step.service)!.reconcile!(step.input, authorize) })
results.push({ stepIndex: index, service: step.service, ...await deps.service(step.service)!.reconcile!(step.input, authorize) })
}
await authorize()
return { operationId: row.id, state: row.state, observedAt: new Date().toISOString(), results, automaticRetry: false }
},
})
}
export const registerGlobalReconciliationActions = (register: typeof add) => registerActions(register, production)
export const registerGlobalReconciliationActionsForTests = registerActions
+111 -15
View File
@@ -1282,6 +1282,7 @@ try {
preflightFixtures = Object.fromEntries(Object.values(preflightPaths("ipv4")).map(path => ["/rest" + path, []]))
preflightFixtures["/rest/ip/route"] = [1, 2].map(index => ({ ".id": `*p${index}`, "dst-address": "192.0.2.0/24", gateway: `192.0.2.${index}`, "immediate-gw": `192.0.2.${index}%same-gre`, "routing-table": "main", distance: "10", active: "true", disabled: "false" }))
preflightFixtures["/rest/ip/settings"] = { "rp-filter": "strict", "ipv4-multipath-hash-policy": "l3" }
preflightFixtures["/rest/routing/table"] = [{ ".id": "*main", name: "main", fib: "true" }]
preflightFixtures["/rest/ip/firewall/connection/tracking"] = { enabled: "auto" }
preflightFixtures["/rest/ip/firewall/filter"] = [{ ".id": "*ft", action: "fasttrack-connection", disabled: "false" }]
preflightFixtures["/rest/ip/firewall/nat"] = [{ ".id": "*nat", action: "masquerade", disabled: "false" }]
@@ -1293,6 +1294,11 @@ try {
assert.equal(preflight.configurationObserved, true); assert.equal(preflight.networkValidation, "not-performed"); assert.equal(catalogCalls, 10)
for (const code of ["SHARED_EGRESS_INTERFACE", "STRICT_RPF_REVIEW", "FASTTRACK_REVIEW", "NAT_AFFINITY_REVIEW"]) assert.ok(preflight.findings.some(item => item.code === code), code)
await assert.rejects(invokeMcpTool(preflightPrincipal, "mm_ecmp_preflight_get", { ...preflightInput, serverId: 2 })); assert.equal(catalogCalls, 10)
preflightFixtures["/rest/routing/table"] = [{ ".id": "*main", name: "main", fib: "" }]
const noFibPreflight = await invokeMcpTool(preflightPrincipal, "mm_ecmp_preflight_get", preflightInput) as typeof preflight
assert.equal(noFibPreflight.configurationObserved, false)
assert.ok(noFibPreflight.findings.some(item => item.code === "FIB_TABLE_UNCONFIRMED"))
preflightFixtures["/rest/routing/table"] = [{ ".id": "*main", name: "main", fib: "true" }]
preflightUnavailable = "/rest/ip/firewall/nat"
const unknownPreflight = await invokeMcpTool(preflightPrincipal, "mm_ecmp_preflight_get", preflightInput) as typeof preflight
assert.equal(unknownPreflight.configurationObserved, false); assert.ok(unknownPreflight.findings.some(item => item.code === "OBSERVATION_UNAVAILABLE")); assert.equal(writes, preflightWritesBefore)
@@ -2224,7 +2230,7 @@ try {
await pool.query("INSERT INTO mcp_routing_observations(id,src_server_id,dst_server_id,src_interface,dst_interface,family,routing_table,ping_ms,loss_pct,dl_mbps,ul_mbps,ping_at,bandwidth_at,context_verified,source) VALUES($1,1,2,'fixture-gre','peer','ipv4','main',12,0,100,200,now(),now(),true,'fixture')", [observationId])
await attestRoutingFixture(observationId)
try {
type OspfAnalysis = { ranking: { currentCost: number; proposedCost: number; observationId: string; peerId: number }[]; serverIds: number[]; changes: number; proposalOnly: boolean; executionAvailable: boolean }
type OspfAnalysis = { ranking: { currentCost: number; proposedCost: number; observationId: string; bandwidthObservationId: string | null; peerId: number }[]; serverIds: number[]; changes: number; proposalOnly: boolean; executionAvailable: boolean }
const applicationPreviewPolicy={name:"OSPF application preview",mode:"observer",expiresAt:new Date(Date.now()+1800000).toISOString(),permissions:["mm:network:read"],scope:{serverIds:[1,2],operationIds:["routing.ospf.application.preview"],allowGlobal:false}}
const applicationPreviewAccess=await createMcpConnection(mcpConnectionCreateSchema.parse(applicationPreviewPolicy),actor),applicationPreviewPrincipal=await authenticateMcpToken(applicationPreviewAccess.token)
const isolatedPreviewAccess=await createMcpConnection(mcpConnectionCreateSchema.parse({...applicationPreviewPolicy,name:"OSPF source-only preview",scope:{...applicationPreviewPolicy.scope,serverIds:[1]}}),actor),isolatedPreviewPrincipal=await authenticateMcpToken(isolatedPreviewAccess.token)
@@ -2388,10 +2394,14 @@ try {
})}finally{countCatalogCalls=false}
assert.equal((await (await import("./routing-observations.js")).readRoutingObservations([1, 2]))[0].sourceVerified, false, "Disable/re-enable must invalidate previous source attestation")
await attestRoutingFixture(observationId)
const pingOnlyId = randomUUID(), otherPeerInterfaceId = randomUUID()
const insertPingObservation = (id: string, peerInterface: string) => pool.query("INSERT INTO mcp_routing_observations(id,src_server_id,dst_server_id,src_interface,dst_interface,family,routing_table,ping_ms,loss_pct,ping_at,context_verified,source) VALUES($1,1,2,'fixture-gre',$2,'ipv4','main',11,0,now(),true,'fixture')", [id, peerInterface])
await insertPingObservation(pingOnlyId, "peer")
await attestRoutingFixture(pingOnlyId)
const proposal = await invokeMcpTool(analysisPrincipal, "mm_ospf_optimizer_analyze", { serverId: 1 }) as OspfAnalysis
assert.deepEqual(proposal.serverIds, [1, 2]); assert.equal(proposal.changes, 1)
assert.equal(proposal.ranking[0].currentCost, 30); assert.equal(proposal.ranking[0].proposedCost, 10)
assert.equal(proposal.ranking[0].observationId, observationId); assert.equal(proposal.ranking[0].peerId, 2)
assert.equal(proposal.ranking[0].observationId, pingOnlyId); assert.equal(proposal.ranking[0].bandwidthObservationId, observationId); assert.equal(proposal.ranking[0].peerId, 2)
assert.equal(proposal.proposalOnly, true); assert.equal(proposal.executionAvailable, false)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_plans WHERE connection_id=$1", [analysisAccess.connection.id])).rows[0].n, 0)
assert.equal(ospfAnalysisDispatches, 0)
@@ -2405,6 +2415,9 @@ try {
await verifyRoutingObservationChecks(observationEvidence.observationChecks)
await assert.rejects(verifyRoutingObservationChecks(observationEvidence.observationChecks.map((check: Record<string, unknown>) => ({ ...check, validUntil: new Date(Date.now() - 1).toISOString() }))))
await approvePlan(prepared.planId, actor, true)
await insertPingObservation(otherPeerInterfaceId, "another-peer-interface")
await attestRoutingFixture(otherPeerInterfaceId)
await verifyRoutingObservationChecks(observationEvidence.observationChecks)
await pool.query("UPDATE mcp_routing_observations SET created_at=now()-interval '1 minute' WHERE id=$1", [observationId])
await attestRoutingFixture(observationId)
await pool.query("INSERT INTO mcp_routing_observations(id,src_server_id,dst_server_id,src_interface,dst_interface,family,routing_table,ping_ms,loss_pct,dl_mbps,ul_mbps,ping_at,bandwidth_at,context_verified,source) VALUES($1,1,2,'fixture-gre','peer','ipv4','main',12,50,100,200,now(),now(),true,'fixture')", [badObservationId])
@@ -2422,7 +2435,7 @@ try {
await pool.query("UPDATE mcp_routing_observations SET loss_pct=0,context_verified=false WHERE id=$1", [badObservationId])
await attestRoutingFixture(badObservationId)
await assert.rejects(invokeMcpTool(analysisPrincipal, "mm_ospf_optimizer_analyze", { serverId: 1 }))
await pool.query("DELETE FROM mcp_routing_observations WHERE id=$1", [badObservationId])
await pool.query("DELETE FROM mcp_routing_observations WHERE id=ANY($1::uuid[])", [[badObservationId, pingOnlyId, otherPeerInterfaceId]])
await pool.query("UPDATE mcp_routing_observations SET loss_pct=5 WHERE id=$1", [observationId])
await attestRoutingFixture(observationId)
await assert.rejects(verifyRoutingObservationChecks(observationEvidence.observationChecks))
@@ -2589,9 +2602,10 @@ try {
const nativeRequests = ospfAnalysisNativeRequests - requestsBeforeApply
assert.ok(nativeRequests <= 160, `One OSPF cost PATCH caused ${nativeRequests} native requests; full graph polling must not run inside each catalog callback`)
console.log(`✓ OSPF dispatch read budget: ${nativeRequests} native requests across three owners, including one PATCH; no nested full graph polling`)
await pool.query("UPDATE mcp_plans SET operation_version=8 WHERE id=$1", [prepared.planId])
const { rows: [version] } = await pool.query<{ operation_version: number }>("SELECT operation_version FROM mcp_plans WHERE id=$1", [prepared.planId])
await pool.query("UPDATE mcp_plans SET operation_version=$2 WHERE id=$1", [prepared.planId, version.operation_version - 1])
try { await assert.rejects(invokeMcpTool(graphPrincipal, "mm_operation_recovery_plan", { operationId: operation.id }), (error: { code?: string }) => error.code === "PLAN_EXPIRED") }
finally { await pool.query("UPDATE mcp_plans SET operation_version=9 WHERE id=$1", [prepared.planId]) }
finally { await pool.query("UPDATE mcp_plans SET operation_version=$2 WHERE id=$1", [prepared.planId, version.operation_version]) }
const oldInverse = await invokeMcpTool(graphPrincipal, "mm_operation_recovery_plan", { operationId: operation.id }) as { planId: string }
await approvePlan(oldInverse.planId, actor, true)
await pool.query("UPDATE mcp_plans SET operation_version=1 WHERE id=$1", [oldInverse.planId])
@@ -2755,8 +2769,8 @@ try {
await approvePlan(brokenEcmp.planId, actor, true)
loseEcmpGateway = true
const brokenOperation = await applyPlan(ecmpPrincipal, brokenEcmp.planId, randomUUID())
await awaitOperation(brokenOperation.id, "partial"); assert.equal(ecmpWrites, 6)
await applyPlan(ecmpPrincipal, brokenEcmp.planId, randomUUID()); await delay(30); assert.equal(ecmpWrites, 6)
await awaitOperation(brokenOperation.id, "partial"); assert.equal(ecmpWrites, 5)
await applyPlan(ecmpPrincipal, brokenEcmp.planId, randomUUID()); await delay(30); assert.equal(ecmpWrites, 5)
loseEcmpGateway = false; fixtureEcmpRoutes = structuredClone(ecmpBaseline)
const tableProtection = randomUUID()
await pool.query("INSERT INTO mcp_protections(id,object_ref,created_by) VALUES($1,$2::jsonb,'integration')", [tableProtection, JSON.stringify({ kind: "routing_table", id: "*main", serverId: 1, protection: "deny" })])
@@ -2786,7 +2800,13 @@ try {
assert.equal(state, "applied"); assert.equal(writes, 1); assert.equal(settings["rp-filter"], "loose")
assert.equal((await pool.query("SELECT state FROM mcp_operation_steps WHERE operation_id=$1", [operation.id])).rows[0].state, "verified")
console.log("✓ Human approval, write-ahead intent, single idempotent write and read-back verification")
const stale = await invokeMcpTool(principal, "mm_ip_settings_update_plan", { serverId: 1, fields: { "rp-filter": "strict" } }) as { planId: string }
const { detectMcpDrift } = await import("./workers.js")
const driftCount = async () => (await pool.query("SELECT count(*)::int AS n FROM mcp_drift WHERE operation_id=$1", [operation.id])).rows[0].n
const readsBeforeMatchingDrift = reconcileSettingsReads
await detectMcpDrift()
assert(reconcileSettingsReads > readsBeforeMatchingDrift)
assert.equal(await driftCount(), 0)
const stale = await invokeMcpTool(principal, "mm_ip_settings_update_plan", { serverId: 1, fields: { "rp-filter": "loose" } }) as { planId: string }
await approvePlan(stale.planId, actor, true)
settings["rp-filter"] = "no"
const conflict = await applyPlan(principal, stale.planId, "conflict-key")
@@ -2797,6 +2817,14 @@ try {
}
assert.equal(state, "rejected"); assert.equal(writes, 1)
console.log("✓ Changed snapshots reject execution before contacting the write endpoint")
const readsBeforeConfirmedDrift = reconcileSettingsReads
await detectMcpDrift()
assert(reconcileSettingsReads > readsBeforeConfirmedDrift)
assert.equal(await driftCount(), 1)
await detectMcpDrift()
assert.equal(await driftCount(), 1)
assert.equal(writes, 1)
console.log("✓ Drift detection reads the bound RouterOS source, records one confirmed incident and never replays writes")
const expiring = await invokeMcpTool(principal, "mm_ip_settings_update_plan", { serverId: 1, fields: { "rp-filter": "loose" } }) as { planId: string }
const sealedEvidence = (await pool.query("SELECT evidence FROM mcp_plans WHERE id=$1", [expiring.planId])).rows[0].evidence
const expiringEvidence = JSON.parse(await decryptSecret(sealedEvidence.sealed))
@@ -2855,6 +2883,7 @@ try {
process.env.MCP_PUBLIC_URL = endpoint
app = await buildApp({ startScheduler: false })
await app.listen({ port: appPort, host: "127.0.0.1" })
await (await import("../../test/users-profile-permissions.js")).verifyUsersProfilePermissions(app)
await (await import("../../test/mcp-backup-download.js")).verifyMcpBackupDownload(actor,app)
await (await import("../../test/mcp-database-backup.js")).verifyMcpDatabaseBackup(actor,app)
const fixtureScheduler = await import("../../services/scheduler-running.js")
@@ -2889,6 +2918,55 @@ try {
assert.ok(!result.isError)
console.log("✓ Official SDK HTTP initialize, filtered tools/list and tools/call")
} 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() }
}
const logsSdkAcceptance = await import("./logs-follow-sdk-cancellation-acceptance.js")
await logsSdkAcceptance.verifyLogsFollowSdkCancellation(app, endpoint, actor)
await logsSdkAcceptance.verifyLogsScopedSdkReads(app, endpoint, actor)
const stdio = new Client({ name: "stdio-integration", version: "1.0.0" })
try {
await stdio.connect(new StdioClientTransport({ command: process.execPath, args: ["--import", "tsx", fileURLToPath(new URL("../../scripts/mcp-stdio.ts", import.meta.url))], env: { MCP_URL: endpoint, MCP_TOKEN: rotated.token }, stderr: "pipe" }))
@@ -2907,17 +2985,41 @@ try {
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 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: {} })
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().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_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: {} })
assert.equal(duplicateReady.statusCode, 429)
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: {} })
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 { withMcpProbeCredential } = await import("./readiness.js")
let endedProbeToken = ""
@@ -2933,7 +3035,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)
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))
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 deviceHeaders = { authorization: "Bearer " + jwt }
const deviceCatalog = await app.inject({ url: "/api/mcp/devices/catalog?connectionId=" + deviceConnection.connection.id, headers: deviceHeaders })
@@ -2964,12 +3066,6 @@ try {
assert.equal((await tokenRequest(refreshPayload)).statusCode, 400)
await assert.rejects(authenticateMcpToken(refreshed.json().access_token))
console.log("✓ OAuth consent, PKCE, resource binding, one-use code, refresh rotation and reuse-family revocation")
const { detectMcpDrift } = await import("./workers.js")
await detectMcpDrift()
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_drift WHERE operation_id=$1", [operation.id])).rows[0].n, 1)
await detectMcpDrift()
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_drift WHERE operation_id=$1", [operation.id])).rows[0].n, 1)
console.log("✓ Drift detection records a single open incident without replaying writes")
const currentPrincipal = await authenticateMcpToken(rotated.token)
await assert.rejects(invokeMcpTool(currentPrincipal, "mm_operation_recovery_plan", { operationId: operation.id }), error => error instanceof Error && "code" in error && error.code === "STATE_CONFLICT")
const beforeRecovery = settings["rp-filter"]
@@ -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,251 @@
import assert from "node:assert/strict"
import { createHash, randomUUID } from "node:crypto"
import type { IncomingMessage, ServerResponse } from "node:http"
import { setTimeout as delay } from "node:timers/promises"
import type { FastifyInstance } from "fastify"
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"
import { SignJWT } from "jose"
import type { LogEntry } from "@mmapp/contracts"
import { mcpConnectionCreateSchema } from "@mmapp/contracts/mcp"
import { pool } from "../../db/index.js"
import { env } from "../../config.js"
import type { AuthUser } from "../../lib/permissions.js"
import { createMcpConnection, setConnectionStatus } from "./store.js"
export async function verifyLogsScopedSdkReads(app: FastifyInstance, endpoint: string, actor: AuthUser) {
const historicalId = 93, hiddenId = 94, marker = `mcp-scoped-logs-${randomUUID()}`
const from = "2001-01-01T00:00:00Z", to = "2001-01-01T01:00:00Z"
const client = new Client({ name: "logs-scoped-sdk-reads", version: "1.0.0" })
try {
await pool.query("INSERT INTO servers(id,name,host,enabled,username,password) VALUES($1,$2,'127.0.0.1',false,'fixture','fixture')", [historicalId, marker])
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({
name: marker, mode: "observer", permissions: ["mm:servers:read"],
expiresAt: new Date(Math.min(actor.expiresAt ?? Infinity, Date.now() + 600_000)).toISOString(),
scope: { serverIds: [1, historicalId], operationIds: ["logs.get", "logs.context", "logs.summary", "logs.export"], allowGlobal: false },
}), actor)
const { rows } = await pool.query<{ id: string; serverId: number; index: string }>(`
INSERT INTO router_log_entries(server_id,server_name,host,router_id,raw_time,received_at,effective_at,topics,level,message,generation)
SELECT server_id,$1,'127.0.0.1',n::text,'00:30:00',now(),$2::timestamptz+(30+n)*interval '1 minute',ARRAY['system'],'info',
CASE WHEN server_id=$4 THEN $1 || ' hidden-row' ELSE $1 END,0
FROM (SELECT 1 AS server_id,n FROM generate_series(-12,12) AS n UNION ALL SELECT $3,0 UNION ALL SELECT $4,0) AS fixture
RETURNING id::text,server_id AS "serverId",router_id AS "index"`, [marker, from, historicalId, hiddenId])
await pool.query("INSERT INTO router_log_collection_state(server_id,server_name,host) SELECT id,$1,'127.0.0.1' FROM unnest($2::int[]) AS id", [marker, [historicalId, hiddenId]])
assert.equal((await pool.query("DELETE FROM servers WHERE id=$1 AND name=$2", [historicalId, marker])).rowCount, 1)
await client.connect(new StreamableHTTPClientTransport(new URL(endpoint), {
requestInit: { headers: { Authorization: `Bearer ${access.token}` } },
}))
const idAt = (serverId: number, index: number) => {
const row = rows.find(item => item.serverId === serverId && Number(item.index) === index)
assert.ok(row)
return row.id
}
const call = (name: string, args: Record<string, unknown>) => client.callTool({ name, arguments: args })
const resultOf = async <T>(name: string, args: Record<string, unknown>) => {
const response = await call(name, args)
assert.ok(!response.isError)
return (response.structuredContent as { result: T }).result
}
const id = idAt(1, 0)
assert.equal((await resultOf<LogEntry>("mm_logs_get", { serverId: 1, id })).id, id)
type LogContext = { entry: LogEntry; before: LogEntry[]; after: LogEntry[] }
const context = await resultOf<LogContext>("mm_logs_context_get", { serverId: 1, id })
assert.equal(context.entry.id, id)
assert.deepEqual(context.before.map(row => row.id), Array.from({ length: 10 }, (_, i) => idAt(1, i - 10)))
assert.deepEqual(context.after.map(row => row.id), Array.from({ length: 10 }, (_, i) => idAt(1, i + 1)))
const historical = { serverId: historicalId, id: idAt(historicalId, 0) }
assert.equal((await resultOf<LogEntry>("mm_logs_get", historical)).serverId, historicalId)
const historicalContext = await resultOf<LogContext>("mm_logs_context_get", historical)
assert.equal(historicalContext.entry.id, historical.id)
assert.deepEqual([...historicalContext.before, ...historicalContext.after], [])
for (const name of ["mm_logs_get", "mm_logs_context_get"]) {
const foreignId = idAt(hiddenId, 0)
const foreign = await call(name, { serverId: 1, id: foreignId })
assert.equal(foreign.isError, true)
assert.match(JSON.stringify(foreign), /NOT_FOUND/)
assert.doesNotMatch(JSON.stringify(foreign), new RegExp(marker))
const outOfScope = await call(name, { serverId: hiddenId, id: foreignId })
assert.equal(outOfScope.isError, true)
assert.match(JSON.stringify(outOfScope), /SERVER_DENIED/)
}
const summary = await resultOf<{ total: number; levels: { info: number }; states: { serverId: number }[] }>(
"mm_logs_summary_get", { from, to, q: marker },
)
assert.equal(summary.total, 26)
assert.equal(summary.levels.info, 26)
assert.ok(summary.states.some(state => state.serverId === historicalId))
assert.ok(summary.states.every(state => state.serverId === 1 || state.serverId === historicalId))
assert.ok(!("settings" in summary))
console.log("✓ MCP logs SDK: scoped entries, same-server context and historical summary")
const portalJwt = (subject: string, permissions: string[]) =>
new SignJWT({ sub: subject, apps: ["mm"], permissions })
.setProtectedHeader({ alg: "HS256" }).setIssuer(env.authIssuer).setExpirationTime("5m")
.sign(new TextEncoder().encode(env.jwtSecret))
const ownerToken = await portalJwt(actor.id, actor.permissions)
const otherToken = await portalJwt(`${actor.id}-other`, ["mm:mcp:read", "mm:servers:read"])
const restrictedToken = await portalJwt(actor.id, ["mm:mcp:read"])
type Export = { artifactId: string; downloadPath: string; expiresInSeconds: number; rows: number; sha256: string }
const exportLogs = (format: "csv" | "json") => resultOf<Export>("mm_logs_export", { from, to, q: marker, format })
const download = (url: string, token?: string) =>
app.inject({ url, headers: token ? { authorization: `Bearer ${token}` } : {} })
const csv = await exportLogs("csv")
assert.equal(csv.rows, 26)
assert.equal(csv.expiresInSeconds, 900)
assert.equal(csv.downloadPath, `/api/mcp/artifacts/${csv.artifactId}`)
const response = await download(csv.downloadPath, ownerToken)
assert.equal(response.statusCode, 200)
assert.equal(response.headers["cache-control"], "private, no-store")
assert.match(String(response.headers["content-type"]), /^text\/csv/)
assert.equal(createHash("sha256").update(response.rawPayload).digest("hex"), csv.sha256)
assert.ok(!response.body.includes("hidden-row"))
assert.equal((await download(csv.downloadPath)).statusCode, 401)
assert.equal((await download(csv.downloadPath, access.token)).statusCode, 401)
assert.equal((await download(csv.downloadPath, otherToken)).statusCode, 404)
assert.equal((await download(csv.downloadPath, restrictedToken)).statusCode, 403)
const json = await exportLogs("json")
await pool.query("UPDATE mcp_artifacts SET expires_at=now()-interval '1 second' WHERE id=$1 AND connection_id=$2", [json.artifactId, access.connection.id])
assert.equal((await download(json.downloadPath, ownerToken)).statusCode, 404)
await setConnectionStatus(access.connection.id, "revoked", actor)
assert.equal((await download(csv.downloadPath, ownerToken)).statusCode, 403)
console.log("✓ MCP logs export: CSV owner download, scope, non-cacheable response, expiry and revocation")
} finally {
await client.close()
await pool.query("DELETE FROM router_log_entries WHERE message=ANY($1::text[])", [[marker, `${marker} hidden-row`]])
await pool.query("DELETE FROM router_log_collection_state WHERE server_id=ANY($1::int[]) AND server_name=$2", [[historicalId, hiddenId], marker])
await pool.query("DELETE FROM servers WHERE id=$1 AND name=$2", [historicalId, marker])
}
}
export async function verifyLogsFollowSdkCancellation(app: FastifyInstance, endpoint: string, actor: AuthUser) {
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({
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" }
}
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])
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
@@ -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)
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)
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[]) {
try { return { requiresHuman: false, managementChannels: await Promise.all([...new Set(serverIds)].map(serverId => verifyManagementChannel(serverId))) } }
@@ -6,6 +6,7 @@ import { mcpConnectionCreateSchema } from "@mmapp/contracts/mcp"
import { pool } from "../../db/index.js"
import type { AuthUser } from "../../lib/permissions.js"
import { managedComment } from "../../managed-markers.js"
import { updateServer } from "../servers/service/servers-service.js"
import type { ChangeEvidence } from "./action-types.js"
import { findAction } from "./actions.js"
import { readRosSnapshot } from "./resources.js"
@@ -18,13 +19,16 @@ import { authenticateMcpToken, createMcpConnection, setConnectionStatus } from "
export async function acceptNativeConservation(actor: AuthUser) {
const serverId = 198
const filterPath = "/ip/firewall/filter", rulePath = "/routing/rule"
const routePaths = ["/ip/route", "/ipv6/route"], settingsPaths = ["/ip/settings", "/ipv6/settings"]
const base = () => ["a", "b", "c"].map(id => ({ ".id": `*${id}`, chain: "fixture", action: "accept", comment: id, disabled: "true" }))
const ruleBase = () => ["a", "b", "c"].map(id => ({ ".id": `*${id}`, action: "lookup", table: "main", comment: id, disabled: "true" }))
let catalogs: Record<string, Record<string, string>[]> = {}, mode = "normal", writes = 0, next = 0, polls = 0
let connectionId: string | undefined
let afterIntent: (() => Promise<void>) | undefined, afterVerified: (() => Promise<void>) | undefined
let afterRouteRead: (() => Promise<void>) | undefined
const query = pool.query.bind(pool)
const commands: Array<{ method: string; path: string }> = []
const readPaths = new Set<string>()
const reset = () => {
mode = "normal"
catalogs = { [filterPath]: base(), [rulePath]: ruleBase(), "/routing/table": [{ ".id": "*main", name: "main", fib: "true", disabled: "false" }], "/ip/settings": [{ "rp-filter": "no", "ipv4-multipath-hash-policy": "l3" }], "/ip/route": [] }
@@ -33,8 +37,18 @@ export async function acceptNativeConservation(actor: AuthUser) {
const router = createServer(async (req, res) => {
const path = decodeURIComponent((req.url ?? "").replace(/^\/rest/, ""))
res.setHeader("content-type", "application/json")
if (req.method === "GET") { polls++; res.end(JSON.stringify(catalogs[path] ?? [])); return }
const parent = [filterPath, rulePath, "/ip/route", "/ip/settings"].find(value => path === value || path.startsWith(`${value}/`))
if (req.method === "GET") {
polls++; readPaths.add(path)
const body = JSON.stringify(catalogs[path] ?? [])
if (path === "/ip/route" && afterRouteRead) {
const hook = afterRouteRead
afterRouteRead = undefined
await hook()
}
res.end(body)
return
}
const parent = [filterPath, rulePath, ...routePaths, ...settingsPaths].find(value => path === value || path.startsWith(`${value}/`))
if (!parent || !["PUT", "PATCH", "DELETE"].includes(req.method ?? "")) { res.statusCode = 404; res.end("{}"); return }
let text = ""; for await (const chunk of req) text += chunk
const fields = text ? JSON.parse(text) as Record<string, string> : {}
@@ -42,11 +56,11 @@ export async function acceptNativeConservation(actor: AuthUser) {
writes++; commands.push({ method: req.method!, path })
if (mode !== "ignored") {
if (req.method === "PUT") rows.push({ ".id": id, ...fields })
if (req.method === "PATCH") Object.assign(parent === "/ip/settings" ? rows[0] : rows.find(row => row[".id"] === id)!, fields)
if (req.method === "PATCH") Object.assign(settingsPaths.includes(parent) ? rows[0] : rows.find(row => row[".id"] === id)!, fields)
if (req.method === "DELETE") catalogs[parent] = rows.filter(row => row[".id"] !== id)
}
const after = catalogs[parent]
if (parent === "/ip/route") for (const row of after) row.active = Number(row.distance) === Math.min(...after.map(item => Number(item.distance))) ? "true" : "false"
if (routePaths.includes(parent)) for (const row of after) row.active = Number(row.distance) === Math.min(...after.map(item => Number(item.distance))) ? "true" : "false"
if (mode === "two-new") after.push({ ".id": `*extra${next}`, ...fields })
if (mode === "collateral-update") after.find(row => row[".id"] === "*b")!.comment = "external"
if (mode === "collateral-remove") catalogs[parent] = after.filter(row => row[".id"] !== "*b")
@@ -80,7 +94,7 @@ export async function acceptNativeConservation(actor: AuthUser) {
try {
assert.equal((await query("SELECT count(*)::int AS n FROM servers WHERE id=$1", [serverId])).rows[0].n, 0)
await query("INSERT INTO servers(id,name,host,port,use_ssl,password) VALUES($1,'native-conservation','127.0.0.1',$2,false,'fixture-only')", [serverId, (router.address() as { port: number }).port])
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Native conservation", mode: "approval", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read", "mm:network:write"], scope: { serverIds: [serverId], operationIds: ["ros.ip_filter.create", "ros.ip_filter.update", "ros.ip_filter.delete", "ros.routing_rule.update", "ros.ipv4_route.update", "ros.ip_settings.update", "routing.ecmp.update", "routing.rpf.update", "operations.recover", "operations.reconcile"], allowGlobal: false }, limits: { callsPerMinute: 1000, mutationsPerHour: 1000 } }), actor)
const access = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Native conservation", mode: "approval", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read", "mm:network:write"], scope: { serverIds: [serverId], operationIds: ["ros.ip_filter.create", "ros.ip_filter.update", "ros.ip_filter.delete", "ros.routing_rule.update", "ros.ipv4_route.update", "ros.ip_settings.update", "routing.ecmp.update", "routing.rpf.update", "operations.recover", "operations.reconcile", "operations.read"], allowGlobal: false }, limits: { callsPerMinute: 1000, mutationsPerHour: 1000 } }), actor)
connectionId = access.connection.id
let principal = await authenticateMcpToken(access.token)
const prepare = (action: "create" | "update" | "delete", resource = "ip_filter") => invokeMcpTool(principal, `mm_${resource}_${action}_plan`, { serverId, ...(action === "create" ? {} : { id: "*a" }), fields: action === "create" ? { chain: "fixture", action: "accept", comment: "new", disabled: true } : action === "update" ? { comment: "changed" } : {} }) as Promise<{ planId: string }>
@@ -204,13 +218,233 @@ export async function acceptNativeConservation(actor: AuthUser) {
const routeBaseline = [0, 1].map(index => ({ ".id": `*route${index}`, "dst-address": "192.0.2.0/24", "routing-table": "main", gateway: `198.18.0.${index + 1}`, "immediate-gw": `198.18.0.${index + 1}%fixture`, scope: "0", distance: String((index + 1) * 10), active: index === 0 ? "true" : "false", disabled: "false", dynamic: "false", comment: managedComment("conservation") }))
catalogs["/ip/route"] = structuredClone(routeBaseline)
const originalSettings = structuredClone(catalogs["/ip/settings"])
const equal = await invokeMcpTool(principal, "mm_ecmp_plan", { serverId, family: "ipv4", routeIds: ["*route0", "*route1"], distance: 10, hashPolicy: "l4" }) as { planId: string }
const ecmpArgs = { serverId, family: "ipv4", routeIds: ["*route0", "*route1"], distance: 10, hashPolicy: "l4" }
const beforeEcmp = writes
const observer = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "ECMP interface-only preflight", mode: "observer", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read"], scope: { serverIds: [serverId], operationIds: ["routing.ecmp.preflight.read"], allowGlobal: false } }), actor)
try {
const reader = await authenticateMcpToken(observer.token)
const { distance, hashPolicy, ...preflightInput } = ecmpArgs
catalogs["/ip/firewall/connection/tracking"] = [{ enabled: "auto" }]
catalogs["/ip/route"] = routeBaseline.map((row, index) => ({ ...row, gateway: `gre${index}`, "immediate-gw": `gre${index}` }))
catalogs["/interface"] = [0, 1].map(index => ({ ".id": `*gre${index}`, name: `gre${index}` }))
const before = polls
const result = await invokeMcpTool(reader, "mm_ecmp_preflight_get", preflightInput) as { configurationObserved: boolean; networkValidation: string; sources: { interfaces: { count: number } } }
assert.equal(result.configurationObserved, true)
assert.equal(result.networkValidation, "not-performed")
assert.equal(result.sources.interfaces.count, 2)
assert.equal(polls - before, 11)
assert.equal(writes, beforeEcmp)
} finally {
await query("DELETE FROM mcp_connections WHERE id=$1", [observer.connection.id])
catalogs["/ip/route"] = structuredClone(routeBaseline)
}
catalogs["/ip/route"] = routeBaseline.map((row, index) => ({ ...row, gateway: `gre${index}`, "immediate-gw": `gre${index}` }))
const namedHopPlan = await invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs) as { planId: string; changes: unknown }
assert.ok(!JSON.stringify(namedHopPlan.changes).includes("nextHopInterface"))
const inventory = structuredClone(catalogs["/interface"])
afterIntent = async () => { catalogs["/interface"][0][".id"] = "*replacement" }
const replacedHop = await apply(namedHopPlan.planId)
assert.equal(afterIntent, undefined)
assert.equal(replacedHop.state, "rejected")
assert.equal(replacedHop.error_code, "STATE_CONFLICT")
assert.equal(writes, beforeEcmp)
catalogs["/interface"] = inventory
catalogs["/ip/route"] = structuredClone(routeBaseline)
console.log("ECMP interface-only next-hop: same-name replacement after intent refused before PATCH; native identity stays private")
afterRouteRead = async () => { catalogs["/ip/route"][1].distance = "25" }
await assert.rejects(invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs), { code: "STATE_CONFLICT" })
assert.equal(afterRouteRead, undefined)
catalogs["/ip/route"] = structuredClone(routeBaseline)
afterRouteRead = async () => {
await query("UPDATE servers SET password='source-aba-fixture' WHERE id=$1", [serverId])
await query("UPDATE servers SET password='fixture-only' WHERE id=$1", [serverId])
}
await assert.rejects(invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs), { code: "STATE_CONFLICT" })
assert.equal(afterRouteRead, undefined)
assert.equal(writes, beforeEcmp)
catalogs["/ip/route"][1]["immediate-gw"] = routeBaseline[0]["immediate-gw"]
await assert.rejects(invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs), { code: "STATE_CONFLICT" })
assert.equal(writes, beforeEcmp)
catalogs["/ip/route"] = structuredClone(routeBaseline)
const fibTable = catalogs["/routing/table"]
for (const rows of [
[{ ...fibTable[0], fib: "false" }],
[{ ...fibTable[0], fib: "" }],
[{ ".id": "*main", name: "main" }],
[{ ...fibTable[0], disabled: "true" }],
[{ ...fibTable[0], invalid: "true" }],
[...fibTable, { ...fibTable[0], ".id": "*duplicate" }],
]) {
catalogs["/routing/table"] = rows
await assert.rejects(invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs), { code: "STATE_CONFLICT" })
assert.equal(writes, beforeEcmp)
}
catalogs["/routing/table"] = fibTable
const staleFib = await invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs) as { planId: string }
await approvePlan(staleFib.planId, actor, true)
catalogs["/routing/table"] = [{ ...fibTable[0], fib: "false" }]
assert.equal((await wait((await applyPlan(principal, staleFib.planId, randomUUID())).id)).state, "rejected")
assert.equal(writes, beforeEcmp)
catalogs["/routing/table"] = fibTable
console.log("ECMP FIB SQL/HTTP: inactive, unconfirmed and ambiguous tables refused; drift rejected before native write")
const equal = await invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs) as { planId: string }
const beforeEqual: number = writes, equalResult = await apply(equal.planId)
assert.equal(equalResult.state, "applied"); assert.equal(writes, beforeEqual + 3)
assert.equal(catalogs["/ip/settings"][0]["ipv4-multipath-hash-policy"], "l4")
const unequal = await invokeMcpTool(principal, "mm_operation_recovery_plan", { operationId: equalResult.operation.id }) as { planId: string }
assert.equal((await apply(unequal.planId)).state, "applied"); assert.equal(writes, beforeEqual + 6)
assert.deepEqual(catalogs["/ip/route"], routeBaseline); assert.deepEqual(catalogs["/ip/settings"], originalSettings)
const lateEcmp = await invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs) as { planId: string }
afterIntent = async () => { catalogs["/ip/route"][1]["immediate-gw"] = "" }
const lateEcmpResult = await apply(lateEcmp.planId)
assert.equal(afterIntent, undefined)
assert.equal(lateEcmpResult.state, "rejected")
assert.equal(lateEcmpResult.error_code, "STATE_CONFLICT")
assert.equal(writes, beforeEqual + 6)
catalogs["/ip/route"] = structuredClone(routeBaseline)
const sourceChanged = await invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs) as { planId: string }
let lateRouteReads = 0
const rotateDuringHealthRead = async () => {
if (++lateRouteReads === 1) {
// The prefix reads the configuration first; the next GET checks the live next-hop.
afterRouteRead = rotateDuringHealthRead
return
}
await assert.rejects(updateServer(serverId, { password: "concurrent-update-fixture" }), { statusCode: 409 })
await query("UPDATE servers SET password='health-source-aba-fixture' WHERE id=$1", [serverId])
await query("UPDATE servers SET password='fixture-only' WHERE id=$1", [serverId])
}
afterIntent = async () => { afterRouteRead = rotateDuringHealthRead }
const staleHealthSource = await apply(sourceChanged.planId)
assert.equal(lateRouteReads, 2)
assert.equal(staleHealthSource.state, "rejected")
assert.equal(staleHealthSource.error_code, "STATE_CONFLICT")
assert.equal(writes, beforeEqual + 6)
const beforeNextStep = await invokeMcpTool(principal, "mm_ecmp_plan", ecmpArgs) as { planId: string }
afterVerified = async () => { catalogs["/ip/route"][1]["immediate-gw"] = "" }
const partialEcmp = await apply(beforeNextStep.planId)
assert.equal(partialEcmp.state, "partial")
assert.equal(writes, beforeEqual + 7)
for (const [result, confirmedWrites] of [[lateEcmpResult, 0], [partialEcmp, 1]] as const) {
const stored = await invokeMcpTool(principal, "mm_operation_get", { operationId: result.operation.id }) as {
state: string; result: { failedStepDispatched: boolean; confirmedWrites: number; needsReview: boolean }
}
assert.equal(stored.state, result.state)
assert.equal(stored.result.failedStepDispatched, false)
assert.equal(stored.result.confirmedWrites, confirmedWrites)
assert.equal(stored.result.needsReview, confirmedWrites > 0)
const { rows: [stop] } = await pool.query("SELECT payload FROM mcp_audit WHERE action='operation.stop' AND payload->>'operationId'=$1", [result.operation.id])
assert.equal(stop.payload.failedStepDispatched, false)
}
catalogs["/ip/route"] = structuredClone(routeBaseline)
const collapsed = await invokeMcpTool(principal, "mm_ecmp_plan", { ...ecmpArgs, hashPolicy: undefined }) as { planId: string }
afterVerified = async () => {
afterVerified = async () => { catalogs["/ip/route"][1]["immediate-gw"] = routeBaseline[0]["immediate-gw"] }
}
const collapsedResult = await apply(collapsed.planId)
assert.equal(collapsedResult.state, "partial")
assert.equal(collapsedResult.verification, "failed")
assert.equal(writes, beforeEqual + 9)
assert.equal(afterVerified, undefined)
catalogs["/ip/route"] = structuredClone(routeBaseline)
console.log("ECMP SQL/HTTP: shared adjacency refuses preparation and post-write collapse fails verification")
const v6Baseline = routeBaseline.map((row, index) => ({
...row, "dst-address": "2001:db8::/64", gateway: `2001:db8:1::${index + 1}`,
"immediate-gw": `fe80::${index + 1}%fixture`,
}))
catalogs["/ipv6/route"] = structuredClone(v6Baseline)
const v6Settings = [{ "multipath-hash-policy": "l3" }]
catalogs["/ipv6/settings"] = structuredClone(v6Settings)
const v6Mangle = "/ipv6/firewall/mangle"
catalogs[v6Mangle] = [{ ".id": "*v6mark", action: "mark-routing", disabled: "false", "new-routing-mark": "NEVER_NATIVE_POLICY_MARK", bytes: "0", packets: "0" }]
const v6Args = { ...ecmpArgs, family: "ipv6" }
readPaths.clear()
for (const [path, field] of [[v6Mangle, "new-routing-mark"], [rulePath, "min-prefix"]]) {
const stale = await invokeMcpTool(principal, "mm_ecmp_plan", v6Args) as { planId: string; changes: { snapshots: Record<string, unknown>[] } }
const policy = stale.changes.snapshots.find(snapshot => snapshot.resource === "routing_policy:ipv6_mangle")
assert.deepEqual(policy, { serverId, resource: "routing_policy:ipv6_mangle", rowCount: 1 })
assert.ok(!JSON.stringify(stale).includes("NEVER_NATIVE_POLICY_MARK"))
await approvePlan(stale.planId, actor, true)
const original = catalogs[path], before = writes
catalogs[path] = [{ ...original[0], [field]: "17" }, ...original.slice(1)]
assert.equal((await wait((await applyPlan(principal, stale.planId, randomUUID())).id)).state, "rejected")
assert.equal(writes, before)
catalogs[path] = original
}
const v6Plan = await invokeMcpTool(principal, "mm_ecmp_plan", v6Args) as { planId: string }
catalogs[v6Mangle][0].bytes = "1500"
catalogs[v6Mangle][0].packets = "1"
const v6Result = await apply(v6Plan.planId)
assert.equal(v6Result.state, "applied")
assert.ok(catalogs["/ipv6/route"].every(row => row.active === "true" && row.distance === "10"))
assert.equal(catalogs["/ipv6/settings"][0]["multipath-hash-policy"], "l4")
const inverseArgs = { operationId: v6Result.operation.id }
catalogs[v6Mangle][0]["new-routing-mark"] = "external-mark"
await assert.rejects(invokeMcpTool(principal, "mm_operation_recovery_plan", inverseArgs), { code: "STATE_CONFLICT" })
catalogs[v6Mangle][0]["new-routing-mark"] = "NEVER_NATIVE_POLICY_MARK"
const v6Inverse = await invokeMcpTool(principal, "mm_operation_recovery_plan", inverseArgs) as { planId: string }
assert.ok(!JSON.stringify(v6Inverse).includes("NEVER_NATIVE_POLICY_MARK"))
assert.equal((await apply(v6Inverse.planId)).state, "applied")
assert.deepEqual(catalogs["/ipv6/route"], v6Baseline)
assert.deepEqual(catalogs["/ipv6/settings"], v6Settings)
assert.ok(![...readPaths].some(path => path === "/ip/settings" || path.startsWith("/ip/firewall/")))
console.log("ECMP IPv6 SQL/HTTP: family-specific reads, policy drift refusal, route/hash apply and inverse")
const rpfArgs = { serverId, mode: "strict", acknowledgeAsymmetry: true }
const manglePath = "/ip/firewall/mangle"
const beforeRpf = writes, beforeIpv6 = polls
await assert.rejects(invokeMcpTool(principal, "mm_rpf_plan", { ...rpfArgs, family: "ipv6" }), { code: "UNSUPPORTED_CAPABILITY" })
assert.equal(polls, beforeIpv6)
catalogs[rulePath][0].disabled = "false"
await assert.rejects(invokeMcpTool(principal, "mm_rpf_plan", rpfArgs), { code: "STATE_CONFLICT" })
const strictSettings = { serverId, fields: { "rp-filter": "strict", "ipv4-multipath-hash-policy": "l4" } }
await assert.rejects(invokeMcpTool(principal, "mm_ip_settings_update_plan", strictSettings), { code: "STATE_CONFLICT" })
catalogs[rulePath][0].disabled = "true"
catalogs[manglePath] = [{ ".id": "*mark", chain: "prerouting", action: "mark-routing", disabled: "false", "new-routing-mark": "fixture-mark" }]
await assert.rejects(invokeMcpTool(principal, "mm_rpf_plan", rpfArgs), { code: "STATE_CONFLICT" })
const loose = await invokeMcpTool(principal, "mm_rpf_plan", { ...rpfArgs, mode: "loose" }) as { changes: { warnings: string[] } }
assert.ok(loose.changes.warnings.some(warning => warning.includes("policy routing")))
catalogs[manglePath][0].disabled = "true"
const strict = await invokeMcpTool(principal, "mm_rpf_plan", rpfArgs) as { planId: string }
const genericStrict = await invokeMcpTool(principal, "mm_ip_settings_update_plan", strictSettings) as { planId: string }
await approvePlan(strict.planId, actor, true)
await approvePlan(genericStrict.planId, actor, true)
catalogs[manglePath][0]["new-routing-mark"] = "changed-after-approval"
const staleRpf = await applyPlan(principal, strict.planId, randomUUID())
assert.equal((await wait(staleRpf.id)).state, "rejected")
assert.equal((await wait((await applyPlan(principal, genericStrict.planId, randomUUID())).id)).state, "rejected")
assert.equal(writes, beforeRpf)
catalogs[manglePath] = []
catalogs["/ip/settings"][0]["rp-filter"] = "loose"
const disableRpf = await invokeMcpTool(principal, "mm_rpf_plan", { ...rpfArgs, mode: "disabled" }) as { planId: string }
assert.equal((await apply(disableRpf.planId)).state, "applied")
assert.deepEqual(catalogs["/ip/settings"], originalSettings)
catalogs["/ip/settings"][0]["rp-filter"] = "strict"
const leaveStrict = await invokeMcpTool(principal, "mm_rpf_plan", { ...rpfArgs, mode: "no" }) as { planId: string }
const leftStrict = await apply(leaveStrict.planId)
assert.equal(leftStrict.state, "applied")
const inverseRpfArgs = { operationId: leftStrict.operation.id }
const beforeInverseRpf = writes
catalogs[manglePath] = [{ ".id": "*late-mark", action: "mark-routing", disabled: "false" }]
await assert.rejects(invokeMcpTool(principal, "mm_operation_recovery_plan", inverseRpfArgs), { code: "STATE_CONFLICT" })
catalogs[manglePath] = []
const legacyRpf = await invokeMcpTool(principal, "mm_operation_recovery_plan", inverseRpfArgs) as { planId: string }
const legacyRpfEvidence = await evidence(legacyRpf.planId)
legacyRpfEvidence.snapshots = legacyRpfEvidence.snapshots.filter(snapshot => !snapshot.resource.startsWith("routing_policy:"))
await replace(legacyRpf.planId, legacyRpfEvidence)
await approvePlan(legacyRpf.planId, actor, true)
await assert.rejects(applyPlan(principal, legacyRpf.planId, randomUUID()), { code: "PLAN_EXPIRED" })
const staleInverseRpf = await invokeMcpTool(principal, "mm_operation_recovery_plan", inverseRpfArgs) as { planId: string }
await approvePlan(staleInverseRpf.planId, actor, true)
catalogs[manglePath] = [{ ".id": "*late-mark", action: "mark-routing", disabled: "false" }]
assert.equal((await wait((await applyPlan(principal, staleInverseRpf.planId, randomUUID())).id)).state, "rejected")
assert.equal(writes, beforeInverseRpf)
catalogs[manglePath] = []
const safeInverseRpf = await invokeMcpTool(principal, "mm_operation_recovery_plan", inverseRpfArgs) as { planId: string }
assert.equal((await apply(safeInverseRpf.planId)).state, "applied")
assert.equal(catalogs["/ip/settings"][0]["rp-filter"], "strict")
catalogs["/ip/settings"] = structuredClone(originalSettings)
console.log("RPF inverse SQL/HTTP: active policy, legacy plans and post-approval drift refused; exact strict restoration under verified policy")
console.log("RPF SQL/HTTP: explicit IPv6 refusal, strict policy admission and drift, loose warning, disabled alias")
for (const [tool, args, path] of [
["mm_rpf_plan", { serverId, mode: "loose", acknowledgeAsymmetry: true }, "/ip/settings"],
["mm_ip_settings_update_plan", { serverId, fields: { "rp-filter": "loose" } }, "/ip/settings"],
@@ -229,6 +463,43 @@ export async function acceptNativeConservation(actor: AuthUser) {
const booleanResult = await apply(booleanPlan.planId); assert.equal(booleanResult.state, "applied")
const booleanInverse = await invokeMcpTool(principal, "mm_operation_recovery_plan", { operationId: booleanResult.operation.id }) as { planId: string }
assert.equal((await apply(booleanInverse.planId)).state, "applied"); assert.deepEqual(catalogs[filterPath], booleanBaseline)
catalogs["/ip/route"] = routeBaseline.map((row, index) => ({ ...row, gateway: `gre${index}`, "immediate-gw": `gre${index}` }))
const stableHopPlan = await invokeMcpTool(principal, "mm_ecmp_plan", { ...ecmpArgs, hashPolicy: undefined }) as { planId: string }
const beforeStableHops = writes
const stableHops = await apply(stableHopPlan.planId)
assert.equal(stableHops.state, "applied")
assert.equal(writes, beforeStableHops + 2)
assert.ok(catalogs["/ip/route"].every(row => row.distance === "10" && row.active === "true"))
console.log("ECMP interface-only next-hop: unchanged interface IDs verified before both writes and after apply")
const namedHopInverseArgs = { operationId: stableHops.operation.id }
const plansBeforeReplacement = (await query("SELECT count(*)::int AS count FROM mcp_plans WHERE connection_id=$1", [connectionId])).rows[0].count
catalogs["/interface"][0][".id"] = "*replaced-before-preparation"
await assert.rejects(invokeMcpTool(principal, "mm_operation_recovery_plan", namedHopInverseArgs), { code: "STATE_CONFLICT" })
assert.equal((await query("SELECT count(*)::int AS count FROM mcp_plans WHERE connection_id=$1", [connectionId])).rows[0].count, plansBeforeReplacement)
catalogs["/interface"][0][".id"] = "*gre0"
const oldHopInverse = await invokeMcpTool(principal, "mm_operation_recovery_plan", namedHopInverseArgs) as { planId: string }
const oldHopEvidence = await evidence(oldHopInverse.planId)
delete oldHopEvidence.healthChecks
await replace(oldHopInverse.planId, oldHopEvidence)
const beforeInverseHops = writes
const oldHopResult = await apply(oldHopInverse.planId)
assert.equal(oldHopResult.state, "rejected")
assert.equal(oldHopResult.error_code, "PLAN_EXPIRED")
assert.equal(writes, beforeInverseHops)
const staleHopInverse = await invokeMcpTool(principal, "mm_operation_recovery_plan", namedHopInverseArgs) as { planId: string; changes: unknown }
assert.ok(!JSON.stringify(staleHopInverse.changes).includes("nextHopInterface"))
afterIntent = async () => { catalogs["/interface"][0][".id"] = "*inverse-replacement" }
const refusedInverse = await apply(staleHopInverse.planId)
assert.equal(afterIntent, undefined)
assert.equal(refusedInverse.state, "rejected")
assert.equal(refusedInverse.error_code, "STATE_CONFLICT")
assert.equal(writes, beforeInverseHops)
catalogs["/interface"][0][".id"] = "*gre0"
const safeHopInverse = await invokeMcpTool(principal, "mm_operation_recovery_plan", namedHopInverseArgs) as { planId: string }
assert.equal((await apply(safeHopInverse.planId)).state, "applied")
assert.equal(writes, beforeInverseHops + 2)
assert.deepEqual(catalogs["/ip/route"].map(row => row.distance), ["10", "20"])
console.log("ECMP inverse: replaced interface refused before PATCH; intact interface restores unequal costs")
console.log("Native multi-catalog inverse SQL/HTTP: two route PATCHes plus singleton hash policy, exact reverse prefix, RPF/raw singleton, numeric zero, false and empty-comment originals preserved")
console.log("Native conservation SQL/HTTP: C/U/D and safe inverses; assigned-ID, membership, field and order failures never VERIFIED/replayed; honest two-step prefix stops before second intent; late source/dependency/protection/policy fence zero writes; stale operation revision refused. No CHR/opaque-field/forwarding claim.")
} finally {
@@ -11,7 +11,7 @@ const creates = rosResources.filter(resource => !resource.readOnly && resource.c
assert.equal(creates.length, 30)
for (const resource of creates) {
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)
for (const id of ["*OLD", undefined, null, ""]) {
const rejected = action.schema.safeParse({ serverId: 1, fields: {}, id })
@@ -5,6 +5,8 @@ await prepareOwnedBandwidthState()
const { pool, closePool } = await import("../../db/index.js")
try {
const { applySqlMigrations } = await import("../../db/migrate.js"); await applySqlMigrations(pool)
const { acceptOperationReplayTombstones } = await import("./operation-replay-tombstones.sql-helper.js")
await acceptOperationReplayTombstones()
const { acceptOwnedMcpOperationRoot } = await import("./operation-execution-origin.sql-helper.js")
console.log("Owned actual MCP operation root:", JSON.stringify(await acceptOwnedMcpOperationRoot()))
} finally { await closePool() }
@@ -9,6 +9,7 @@ import { performance } from "node:perf_hooks"
import type { QueryConfig, PoolClient } from "pg"
import { mcpTemporaryGrantSchema, type McpObjectRef } from "@mmapp/contracts/mcp"
import { pool } from "../../db/index.js"
import { sharedDatabaseGate } from "../../services/shared-database-gate.js"
import { assertNoConfigurationLockContext } from "../../services/config-mutation-lock.js"
import { assertNoOperationConfigurationLockContext } from "../../services/operation-config-lock-lease.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 work = async () => {
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")
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
@@ -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 }) }
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. */
export async function retireMcpOperationAdmission(admission: unknown): Promise<void> {
const entry = admission && typeof admission === "object" ? admissions.get(admission) : undefined
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.
* 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)
} finally {
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> {
@@ -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()
}
@@ -32,13 +32,13 @@ assert.equal(rebuildReconcile.operation.effect, "read")
assert.equal(rebuildReconcile.operation.scope, "global")
assert.deepEqual(rebuildReconcile.operation.requiredPermissions, ["mm:traffic:write"])
assert.throws(() => rebuildReconcile.schema.parse({ jobId: "caller" }))
assert.equal(action.operation.version, 9)
assert.equal(findMcpOperation(action.operation.id)?.version, 9)
assert.equal(action.operation.version, 10)
assert.equal(findMcpOperation(action.operation.id)?.version, 10)
assert.throws(() => action.schema.parse({ serverId: 1, expectedServerProof: "forged-source" }))
assert.throws(() => action.schema.parse({ serverId: 1, version: 1 }))
assert.equal(action.schema.parse({ serverId: 1 }).family, "ipv4")
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_update_plan").operation.version, 4)
assert.equal(findAction("mm_ospf_template_delete_plan").operation.version, 3)
@@ -49,17 +49,17 @@ 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_create_plan").operation.version, 3)
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_update_plan").operation.version, 3)
assert.equal(findAction("mm_certificate_delete_plan").operation.version, 3)
for (const [tool, version] of [["mm_route_optimizer_plan", 7], ["mm_route_profile_save_plan", 4], ["mm_route_path_verify", 2], ["mm_operation_recovery_plan", 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", 5], ["mm_route_path_verify", 2], ["mm_operation_recovery_plan", 8], ["mm_ecmp_plan", 7], ["mm_rpf_plan", 4], ["mm_ip_settings_update_plan", 3], ["mm_vrf_plan", 4], ["mm_firewall_rule_update_plan", 2]] as const) {
const routed = findAction(tool)
assert.equal(routed.operation.version, version)
assert.equal(findMcpOperation(routed.operation.id)?.version, version)
}
assert.throws(() => findAction("mm_routing_measurement_plan").schema.parse({ measurement: { srcServerId: 1, dstServerId: 2, srcInterface: "gre", dstInterface: "gre", family: "ipv4", expectedEndpoints: [] } }))
console.log("MCP operation revisions: OSPF v9 and template order revisions agree; clients cannot inject revision or private source proof")
console.log("MCP operation revisions: OSPF v10 and template order revisions agree; clients cannot inject revision or private source proof")
assert.equal(findAction("mm_ipsec_peer_create_plan").operation.version, 5)
assert.equal(findAction("mm_certificate_update_plan").operation.version, 4)
@@ -0,0 +1,314 @@
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,getMcpSettings}=await import("./store.js")
const {invokeMcpTool,approvePlan,applyPlan}=await import("./runtime.js")
const {OrdinaryRetirementCommitUnknown,retireOrdinaryMcpOperations}=await import("./ordinary-operation-retirement.js")
const {PARTITION_WRITER_ADVISORY_KEY}=await import("../../services/partition-write-transaction.js")
const {tickMcpWorkers}=await import("./workers.js")
const {pauseSchedulerJobsAndWait,resumeSchedulerJobs}=await import("../../services/scheduler-running.js")
const {Client}=await import("pg")
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
async function assertWorkerUsesCommittedRetention() {
const actorId=randomUUID()
const original=await getMcpSettings()
await pool.query("UPDATE mcp_settings SET requests_retention_days=1,audit_retention_days=30 WHERE id=1")
const settingsWriter=await pool.connect()
let tick:Promise<void>|undefined,settled=false,transactionOpen=false
try{
await pool.query(`INSERT INTO mcp_requests(id,actor_id,tool,status,created_at) VALUES
($1,$5,'protected','completed',now()-interval '2 days'),
($2,$5,'active','running',now()-interval '33 days'),
($3,$5,'recent','completed',now()-interval '1 hour'),
($4,$5,'expired','completed',now()-interval '33 days')`,
[randomUUID(),randomUUID(),randomUUID(),randomUUID(),actorId])
await pool.query(`INSERT INTO mcp_audit(actor_id,action,created_at) VALUES
($1,'protected',now()-interval '31 days'),($1,'recent',now()-interval '1 hour'),
($1,'expired',now()-interval '183 days')`,[actorId])
await settingsWriter.query("BEGIN")
transactionOpen=true
const {pid}=(await settingsWriter.query("SELECT pg_backend_pid() AS pid")).rows[0]
await settingsWriter.query("UPDATE mcp_settings SET requests_retention_days=30,audit_retention_days=180 WHERE id=1")
tick=tickMcpWorkers().finally(()=>{settled=true})
void tick.catch(()=>{})
let waiting=false
for(const deadline=performance.now()+3000;!settled&&!waiting&&performance.now()<deadline;){
waiting=(await pool.query(`SELECT EXISTS(SELECT 1 FROM pg_stat_activity
WHERE $1=ANY(pg_blocking_pids(pid)) AND query LIKE '%FROM mcp_settings WHERE id=1 FOR SHARE%') AS waiting`,[pid])).rows[0].waiting
if(!waiting)await delay(10)
}
assert(waiting,"retention worker must wait for the policy update before choosing candidates")
assert.deepEqual(await getMcpSettings(),{...original,requestsRetentionDays:1,auditRetentionDays:30})
await settingsWriter.query("COMMIT")
transactionOpen=false
await tick
assert.deepEqual(await getMcpSettings(),{...original,requestsRetentionDays:30,auditRetentionDays:180})
assert.deepEqual((await pool.query("SELECT tool,status FROM mcp_requests WHERE actor_id=$1 ORDER BY tool",[actorId])).rows,
[{tool:"active",status:"running"},{tool:"protected",status:"completed"},{tool:"recent",status:"completed"}])
assert.deepEqual((await pool.query("SELECT action FROM mcp_audit WHERE actor_id=$1 ORDER BY action",[actorId])).rows,
[{action:"protected"},{action:"recent"}])
}finally{
try{if(transactionOpen)await settingsWriter.query("ROLLBACK")}finally{settingsWriter.release()}
await tick?.catch(()=>{})
await pool.query("DELETE FROM mcp_requests WHERE actor_id=$1",[actorId])
await pool.query("DELETE FROM mcp_audit WHERE actor_id=$1",[actorId])
await pool.query("UPDATE mcp_settings SET requests_retention_days=$1,audit_retention_days=$2 WHERE id=1",[original.requestsRetentionDays,original.auditRetentionDays])
}
console.log("Owned MCP retention: worker waits for committed policy, preserves protected and running rows, and deletes expired rows")
}
try {
await applySqlMigrations(pool)
await pauseSchedulerJobsAndWait()
await assertWorkerUsesCommittedRetention()
await pool.query("UPDATE public.mcp_settings SET enabled=true WHERE id=1")
const serverId=Number((await pool.query("INSERT INTO servers(name,host,enabled) VALUES('owned logs retention','127.0.0.1',false) RETURNING id")).rows[0].id)
async function access(){
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.read","logs.settings.update","traffic.settings.update","uptime.settings.update",
"servers.api-ping.settings.update","internet-path.settings.update","geoip.settings.update","backups.schedule.update","operations.read","changes.apply"],allowGlobal:true},
limits:{callsPerMinute:100,mutationsPerHour:20}}),actor)
return {actor,principal:await authenticateMcpToken(issued.token)}
}
const {actor,principal}=await access(),foreign=(await access()).principal
const initialLogs=await invokeMcpTool(principal,"mm_logs_settings_get",{})
assert.deepEqual(initialLogs,{enabled:false,intervalSec:10,retentionDays:14})
const shorterLogs=await invokeMcpTool(principal,"mm_logs_settings_update_plan",{settings:{retentionDays:7}}) as {state:string;changes:{warnings:string[]}}
assert.equal(shorterLogs.state,"awaiting-approval")
assert.match(shorterLogs.changes.warnings.join(" "),/с 14 до 7 суток.*полученные более 7 суток назад/)
assert.deepEqual(await invokeMcpTool(principal,"mm_logs_settings_get",{}),initialLogs)
async function operation(retentionDays:number,ageMs:number,tool="mm_logs_settings_update_plan",
settings:Record<string,unknown>={retentionDays}){
const plan=await invokeMcpTool(principal,tool,{settings}) as {planId:string;state:string}
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 beforeFence=await projection()
const exclusive=await pool.connect()
let blockedAttempt:ReturnType<typeof retireOrdinaryMcpOperations>|undefined
try{
await exclusive.query("SELECT pg_advisory_lock($1::bigint)",[PARTITION_WRITER_ADVISORY_KEY])
await exclusive.query("BEGIN")
blockedAttempt=retireOrdinaryMcpOperations()
void blockedAttempt.catch(()=>{})
let waiting=false
for(const deadline=Date.now()+3000;!waiting&&Date.now()<deadline;){
waiting=(await exclusive.query(`SELECT EXISTS(SELECT 1 FROM pg_catalog.pg_locks
WHERE locktype='advisory' AND database=(SELECT oid FROM pg_catalog.pg_database WHERE datname=current_database())
AND classid=0 AND objid=$1::oid AND objsubid=1 AND mode='ShareLock' AND NOT granted) AS waiting`,[PARTITION_WRITER_ADVISORY_KEY])).rows[0].waiting
if(!waiting)await delay(10)
}
assert.equal(waiting,true,"the retiring checkout must wait at the common fence")
assert.equal((await exclusive.query("SELECT id FROM public.mcp_connections WHERE id=$1 FOR UPDATE NOWAIT",[principal.connectionId])).rows.length,1)
for(const candidate of [spent,due]){
const {rows:[row]}=await exclusive.query("SELECT pg_try_advisory_xact_lock(hashtextextended($1,0)) AS locked",["mcp-operation:"+candidate.id])
assert.equal(row.locked,true,"the retiring checkout must not hold a secondary operation key")
}
const blocked=await blockedAttempt
assert(blocked.scanned>0);assert.equal(blocked.retired,0);assert.equal(blocked.automaticReplay,false)
assert.deepEqual(await projection(),beforeFence)
}finally{
try{await blockedAttempt}finally{
try{
await exclusive.query("ROLLBACK")
assert.equal((await exclusive.query("SELECT pg_advisory_unlock($1::bigint) AS unlocked",[PARTITION_WRITER_ADVISORY_KEY])).rows[0].unlocked,true)
}finally{exclusive.release(true)}
}
}
const originalQuery=Client.prototype.query
let commits=0
try{
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,224 @@
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 { admitPartitionWriter } from "../../services/partition-write-transaction.js"
import { sharedDatabaseGate } from "../../services/shared-database-gate.js"
import { operationIdempotencyDigest } from "./operation-replay-tombstones.js"
import { decryptSecretExisting, fingerprint, privateFingerprinterExisting, redact } from "./security.js"
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, mode:"scan"|"write", work:(sql:ProbeStorageSql)=>Promise<T>):Promise<T> {
const sql = await connectProbeStorage({deadlineAt})
let committing=false,committed=false
try {
await sql.query("BEGIN")
// Preserve the one-second lock budget and admit before any target row or operation lock.
if(mode==="write")await admitPartitionWriter(sql,{deadlineAt:Math.min(deadlineAt,Date.now()+1000)})
await sql.query("SELECT set_config('TimeZone','UTC',true),set_config('statement_timeout','5000ms',true),set_config('lock_timeout','1000ms',true)")
const result=await work(sql)
committing=true
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,"write",async sql=>{
const source=await prepare(sql,operationId,connectionId)
const claim=await sql.query(`INSERT INTO public.mcp_ordinary_retirement_claims(operation_id,plan_id,connection_id,source_proof,claim_token)
VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING RETURNING operation_id`,[operationId,source.planId,connectionId,source.proof,token])
requireThat(claim.rows.length===1&&claim.rows[0].operation_id===operationId)
})
await transaction(deadlineAt,"write",async sql=>{
const claim=(await sql.query(`SELECT source_proof,plan_id FROM public.mcp_ordinary_retirement_claims
WHERE operation_id=$1 AND connection_id=$2 AND claim_token=$3 AND claim_xid<>pg_current_xact_id()`,[operationId,connectionId,token])).rows
requireThat(claim.length===1)
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,"scan",async sql=>(await sql.query(CANDIDATES,[cursor?.finishedAt??null,cursor?.operationId??null])).rows)
requireThat(rows.length<=8)
let scanned=0,retired=0
for(const row of rows){
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 { fingerprint, McpError, redact } from "./security.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 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()
}
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 = []
const catalogPrefix = createRosPrefix([catalog], async (_id, _resource, beforeRead = async () => {}) => {
// 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 type PathProofPurpose = z.infer<typeof pathProofPurposeSchema>
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 function isCommunityPathEvidence(value: Record<string, unknown>) {
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 type { add } from "./actions.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]) {
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 = {}) {
// Compare the database timestamp with the database clock. The signed
// 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)
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 })
if (!current.verifyEvidence(evidence, proof)) throw new McpError("MEASUREMENT_UNKNOWN", "Зависимости пути или сохранённое подтверждение изменились", 409)
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 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", () => {
const bound = iso(300000)
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
* 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 {
const now = Date.now(), ordinaryExpiry = now + (section === "network" ? 300000 : 900000)
if (operationId !== "servers.poll") return new Date(ordinaryExpiry)
const validity = z.string().datetime({ offset: true }).safeParse(evidence.validUntil)
if (operationId !== "servers.poll" && operationId !== "routing.community.switch") return new Date(ordinaryExpiry)
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
if (!Number.isFinite(bound) || bound <= now) return null
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 {
for (const state of ["malformed", "redirect", "large", "announced", "stalled"]) {
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.ok(performance.now() - started < 3000, state)
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)
} 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";
elapsedMs: number;
reason?: string;
credentialType: "temporary-no-operations";
credentialType: "temporary-no-operations" | "temporary-capabilities-read";
capabilitiesRead: "passed" | "failed" | "skipped";
clientCredentialsVerified: false;
}
/** A temporary access credential may negotiate metadata, but its empty grant
* permits no tools. The ordinary connection credentials are never recovered. */
export async function withMcpProbeCredential<T>(connectionId: string, actor: AuthUser, work: (token: string) => Promise<T>): Promise<T> {
const capabilitiesOperation = "capabilities.read"
const capabilitiesTool = "mm_capabilities_get"
/** 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")
await transaction(async client => {
const readCapabilities = await transaction(async client => {
await client.query("SELECT id FROM mcp_connections WHERE id=$1 FOR UPDATE", [connectionId])
const connection = await getConnection(connectionId, client)
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)
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 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_grants(id,connection_id,payload) VALUES($1,$2,$3::jsonb)", [grantId, connectionId, JSON.stringify({ ...grant, credentialId })])
await validatePrincipal({ connectionId, credentialId, credentialExpiresAt: expiresAt }, client)
await audit(client, actor.id, "connection.handshake.start", connectionId, { credentialId, expiresAt })
return allowed
})
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 {
await transaction(async client => {
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. */
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 timer = setTimeout(() => controller.abort(), timeoutMs)
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 {
const transport = new StreamableHTTPClientTransport(endpoint, { requestInit: { headers: { Authorization: `Bearer ${token}` }, redirect: "error" }, redirectPolicy: "follow",
fetch: async (url, init) => {
@@ -67,13 +74,22 @@ export async function runMcpProtocolProbe(endpoint: URL, token: string, timeoutM
} })
await client.connect(transport, { 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")
} 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(() => {}) }
}
export async function checkMcpProtocol(connectionId: string, actor: AuthUser) {
const endpoint = canonicalMcpUrl()
return withMcpProbeCredential(connectionId, actor, token => runMcpProtocolProbe(endpoint, token))
return withMcpProbeCredential(connectionId, actor, (token, readCapabilities) => runMcpProtocolProbe(endpoint, token, { readCapabilities }))
}
+34 -5
View File
@@ -6,13 +6,16 @@ import { decryptSecret, fingerprint, McpError } from "./security.js"
import { effectiveMcpConnection } from "./store.js"
import { findMcpService } from "./service-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 type { add } from "./actions.js"
import type { ActionContext, ChangeEvidence, ChangeStep } from "./action-types.js"
import { loadScopedRecoverySource } from "./routing-community-scoped-recovery-source.js"
import { prepareScopedRecovery, registerScopedRecoveryService } from "./routing-community-scoped-recovery.js"
import { reconcileVrfProductionStep } from "../../services/vrf-production-execution.js"
import { needsBridgeVlanRecovery, prepareBridgeVlanRecovery } from "./bridge-vlan-recovery.js"
import { readRpfDependencies } from "./routing-controls.js"
import { verifyHealthChecks } from "./routing-health-checks.js"
async function source(operationId: string, context: ActionContext, allowServices = false) {
const { rows: [row] } = await pool.query("SELECT o.*,p.operation_id AS source_action,p.operation_version AS source_version,p.evidence FROM mcp_operations o JOIN mcp_plans p ON p.id=o.plan_id WHERE o.id=$1 AND o.connection_id=$2", [operationId, context.connection.id])
@@ -86,7 +89,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(), {
version: 7,
version: 8,
target: async (input, context) => {
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)
@@ -106,14 +109,16 @@ 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 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 (needsBridgeVlanRecovery(row.source_action, evidence)) return prepareBridgeVlanRecovery({ operationId: row.id, evidence, checkpoints, intended }, context)
const snapshots = new Map<string, RosSnapshot>(), steps: ChangeStep[] = [], objects: ChangeEvidence["objects"] = [...evidence.objects]
const warnings = [`Обратный план операции ${row.id}; не является повтором исходного действия.`, "Восстановление удалённых объектов и сервисов требует их собственного механизма ревизий.", "Сохраняются инварианты Full OSPF/Up BFD и привязка соседств к устройствам, VRF, адресам и интерфейсам; достижимость восстановленной сетевой конфигурации проверяется контрольными пробами."]
for (const checkpoint of checkpoints) {
const original = evidence.steps[checkpoint.step_index]
if (!original || original.kind !== "ros" || original.step.action === "delete") throw new McpError("UNSUPPORTED_CAPABILITY", "Удалённый объект может иметь новые ID и зависимости; используйте проверенную ревизию", 409)
const step = original.step, resource = resourceById(step.resource)
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
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}`
if (!snapshots.has(key)) {
const current = await readRosSnapshot(step.serverId, step.resource, undefined, after.sourceProof)
@@ -143,11 +148,35 @@ export function registerRecoveryActions(register: typeof add) {
const key = `${dependency.serverId}:${dependency.resource}`
if (snapshots.has(key)) continue
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)
}
for (const change of steps) {
if (change.kind !== "ros" || change.step.resource !== "ip_settings" || !change.step.fields["rp-filter"]) continue
const settings = snapshots.get(`${change.step.serverId}:ip_settings`)
const rpf = await readRpfDependencies(change.step.serverId, change.step.fields["rp-filter"], settings?.sourceProof)
for (const snapshot of rpf.snapshots) {
const key = `${snapshot.serverId}:${snapshot.resource}`, previous = snapshots.get(key)
if (previous && (previous.fingerprint !== snapshot.fingerprint || previous.sourceProof !== snapshot.sourceProof)) {
throw new McpError("STATE_CONFLICT", "Зависимость RPF изменилась при подготовке восстановления", 409)
}
snapshots.set(key, snapshot)
}
warnings.push(...rpf.warnings)
}
objects.push(...[...new Set([...snapshots.values()].map(snapshot => snapshot.serverId))].map(serverId => ({ kind: "server", id: String(serverId), serverId })))
return { target: { scope: "servers", serverIds: [...new Set([...snapshots.values()].map(snapshot => snapshot.serverId))] }, objects, snapshots: [...snapshots.values()], steps, ospfOwnerGraph: evidence.ospfOwnerGraph, healthChecks: evidence.healthChecks?.filter(check => check.field === "state" || check.field === "bfd-up"), requiresHuman: true, warnings: [`Обратный план операции ${row.id}; не является повтором исходного действия.`, "Восстановление удалённых объектов и сервисов требует их собственного механизма ревизий.", "Сохраняются инварианты Full OSPF/Up BFD и привязка соседств к устройствам, VRF, адресам и интерфейсам; достижимость восстановленной сетевой конфигурации проверяется контрольными пробами."] }
const healthChecks = evidence.healthChecks?.filter(check => check.field === "state" || check.field === "bfd-up" || check.field === "resolved")
const currentSnapshots = [...snapshots.values()]
const serverIds = [...new Set(currentSnapshots.map(snapshot => snapshot.serverId))]
const nextHopChecks = healthChecks?.filter(check => check.field === "resolved")
if (nextHopChecks?.length) await verifyHealthChecks({ healthChecks: nextHopChecks, snapshots: currentSnapshots }, serverIds, false, async () => {
if (context.signal?.aborted) throw new McpError("READ_CANCELLED", "Подготовка обратного плана отменена", 409)
const current = await effectiveMcpConnection(context.principal)
if (fingerprint(current.policy) !== fingerprint(context.connection.policy) || fingerprint(current.owner_permissions) !== fingerprint(context.connection.owner_permissions)) {
throw new McpError("RESET_REQUIRED", "Допуск восстановления изменился", 409)
}
})
return { target: { scope: "servers", serverIds }, objects, snapshots: currentSnapshots, steps, ospfOwnerGraph: evidence.ospfOwnerGraph, healthChecks, requiresHuman: true, warnings }
},
})
}
+53 -10
View File
@@ -5,14 +5,19 @@ import { eq } from "drizzle-orm"
import { db } from "../../db/index.js"
import { servers } from "../../db/schema.js"
import { MikrotikClient } from "../../services/mikrotik.js"
import { McpError, fingerprint, privateSecretFingerprint } from "./security.js"
import { readRouterOsSnapshotGet } from "../../services/routeros-snapshot-read.js"
import { McpError, fingerprint, privateFingerprinter, privateSecretFingerprint } from "./security.js"
import { readOspfEffectiveDisabled } from "../../services/ospf-effective-disabled.js"
import { greNativeFieldsSchema } from "../../services/gre-mutation-input.js"
import { canonicalVrfMembers } from "../../services/vrf-members.js"
import { canonicalBridgeVlanValue } from "../../services/bridge-vlan-values.js"
const text = z.string().max(1000)
const name = z.string().trim().min(1).max(100)
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 common = { name: name.optional(), comment: text.optional(), disabled: bool.optional() }
const ipv4 = name.refine(value => isIP(value) === 4, "Требуется IPv4 router/area ID")
@@ -31,19 +36,21 @@ const route = {
export interface RosResourceSpec {
id: string; path: string; title: string; permission: string; fields: z.ZodObject;
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[] = [
{ 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: "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_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: "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: "ipv4_route", path: "/ip/route", title: "Маршруты IPv4", permission: "mm:network", fields: z.object(route).strict(), create: true, remove: true, requiredOnCreate: ["dst-address"] },
{ id: "ipv6_route", path: "/ipv6/route", title: "Маршруты IPv6", permission: "mm:network", fields: z.object(route).strict(), create: true, remove: true, requiredOnCreate: ["dst-address"] },
{ id: "routing_table", path: "/routing/table", title: "Таблицы маршрутизации", permission: "mm:network", fields: z.object({ name: name.optional(), fib: bool.optional(), disabled: bool.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["name"] },
{ id: "routing_table", path: "/routing/table", title: "Таблицы маршрутизации", permission: "mm:network", fields: z.object({ name: name.optional(), fib: bool.optional(), disabled: bool.optional() }).strict(), observeFields: ["invalid"], create: true, remove: true, requiredOnCreate: ["name"] },
{ id: "routing_rule", path: "/routing/rule", title: "Правила маршрутизации", permission: "mm:network", fields: z.object({ "src-address": text.optional(), "dst-address": text.optional(), interface: text.optional(), action: z.enum(["lookup", "lookup-only-in-table", "drop", "unreachable"]).optional(), table: name.optional(), disabled: bool.optional(), comment: text.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["action"] },
{ id: "vrf", path: "/ip/vrf", title: "VRF", permission: "mm:network", fields: z.object({ name: name.optional(), interfaces: text.optional(), disabled: bool.optional(), comment: text.optional() }).strict(), create: true, remove: true, requiredOnCreate: ["name", "interfaces"] },
{ id: "ip_settings", path: "/ip/settings", title: "RPF и IPv4 hash-policy", permission: "mm:network", fields: z.object({ "rp-filter": z.enum(["no", "loose", "strict"]).optional(), "ipv4-multipath-hash-policy": z.enum(["l3", "l4", "l3-inner"]).optional() }).strict(), create: false, remove: false },
@@ -70,7 +77,34 @@ export const rosResources: RosResourceSpec[] = [
export type ConfigurationRow = Record<string, unknown>
export interface RosStep { serverId: number; resource: string; action: "create" | "update" | "delete"; id?: string; fields: Record<string, string>; placeBeforeId?: string; expectedCost?: number; expectedServerProof?: string }
export const ROS_SNAPSHOT_VERSION = 3
export const rosSnapshotVersion = (resource: string): number => ["bridge", "bridge_port", "bridge_vlan", "routing_table"].includes(resource) ? 4 : ROS_SNAPSHOT_VERSION
export interface RosSnapshot { serverId: number; resource: string; rows: ConfigurationRow[]; fingerprint: string; projectionVersion?: number; sourceProof?: string }
const routingPolicyResources = new Set(["routing_rule", "ip_filter", "ip_nat", "ip_mangle", "ip_raw", "ipv6_filter", "ipv6_nat", "ipv6_mangle", "ipv6_raw"])
const routingPolicyPrefix = "routing_policy:"
const routingPolicyCounters = new Set(["bytes", "packets"])
export function isRoutingPolicySnapshot(resource: string) {
return resource.startsWith(routingPolicyPrefix) && routingPolicyResources.has(resource.slice(routingPolicyPrefix.length))
}
export function readRoutingPolicySnapshot(serverId: number, resource: string, expectedSourceProof?: string) {
if (!routingPolicyResources.has(resource)) throw new McpError("UNSUPPORTED_CAPABILITY", "Неизвестный каталог политики маршрутизации", 409)
return readRosSnapshot(serverId, routingPolicyPrefix + resource, undefined, expectedSourceProof)
}
async function routingPolicyRows(resource: string, payload: unknown): Promise<ConfigurationRow[]> {
if (!Array.isArray(payload)) throw new McpError("STATE_CONFLICT", "Каталог политики маршрутизации не подтверждён", 409)
const identify = await privateFingerprinter(`mcp-routing-policy-v1:${resource}`)
const ids = new Set<string>()
return payload.map(row => {
const id = row?.[".id"]
if (!row || typeof row !== "object" || Array.isArray(row) || typeof id !== "string" || !id.trim() || ids.has(id)) {
throw new McpError("STATE_CONFLICT", "Неоднозначные ID каталога политики маршрутизации", 409)
}
ids.add(id)
// Seal the native rule, including selectors outside the write schema.
const configuration = Object.fromEntries(Object.entries(row).filter(([name]) => resource === routingPolicyPrefix + "routing_rule" || !routingPolicyCounters.has(name)))
return { ".id": id, action: row.action ?? null, disabled: row.disabled ?? null, proof: identify(fingerprint(configuration)) }
})
}
export function resourceById(id: string): RosResourceSpec {
const resource = rosResources.find(item => item.id === id)
if (!resource) throw new McpError("UNSUPPORTED_CAPABILITY", "Неизвестный тип сетевого объекта", 400)
@@ -93,13 +127,21 @@ export function configurationRows(resource: RosResourceSpec, payload: unknown):
// RouterOS creates/removes IPv6 link-local rows with interface runtime state.
// They cannot be configured independently and must not invalidate a 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 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.
// Normalize only this observed field; missing values must remain unknown.
if (resource.id === "routing_table" && selected.fib === "") selected.fib = "false"
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
})
// Reordering an ordered catalog changes configuration even with unchanged rows.
@@ -107,7 +149,8 @@ export function configurationRows(resource: RosResourceSpec, payload: unknown):
}
export async function readRosSnapshot(serverId: number, resourceId: string, beforeRead: () => Promise<unknown> = async () => {}, expectedSourceProof?: string): Promise<RosSnapshot> {
await beforeRead()
const resource = resourceById(resourceId), { client, server } = await routerClient(serverId)
const policy = isRoutingPolicySnapshot(resourceId)
const resource = resourceById(policy ? resourceId.slice(routingPolicyPrefix.length) : resourceId), { client, server } = await routerClient(serverId)
const sourceProof = await rosSnapshotSourceProof(server)
if (expectedSourceProof !== undefined && expectedSourceProof !== sourceProof) throw new McpError("STATE_CONFLICT", "Источник сетевого снимка изменился до опроса", 409)
const checkSource = async () => {
@@ -118,10 +161,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 => {
await checkSource()
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()
const rows = configurationRows(resource, payload)
return { serverId, resource: resourceId, rows, fingerprint: fingerprint(rows), projectionVersion: ROS_SNAPSHOT_VERSION, sourceProof }
const rows = policy ? await routingPolicyRows(resourceId, payload) : configurationRows(resource, payload)
return { serverId, resource: resourceId, rows, fingerprint: fingerprint(rows), projectionVersion: rosSnapshotVersion(resourceId), sourceProof }
}
export async function rosSnapshotSourceProof(server: Awaited<ReturnType<typeof routerClient>>["server"]): Promise<string> {
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")
@@ -60,6 +60,16 @@ test("non-JSON detail cannot overwrite the already applied receipt", () => {
output.result = output
assert.equal(projected(accepted(output, "operation")).state, "applied")
})
test("compact receipt keeps a known pre-dispatch stop and its verified prefix", () => {
for (const [state, confirmedWrites] of [["rejected", 0], ["partial", 2]] as const) {
const result = { failedStepDispatched: false, confirmedWrites }
const output = { id, plan_id: plan, state, verification: "inconclusive", error_code: "STATE_CONFLICT" }
assert.deepEqual(projected(accepted({ ...output, result: { ...result, detail: 1n } }, "operation")), { ...output, result })
for (const invalid of [{ ...result, confirmedWrites: -1 }, { ...result, failedStepDispatched: true }]) {
assert.deepEqual(projected(accepted({ ...output, result: { ...invalid, detail: 1n } }, "operation")), output)
}
}
})
test("prepare omission keeps real plan/state, requests review, never invents executionAvailable", () => {
const result = accepted({ planId: plan, state: "approved", expiresAt: "2026-10-31T12:00:00Z", executionAvailable: true, changes: "x".repeat(MCP_CALL_RESULT_BYTES) }, "plan")
assert.equal(result.isError, undefined)
+7
View File
@@ -36,6 +36,13 @@ function project(output: unknown, projection: ResultProjection): Record<string,
const receipt: Record<string, unknown> = { id, plan_id: planId, state, verification }
const errorCode = data(output, "error_code")
if (errorCode === null || short(errorCode)) receipt.error_code = errorCode
const outcome = data(output, "result")
const confirmedWrites = data(outcome, "confirmedWrites")
if ((state === "partial" || state === "rejected") && data(outcome, "failedStepDispatched") === false &&
typeof confirmedWrites === "number" && Number.isSafeInteger(confirmedWrites) &&
(state === "partial" ? confirmedWrites > 0 : confirmedWrites === 0)) {
receipt.result = { failedStepDispatched: false, confirmedWrites }
}
return receipt
}
const planId = data(output, "planId"), state = data(output, "state")
+3 -2
View File
@@ -1,9 +1,9 @@
import assert from "node:assert/strict"
import { createRosPrefix } from "./ros-prefix.js"
import { fingerprint, McpError } from "./security.js"
import { ROS_SNAPSHOT_VERSION, type RosSnapshot, type RosStep, type ConfigurationRow } from "./resources.js"
import { rosSnapshotVersion, type RosSnapshot, type RosStep, type ConfigurationRow } from "./resources.js"
const snapshot = (resource: string, rows: ConfigurationRow[], sourceProof = "a".repeat(64)): RosSnapshot => ({ serverId: 1, resource, rows, fingerprint: fingerprint(rows), projectionVersion: ROS_SNAPSHOT_VERSION, sourceProof })
const snapshot = (resource: string, rows: ConfigurationRow[], sourceProof = "a".repeat(64)): RosSnapshot => ({ serverId: 1, resource, rows, fingerprint: fingerprint(rows), projectionVersion: rosSnapshotVersion(resource), sourceProof })
const old = snapshot("ip_filter", [{ ".id": "*a", chain: "forward", action: "accept", comment: "old" }, { ".id": "*b", chain: "forward", action: "drop", comment: "keep" }])
const dependency = snapshot("routing_table", [{ ".id": "*main", name: "main", fib: "true" }])
const changed = snapshot("ip_filter", [{ ...old.rows[0], comment: "new" }, old.rows[1]])
@@ -15,6 +15,7 @@ const isFailed = (error: unknown) => error instanceof McpError && error.code ===
for (const invalid of [{ ...old, sourceProof: undefined }, { ...old, projectionVersion: 1 }, { ...old, projectionVersion: 2 }, { ...old, fingerprint: "forged" }]) assert.throws(() => createRosPrefix([invalid]), error => error instanceof McpError && error.code === "PLAN_EXPIRED")
assert.throws(() => createRosPrefix([old, changed]), isConflict)
assert.throws(() => createRosPrefix([{ ...dependency, projectionVersion: 3 }]), { code: "PLAN_EXPIRED" })
const prefix = createRosPrefix([old, dependency, structuredClone(old)], read)
await prefix.check()
actual.set(old.resource, changed)
+2 -2
View File
@@ -1,11 +1,11 @@
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 { acknowledgedCreateId } from "./native-create-identity.js"
const key = (snapshot: Pick<RosSnapshot, "serverId" | "resource">) => `${snapshot.serverId}:${snapshot.resource}`
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])
const ids = snapshot.rows.map(acknowledgedCreateId)
return ids.every(id => id !== undefined) && new Set(ids).size === ids.length
@@ -1,7 +1,7 @@
import assert from "node:assert/strict"
import { configurationRows, isOrderedRosResource, resourceById, type ConfigurationRow, type RosSnapshot, type RosStep } from "./resources.js"
import { configurationRows, isOrderedRosResource, resourceById, rosSnapshotVersion, type ConfigurationRow, type RosSnapshot, type RosStep } from "./resources.js"
import { fingerprint } from "./security.js"
import { verifyRosStepSnapshots } from "./ros-step-verification.js"
import { proveRosDriftExpectation, verifyRosStepSnapshots } from "./ros-step-verification.js"
import { readOspfTemplateFlags } from "../../services/ospf-template-flags.js"
function snapshot(resource: string, rows: ConfigurationRow[]): RosSnapshot {
@@ -106,3 +106,55 @@ assert.equal(raw[0].opaque, "outside-projection")
assert.equal(verifyRosStepSnapshots(update, {}, { ...snapshot("ip_filter", [a]), serverId: 2 }, snapshot("ip_filter", [updated])), false)
assert.equal(verifyRosStepSnapshots(update, {}, snapshot("bridge", [a]), snapshot("ip_filter", [updated])), false)
console.log("ROS projected whole-catalog verifier: acknowledged assigned ID, exact membership/field conservation, 12 ordered catalogs including OSPF templates, singleton settings, schema booleans, effective OSPF unset, immutable inputs")
const sourceProof = "a".repeat(64)
function driftSnapshot(resource: string, rows: ConfigurationRow[]): RosSnapshot {
return { ...snapshot(resource, rows), projectionVersion: rosSnapshotVersion(resource), sourceProof }
}
function expectDrift(step: RosStep, receipt: unknown, before: ConfigurationRow[], after: ConfigurationRow[]) {
const expectation = proveRosDriftExpectation(step, receipt, driftSnapshot(step.resource, before), driftSnapshot(step.resource, after))
assert.ok(expectation)
assert.equal(expectation.sourceProof, sourceProof)
return expectation
}
const settingsDrift = expectDrift(settings, {}, [s0], [s1])
assert.equal(settingsDrift.nativeId, undefined)
assert.equal(settingsDrift.matches(driftSnapshot("ip_settings", [{ ...s1, "ipv4-multipath-hash-policy": "l4" }])), true)
assert.equal(settingsDrift.matches(driftSnapshot("ip_settings", [s0])), false)
for (const rows of [[], [s1, s1], [{}], [{ "rp-filter": "unknown" }], [{ "rp-filter": null }], [{ "rp-filter": ["loose"] }]]) {
assert.equal(settingsDrift.matches(driftSnapshot("ip_settings", rows)), undefined)
}
const verifiedSettings = driftSnapshot("ip_settings", [s1])
assert.equal(settingsDrift.matches({ ...verifiedSettings, sourceProof: "b".repeat(64) }), undefined)
assert.equal(settingsDrift.matches({ ...verifiedSettings, serverId: 2 }), undefined)
assert.equal(settingsDrift.matches({ ...verifiedSettings, fingerprint: "b".repeat(64) }), undefined)
assert.equal(proveRosDriftExpectation(settings, {}, snapshot("ip_settings", [s0]), verifiedSettings), undefined, "Legacy evidence without a source/version cannot prove drift")
assert.equal(proveRosDriftExpectation(settings, {}, driftSnapshot("ip_settings", [s0]), { kind: "vrf-production-private-checkpoint-v1" }), undefined)
assert.equal(proveRosDriftExpectation(settings, {}, driftSnapshot("ip_settings", [s0]), { ...verifiedSettings, sourceProof: "b".repeat(64) }), undefined)
assert.equal(proveRosDriftExpectation(settings, undefined, driftSnapshot("ip_settings", [s0]), verifiedSettings), undefined)
const createDrift = expectDrift(create, ack, [a, b], [a, b, c])
assert.equal(createDrift.nativeId, "*C")
assert.equal(createDrift.matches(driftSnapshot("ip_filter", [{ ...b, comment: "unrelated" }, c])), true)
assert.equal(createDrift.matches(driftSnapshot("ip_filter", [a, b])), false)
assert.equal(createDrift.matches(driftSnapshot("ip_filter", [a, b, c, c])), undefined)
assert.equal(proveRosDriftExpectation(create, {}, driftSnapshot("ip_filter", [a, b]), driftSnapshot("ip_filter", [a, b, c])), undefined)
const updateDrift = expectDrift(update, {}, [a, b], [updated, b])
assert.equal(updateDrift.nativeId, "*A")
assert.equal(updateDrift.matches(driftSnapshot("ip_filter", [{ ...updated, comment: "unrelated" }, c])), true)
assert.equal(updateDrift.matches(driftSnapshot("ip_filter", [a, c])), false)
assert.equal(updateDrift.matches(driftSnapshot("ip_filter", [b, c])), false)
const booleanDrift = expectDrift(booleanStep, {}, [a], [a])
assert.equal(booleanDrift.matches(driftSnapshot("ip_filter", [{ ...a, disabled: "no" }])), true)
assert.equal(booleanDrift.matches(driftSnapshot("ip_filter", [{ ...a, disabled: "yes" }])), false)
assert.equal(booleanDrift.matches(driftSnapshot("ip_filter", [{ ...a, disabled: "" }])), undefined)
const deleteDrift = expectDrift(remove, null, [a, b], [b])
assert.equal(deleteDrift.nativeId, "*A")
assert.equal(deleteDrift.matches(driftSnapshot("ip_filter", [{ ...b, comment: "unrelated" }, c])), true)
assert.equal(deleteDrift.matches(driftSnapshot("ip_filter", [{ ...a, disabled: "no" }, c])), false)
assert.equal(deleteDrift.matches(driftSnapshot("ip_filter", [{ ...a, chain: "output", comment: "a different rule reused this ID" }, b])), undefined)
console.log("ROS drift proof: bound source and verified transition, scoped typed fields, unknown observations and delete ID reuse")
@@ -1,6 +1,8 @@
import { isDeepStrictEqual } from "node:util"
import { fingerprint } from "./security.js"
import { acknowledgedCreateId } from "./native-create-identity.js"
import { isOrderedRosResource, resourceById, type ConfigurationRow, type RosResourceSpec, type RosSnapshot, type RosStep } from "./resources.js"
import { canonicalBridgeVlanValue } from "../../services/bridge-vlan-values.js"
import { isOrderedRosResource, resourceById, rosSnapshotVersion, type ConfigurationRow, type RosResourceSpec, type RosSnapshot, type RosStep } from "./resources.js"
function booleanValue(value: unknown): unknown {
if ([true, "true", "yes"].includes(value as boolean | string)) return true
@@ -23,6 +25,9 @@ function requested(resource: RosResourceSpec, fields: RosStep["fields"], row: Co
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.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
})
}
@@ -88,3 +93,66 @@ export function verifyRosStepSnapshots(step: RosStep, result: unknown, before: R
// already prove exact membership. Ordered catalogs must also retain position.
return !isOrderedRosResource(step.resource) || isDeepStrictEqual(expectedIds, [...current.keys()])
}
const singletonRosResource = (resource: string) => resource === "ip_settings" || resource === "ipv6_settings"
function projectedSnapshot(value: unknown, step: RosStep): value is RosSnapshot & { sourceProof: string } {
if (!value || typeof value !== "object" || Array.isArray(value)) return false
const snapshot = value as RosSnapshot
if (snapshot.serverId !== step.serverId || snapshot.resource !== step.resource
|| snapshot.projectionVersion !== rosSnapshotVersion(step.resource)
|| typeof snapshot.sourceProof !== "string" || !/^[a-f0-9]{64}$/.test(snapshot.sourceProof)
|| !Array.isArray(snapshot.rows) || snapshot.fingerprint !== fingerprint(snapshot.rows)) return false
if (!singletonRosResource(step.resource)) return indexed(snapshot.rows) !== undefined
const row = snapshot.rows[0]
return snapshot.rows.length === 1 && !!row && typeof row === "object" && !Array.isArray(row)
}
interface RosDriftExpectation {
nativeId: string | undefined
sourceProof: string
matches(current: unknown): boolean | undefined
}
/** Bind the drift expectation to an acknowledged and verified transition.
* The caller distinguishes a stored JSON null (native 204) from SQL NULL. */
export function proveRosDriftExpectation(step: RosStep, receipt: unknown, before: unknown, after: unknown): RosDriftExpectation | undefined {
if (receipt === undefined || !projectedSnapshot(before, step) || !projectedSnapshot(after, step)
|| before.sourceProof !== after.sourceProof || !verifyRosStepSnapshots(step, receipt, before, after)) return undefined
const resource = resourceById(step.resource)
const nativeId = step.action === "create" ? acknowledgedCreateId(receipt) : step.id
const singleton = singletonRosResource(step.resource)
const deleted = step.action === "delete" ? before.rows.find(row => row[".id"] === nativeId) : undefined
const sourceProof = after.sourceProof
return {
nativeId,
sourceProof,
matches(current) {
if (!projectedSnapshot(current, step) || current.sourceProof !== sourceProof) return undefined
const row = singleton ? current.rows[0] : current.rows.find(item => item[".id"] === nativeId)
if (deleted) {
if (!row) return true
// A different row at a reused ID does not prove the original row returned.
return conserved(resource, deleted, row) ? false : undefined
}
if (!row) return false
let complete = true
for (const [key, expected] of Object.entries(step.fields)) {
const schema = resource.fields.shape[key]
const raw = row[key]
if (!schema || !Object.hasOwn(row, key) || raw === undefined || raw === null) {
complete = false
continue
}
const boolean = schema.safeParse(true).success
const valid = boolean ? typeof booleanValue(raw) === "boolean"
: (typeof raw === "string" || typeof raw === "number") && (
schema.safeParse(raw).success || typeof raw === "string" && /^\d+$/.test(raw) && schema.safeParse(Number(raw)).success
)
if (!valid) complete = false
else if (!requested(resource, { [key]: expected }, row)) return false
}
return complete ? true : undefined
},
}
}
+60 -13
View File
@@ -1,5 +1,5 @@
import { prepareRoutingMainTransition, beginRoutingMainTransition, finishRoutingMainTransition, type RoutingMainTransitionAttempt } from "./routing-main-transition.js"
import { readRoutingObservations } from "./routing-observations.js"
import { queryRoutingObservations, readRoutingObservations } from "./routing-observations.js"
import { isIP } from "node:net"
import { hasPermission } from "../../lib/permissions.js"
import { effectiveMcpConnection } from "./store.js"
@@ -25,10 +25,45 @@ import { resolveRoutingProfileAuthority, assertRoutingProfileAuthority, type Rou
import { createRoutingSourceContext, readRoutingEndpoints } from "./routing-observation-source.js"
const sid = z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
const historySelector = z.string().trim().min(1).max(100)
const routingHistorySchema = z.object({
serverId: sid.optional(), srcServerId: sid.optional(), dstServerId: sid.optional(),
srcInterface: historySelector.optional(), dstInterface: historySelector.optional(),
family: z.enum(["ipv4", "ipv6"]).optional(), table: historySelector.optional(),
limit: z.number().int().min(1).max(100).default(50),
cursor: z.object({ recordedAt: z.string().datetime({ precision: 6 }), id: z.string().uuid() }).strict().optional(),
}).strict()
const ospfOptimizerSchema = z.object({ serverId: sid, family: z.enum(["ipv4", "ipv6"]).default("ipv4"), table: z.string().min(1).default("main"), ecmpPaths: z.number().int().min(1).max(64).default(1), pingWeight: z.number().min(0).max(100).default(60), maxLossPct: z.number().min(0).max(100).default(1) }).strict()
export { readRoutingObservations } from "./routing-observations.js"
const profileTarget = (profile: z.infer<typeof routeAiProfileSchema>) => [...new Set([profile.serverId, ...profile.candidates.flatMap(candidate => [candidate.jumpHostId, candidate.exitNodeId])])]
const routeAnalysisSchema = z.object({
profile: routeAiProfileSchema,
offset: z.number().int().min(0).max(100).default(0),
limit: z.number().int().min(1).max(100).default(20),
eligibility: z.enum(["all", "eligible", "ineligible"]).default("all"),
}).strict()
const routeComparisonSchema = z.object({ profile: routeAiProfileSchema, candidateIds: z.array(z.string().min(1).max(100)).min(2).max(8) }).strict().superRefine(({ profile, candidateIds }, context) => {
const known = new Set(profile.candidates.map(candidate => candidate.id))
if (new Set(candidateIds).size !== candidateIds.length || candidateIds.some(id => !known.has(id))) {
context.addIssue({ code: "custom", path: ["candidateIds"], message: "Укажите разные ID путей из профиля" })
}
})
async function analyzeCommunityProfile(profile: z.infer<typeof routeAiProfileSchema>, context: ActionContext) {
const ids = profileTarget(profile)
if (ids.some(id => !context.connection.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Профиль выходит за область доступа")
const authorize = routeContextAccess(context)
await authorize()
const ranking = rankRoutingCandidates(profile, await readRoutingObservations(ids))
const routeContext = await readRouteCommunityContext(profile, { authorize, signal: context.signal })
for (const row of ranking) if (routeContext.before.candidateDiagnostics.find(diagnostic => diagnostic.candidateId === row.candidate.id)?.status !== "available") {
row.eligible = false; row.reasons.push("CANDIDATE_CONTEXT_UNAVAILABLE")
}
ranking.sort((a, b) => Number(b.eligible) - Number(a.eligible))
return { profile, ranking, ...routeContext.before, observedPath: false as const, returnPathVerified: false as const, observedAt: new Date().toISOString() }
}
async function assertCandidateEligible(profile: z.infer<typeof routeAiProfileSchema>, candidateId: string) {
const candidate = rankRoutingCandidates(profile, await readRoutingObservations(profileTarget(profile))).find(row => row.candidate.id === candidateId)
if (!candidate?.eligible) throw new McpError("MEASUREMENT_UNKNOWN", "Выбранный путь не удовлетворяет свежести измерений, ограничениям и SLA: " + (candidate?.reasons.join("; ") ?? "путь отсутствует"), 409)
@@ -65,21 +100,33 @@ async function filtersBefore(input: Record<string, unknown>, afterWrite = false)
export function registerRoutingActions(register: typeof add) {
registerRoutingMeasurement(register)
registerScopedCommunityService()
register("routing.measurements.read", "mm_routing_measurements_query", "История измерений со статусом проверки источника", "network", "mm:network:read", "read", "servers", z.object({ serverId: sid.optional() }).strict(), {
register("routing.measurements.read", "mm_routing_measurements_query", "История измерений со статусом проверки источника", "network", "mm:network:read", "read", "servers", routingHistorySchema, {
target: (_input, context) => ({ scope: "servers", serverIds: context.connection.policy.scope.serverIds }),
read: async (input, context) => ({ observations: await readRoutingObservations(context.connection.policy.scope.serverIds, input.serverId as number | undefined) }),
read: async (input, context) => queryRoutingObservations(context.connection.policy.scope.serverIds, routingHistorySchema.parse(input)),
})
register("routing.analyze", "mm_route_optimizer_analyze", "Анализ профиля Community", "network", "mm:network:read", "read", "servers", z.object({ profile: routeAiProfileSchema }).strict(), {
register("routing.analyze", "mm_route_optimizer_analyze", "Анализ профиля Community", "network", "mm:network:read", "read", "servers", routeAnalysisSchema, {
target: input => ({ scope: "servers", serverIds: profileTarget(routeAiProfileSchema.parse(input.profile)) }),
read: async (input, context) => {
const profile = routeAiProfileSchema.parse(input.profile), ids = profileTarget(profile)
if (ids.some(id => !context.connection.policy.scope.serverIds.includes(id))) throw new McpError("SERVER_DENIED", "Профиль выходит за область доступа")
const ranking = rankRoutingCandidates(profile, await readRoutingObservations(ids))
const routeContext = await readRouteCommunityContext(profile, { authorize: routeContextAccess(context), signal: context.signal })
for (const row of ranking) if (routeContext.before.candidateDiagnostics.find(diagnostic => diagnostic.candidateId === row.candidate.id)?.status !== "available") {
row.eligible = false; row.reasons.push("CANDIDATE_CONTEXT_UNAVAILABLE")
}
return { profile, ranking, ...routeContext.before, observedPath: false, returnPathVerified: false, observedAt: new Date().toISOString() }
const { profile, offset, limit, eligibility } = routeAnalysisSchema.parse(input)
const { ranking, candidateDiagnostics, ...analysis } = await analyzeCommunityProfile(profile, context)
const filtered = eligibility === "all" ? ranking : ranking.filter(row => row.eligible === (eligibility === "eligible"))
const page = filtered.slice(offset, offset + limit)
const candidateIds = new Set(page.map(row => row.candidate.id))
return { ...analysis, ranking: page, candidateDiagnostics: candidateDiagnostics.filter(row => candidateIds.has(row.candidateId)),
total: filtered.length, offset, limit, nextOffset: offset + limit < filtered.length ? offset + limit : null }
},
})
register("routing.compare", "mm_route_optimizer_compare", "Сравнение выбранных путей Community", "network", "mm:network:read", "read", "servers", routeComparisonSchema, {
target: input => ({ scope: "servers", serverIds: profileTarget(routeAiProfileSchema.parse(input.profile)) }),
read: async (input, context) => {
const { profile, candidateIds } = routeComparisonSchema.parse(input)
const analysis = await analyzeCommunityProfile(profile, context)
const ranking = analysis.ranking.flatMap(({ candidate, ...row }, index) => candidateIds.includes(candidate.id)
? [{ candidateId: candidate.id, profileRank: index + 1, ...row }]
: [])
return { serverId: profile.serverId, family: profile.family, table: profile.table, community: profile.community,
ranking, bestEligibleCandidateId: ranking.find(row => row.eligible)?.candidateId ?? null,
observedPath: analysis.observedPath, returnPathVerified: analysis.returnPathVerified, observedAt: analysis.observedAt }
},
})
@@ -216,7 +263,7 @@ export function registerRoutingActions(register: typeof add) {
warnings: evidence.warnings, criteria: { maxLossPct: input.maxLossPct, pingWeight: input.pingWeight, ecmpPaths: input.ecmpPaths }, executionAvailable: false, proposalOnly: true, forwardingVerified: false, newApprovedPlanRequired: true }
},
})
register("routing.ospf.optimize", "mm_ospf_optimizer_plan", "Оптимизация стоимости OSPF без пересчёта при применении", "network", "mm:network:write", "write", "servers", ospfOptimizerSchema, { target: (_input, context) => ({ scope: "servers", serverIds: context.connection.policy.scope.serverIds }), prepare: buildOspfOptimizationEvidence, version: 9 })
register("routing.ospf.optimize", "mm_ospf_optimizer_plan", "Оптимизация стоимости OSPF без пересчёта при применении", "network", "mm:network:write", "write", "servers", ospfOptimizerSchema, { target: (_input, context) => ({ scope: "servers", serverIds: context.connection.policy.scope.serverIds }), prepare: buildOspfOptimizationEvidence, version: 10 })
}
async function buildOspfOptimizationEvidence(input: Record<string, unknown>, context: ActionContext, mode: "read" | "write" = "write"): Promise<ChangeEvidence & { before: { ranking: OspfRank[] } }> {
@@ -151,6 +151,46 @@ export async function acceptRouteCommunityContextMcp(actor: AuthUser) {
const legacyComment = policyComment
const withoutProbe = (({ controlProbe: _probe, ...candidate }) => candidate)(base.candidates[0])
const mainProfile = routeAiProfileSchema.parse({ ...base, table: "main", candidates: [withoutProbe, { ...withoutProbe, id: "b", gateway: "192.0.2.9", gatewayRef: { resource: "ipv4_route", id: "*r2" } }] })
const compareAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Community comparison", mode: "observer", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read"], scope: { serverIds: [sid], operationIds: ["routing.compare"], allowGlobal: false } }), actor)
const analyzeAccess = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: "Community analysis only", mode: "observer", expiresAt: new Date(Date.now() + 1800000).toISOString(), permissions: ["mm:network:read"], scope: { serverIds: [sid], operationIds: ["routing.analyze"], allowGlobal: false } }), actor)
try {
const comparePrincipal = await authenticateMcpToken(compareAccess.token)
const analyzePrincipal = await authenticateMcpToken(analyzeAccess.token)
const compare = (candidateIds: string[], profile = mainProfile) => invokeMcpTool(comparePrincipal, "mm_route_optimizer_compare", { profile, candidateIds })
const beforeDenied = requests
await assert.rejects(invokeMcpTool(analyzePrincipal, "mm_route_optimizer_compare", { profile: mainProfile, candidateIds: ["a", "b"] }))
await assert.rejects(compare(["a", "a"]))
await assert.rejects(compare(["a", "missing"]))
await assert.rejects(compare(["a", "b"], routeAiProfileSchema.parse({ ...mainProfile, candidates: [mainProfile.candidates[0], { ...mainProfile.candidates[1], exitNodeId: sid + 1 }] })))
assert.equal(requests, beforeDenied)
const beforeWrites = writes
const analyzed = await invokeMcpTool(analyzePrincipal, "mm_route_optimizer_analyze", { profile: mainProfile }) as { ranking: Array<{ candidate: { id: string }; eligible: boolean; score: number | null }> }
const result = await compare(["b", "a"]) as { ranking: Array<{ candidateId: string; profileRank: number; eligible: boolean; score: number | null }>; observedPath: boolean; returnPathVerified: boolean }
assert.deepEqual(result.ranking.map(({ candidateId, profileRank, eligible, score }) => ({ candidateId, profileRank, eligible, score })), analyzed.ranking.map(({ candidate, eligible, score }, index) => ({ candidateId: candidate.id, profileRank: index + 1, eligible, score })))
assert.equal(result.observedPath, false)
assert.equal(result.returnPathVerified, false)
const pagedProfile = routeAiProfileSchema.parse({ ...mainProfile, candidates: [...mainProfile.candidates, ...Array.from({ length: 20 }, (_, index) => ({ ...mainProfile.candidates[1], id: `missing-${String(index).padStart(2, "0")}`, gatewayRef: { resource: "ipv4_route", id: `*absent${index}` } }))] })
type AnalysisPage = { ranking: Array<{ candidate: { id: string }; eligible: boolean }>; candidateDiagnostics: Array<{ candidateId: string }>; total: number; nextOffset: number | null; currentCandidateId: string | null }
const page = (options = {}) => invokeMcpTool(analyzePrincipal, "mm_route_optimizer_analyze", { profile: pagedProfile, ...options }) as Promise<AnalysisPage>
const firstPage = await page(), secondPage = await page({ offset: 20 }), full = await page({ limit: 100 })
assert.equal(firstPage.total, 22)
assert.equal(firstPage.ranking.length, 20)
assert.equal(firstPage.candidateDiagnostics.length, 20)
assert.equal(firstPage.nextOffset, 20)
assert.equal(secondPage.nextOffset, null)
assert.deepEqual([...firstPage.ranking, ...secondPage.ranking], full.ranking)
assert.equal(secondPage.currentCandidateId, full.currentCandidateId)
const eligible = await page({ eligibility: "eligible" }), ineligible = await page({ eligibility: "ineligible", limit: 100 })
assert.deepEqual(eligible.ranking, full.ranking.filter(row => row.eligible))
assert.deepEqual(ineligible.ranking, full.ranking.filter(row => !row.eligible))
await assert.rejects(page({ limit: 101 }))
assert.equal(writes, beforeWrites)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM mcp_plans WHERE connection_id=$1", [compareAccess.connection.id])).rows[0].n, 0)
assert.deepEqual(privateMarkerPaths(result), [])
console.log("✓ Route AI comparison: explicit observer scope, invalid IDs and hidden dependencies rejected before RouterOS, analysis parity and no plan or write")
} finally {
await pool.query("DELETE FROM mcp_connections WHERE id=ANY($1::uuid[])", [[compareAccess.connection.id, analyzeAccess.connection.id]])
}
for (const iface of ["wan", "hop"]) {
const observationId = randomUUID()
await pool.query("INSERT INTO mcp_routing_observations(id,src_server_id,dst_server_id,src_interface,dst_interface,family,routing_table,ping_ms,loss_pct,dl_mbps,ul_mbps,ping_at,bandwidth_at,context_verified,source) VALUES($1,$2,$2,$3,'peer','ipv4','main',1,0,100,100,now(),now(),true,'fixture')", [observationId, sid, iface])
@@ -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 { pool } from "../../db/index.js"
import { nativeBgpRows, bgpTemplateReferences, type NativeBgpRow } from "../../services/bgp-export-model.js"
@@ -15,6 +17,7 @@ import { routeContextClients } from "./routing-community-context-native.js"
import { pathProofTag } from "./path-proof-purpose.js"
import { assertManagementTransport, managementTransportClient, type ManagementTransportCapability } from "./management-transport.js"
import { assertRoutingNetworkSharedContextRead, inspectRoutingNetworkSharedContextRead, readRoutingNetworkSharedResource, routingNetworkSharedContextPublicationFence, type RoutingNetworkSharedContextRead } from "./routing-network-native-session.js"
import { scopedTargetRuleMatches } from "./routing-community-scoped-model.js"
export function routeContextAccess(context: ActionContext) {
return async () => {
@@ -25,7 +28,7 @@ export function routeContextAccess(context: ActionContext) {
}
}
export type RouteContextOptions = { authorize?: () => Promise<void>; signal?: AbortSignal; expectedSources?: Array<{ serverId: number; proof: string }>; selectedCandidateId?: string; managementTransport?: ManagementTransportCapability }
export type OwnedRouteCommunityTarget = { nativeId: string; rawRow: NativeBgpRow; endpoint: Readonly<RoutingEndpoint>; desiredTargetProof: (ruleBody: string) => string }
export type OwnedRouteCommunityTarget = { nativeId: string; rawRow: NativeBgpRow; endpoint: Readonly<RoutingEndpoint>; desiredTargetProof: (ruleBody: string) => string; matchesRuleProof: (ruleBody: string, proof: unknown) => boolean }
/** Fixed endpoint clients and private proofs are retained in closures. Public
* results contain no credentials, native rules, arbitrary comments or SQL rows. */
@@ -96,7 +99,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 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))
// Keep ordered VRF membership (first matching row wins), never equate an
// arbitrary policy table with the diagnostic VRF's same-name table.
@@ -140,7 +143,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 }))
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"])
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))
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 +180,7 @@ async function readContext(profile: RouteAiProfile, options: RouteContextOptions
let gateway: ReturnType<typeof routeGatewayContext>
try { gateway = routeGatewayContext(candidate.gateway) } catch { throw new McpError("STATE_UNAVAILABLE", "Gateway кандидата не подтверждён", 409) }
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)
} catch (error) { unavailableCandidate(candidate.id, error) }
}
@@ -232,13 +235,16 @@ async function readContext(profile: RouteAiProfile, options: RouteContextOptions
// untouched dependencies after its deliberate, exact body replacement.
const targetRuleProof = targetRule ? sign("target-rule:" + fingerprint(stableBgpPolicyRow(targetRule))) : null
const immutableTarget = targetRule ? structuredClone(targetRule) : null
const ownedTarget: OwnedRouteCommunityTarget | undefined = immutableTarget ? {
nativeId: immutableTarget[".id"], rawRow: structuredClone(immutableTarget), endpoint: Object.freeze({ ...endpoints.find(endpoint => endpoint.id === sourceId)! }),
desiredTargetProof(ruleBody) {
const ownedTarget: OwnedRouteCommunityTarget | undefined = immutableTarget ? (() => {
const desiredTargetProof = (ruleBody: string) => {
if (typeof ruleBody !== "string" || Buffer.byteLength(ruleBody, "utf8") > 65536) throw new McpError("LIMIT_EXCEEDED", "Тело scoped Community фильтра превышает лимит", 409)
return sign("target-rule:" + fingerprint(stableBgpPolicyRow({ ...immutableTarget, rule: ruleBody })))
},
} : undefined
}
return {
nativeId: immutableTarget[".id"], rawRow: structuredClone(immutableTarget), endpoint: Object.freeze({ ...endpoints.find(endpoint => endpoint.id === sourceId)! }),
desiredTargetProof, matchesRuleProof: (ruleBody: string, proof: unknown) => scopedTargetRuleMatches({ rawRow: immutableTarget, desiredTargetProof }, ruleBody, proof),
}
})() : undefined
const dependencies = rawProof.map(value => {
const snapshot = value as { path?: string; rows?: Record<string, unknown>[] }
return snapshot.path === "/routing/filter/rule" && snapshot.rows && targetRule ? { ...snapshot, rows: snapshot.rows.map(row => row[".id"] === targetRule[".id"] ? { ...row, rule: "[separately-sealed-owned-body]" } : row) } : value
@@ -250,6 +256,14 @@ async function readContext(profile: RouteAiProfile, options: RouteContextOptions
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 }
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 })),
verifyEvidence: (value: unknown, proof: unknown) => typeof proof === "string" && constantEqual(sign(pathProofTag("community") + fingerprint({ dependencyProof, targetRuleProof, value })), proof),
}
@@ -274,5 +288,5 @@ export async function readRouteCommunityContextWithSharedNativeSession(sharedCon
// The acquisition ends when this function returns. Later publication fences
// must use the owner rather than retain a completed transport-read permit.
return { before: result.before, fence: routingNetworkSharedContextPublicationFence(sharedContext),
ownedTarget: result.ownedTarget ? { nativeId: result.ownedTarget.nativeId, rawRow: structuredClone(result.ownedTarget.rawRow) } : undefined }
ownedTarget: result.ownedTarget ? { nativeId: result.ownedTarget.nativeId, rawRow: structuredClone(result.ownedTarget.rawRow), matchesRuleProof: result.ownedTarget.matchesRuleProof } : undefined }
}
@@ -1,4 +1,5 @@
import type { ChangeEvidence } from "./action-types.js"
import { isRoutingPolicySnapshot } from "./resources.js"
const record = (value: unknown): Record<string, unknown> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {}
const rules = (value: unknown) => Array.isArray(value) ? value.map(item => {
@@ -33,8 +34,13 @@ export function publicRoutingCommunityChanges(evidence: ChangeEvidence) {
/** Service recovery has the same private native bodies as its forward plan. */
export function publicMcpChangeEvidence(operationId: string, evidence: ChangeEvidence) {
const scopedRecovery = operationId === "operations.recover" && evidence.steps.length === 1 && evidence.steps[0].kind === "service" && evidence.steps[0].service === "routing.community.scoped.recover"
if (operationId === "routing.profiles.save") return {...evidence,steps:evidence.steps.map(step=>{if(step.kind!=="service")return step;const {baseline:_privateBaseline,...input}=step.input;return {...step,input}})}
return operationId === "routing.community.switch" || scopedRecovery ? publicRoutingCommunityChanges(evidence) : evidence
if (operationId === "routing.profiles.save" || operationId === "routing.controller.update") return {...evidence,steps:evidence.steps.map(step=>{if(step.kind!=="service")return step;const {baseline:_privateBaseline,...input}=step.input;return {...step,input}})}
if (operationId === "routing.community.switch" || scopedRecovery) return publicRoutingCommunityChanges(evidence)
if (evidence.healthChecks?.some(check => check.nextHopInterface)) evidence = { ...evidence, healthChecks: evidence.healthChecks.map(({ nextHopInterface: _privateInterface, ...check }) => check) }
if (!evidence.snapshots.some(snapshot => isRoutingPolicySnapshot(snapshot.resource))) return evidence
return { ...evidence, snapshots: evidence.snapshots.map(snapshot => isRoutingPolicySnapshot(snapshot.resource)
? { serverId: snapshot.serverId, resource: snapshot.resource, rowCount: snapshot.rows.length }
: snapshot) }
}
/** Public controller DTO is explicit. Private baseline/native proofs, seal
@@ -1,5 +1,8 @@
import assert from "node:assert/strict"
import { readFile } from "node:fs/promises"
import { test } from "node:test"
import { runInNewContext } from "node:vm"
import ts from "typescript"
import type { ActionContext } from "./action-types.js"
import { loadReservedScopedRecoverySource } from "./routing-community-scoped-recovery-source.js"
import { executeReservedScopedRecovery, scopedRecoveryIntentFromCheckpoint, validateReservedInverseVerified } from "./routing-community-scoped-recovery.js"
@@ -16,6 +19,67 @@ test("a serialized allocation cannot enter the shared inverse engine", async ()
await assert.rejects(executeReservedScopedRecovery(forged, {} as Parameters<typeof executeReservedScopedRecovery>[1]), /capability/)
})
test("confirmed inverse spend quarantines the exact slot on post-spend refusal without replay", async () => {
// Execute the actual boundary body with only its I/O dependencies substituted.
// This does not mint a production capability or claim native/SQL acceptance.
const source = ts.createSourceFile("routing-community-scoped-recovery.ts",
await readFile(new URL("./routing-community-scoped-recovery.ts", import.meta.url), "utf8"), ts.ScriptTarget.ES2022)
const declaration = source.statements.find(node => ts.isFunctionDeclaration(node) && node.name?.text === "executeReservedScopedRecovery")
assert(declaration)
const compiled = ts.transpileModule(declaration.getText(source), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText
const allocation = Object.freeze({ kind: "routing-reserved-inverse-allocation" })
const execution = Object.freeze({ kind: "routing-reserved-inverse-execution" })
const principal = { connectionId: "owned", credentialId: "original" }
const original = new Error("original authority failure"), result = Object.freeze({ outcome: "recovered" })
const scenarios = {
spend: ["spend"],
inspect: ["spend", "inspect", "quarantine"],
authority: ["spend", "inspect", "authority", "quarantine"],
source: ["spend", "inspect", "authority", "source", "quarantine"],
proof: ["spend", "inspect", "authority", "source", "proof", "quarantine"],
engine: ["spend", "inspect", "authority", "source", "proof", "engine", "quarantine"],
success: ["spend", "inspect", "authority", "source", "proof", "engine"],
} as const
for (const [scenario, expected] of Object.entries(scenarios)) {
const events: string[] = [], exports: Record<string, unknown> = {}, capabilities = new WeakMap()
let request: object | undefined
const at = (phase: string) => { events.push(phase); if (phase === scenario) throw original }
runInNewContext(compiled, {
exports, reservedExecutions: capabilities, fingerprint: JSON.stringify,
createRoutingReservationStore: () => ({
async beginInverseAttempt(value: unknown) {
assert.equal(value, allocation); at("spend"); return { reservationId: "spent-slot", execution }
},
async inspectInverseExecution(value: unknown) {
assert.equal(value, execution); at("inspect")
return { principal, sourceOperationId: "source", deadline: "2026-10-29T01:00:00.000Z" }
},
async markUnknown(id: string, lane: string) {
assert.equal(id, "spent-slot"); assert.equal(lane, "inverse"); at("quarantine")
throw new Error("quarantine persistence unavailable")
},
}),
effectiveMcpConnection: async () => { at("authority"); return { owner_id: "owner", policy: {}, owner_permissions: [] } },
loadReservedScopedRecoverySource: async () => {
at("source"); return { profile: { serverId: 1 }, input: {}, configuration: { packet: {} } }
},
accessGeneration: async () => "generation",
deadline: () => Date.parse("2026-10-29T01:00:00.000Z"),
sealedProof: async () => { at("proof"); return "proof" },
executeScopedRecovery: async (value: object) => {
request = value; assert.equal(capabilities.get(value), execution); at("engine"); return result
},
})
const execute = exports.executeReservedScopedRecovery as
(allocation: unknown, expected: { sourceOperationId: string; principal: typeof principal }) => Promise<unknown>
const outcome = execute(allocation, { sourceOperationId: "source", principal })
if (scenario === "success") assert.equal(await outcome, result)
else await assert.rejects(outcome, error => error === original)
assert.deepEqual(events, expected, scenario)
if (request) assert.equal(capabilities.has(request), false)
}
})
test("configuration equality or generic VERIFIED cannot replace reserved signed receipts", async () => {
const expected = {} as Parameters<typeof validateReservedInverseVerified>[1]
for (const row of [{}, { state: "verified", phase: "RECOVERY_VERIFIED", configurationVerified: true }, { inverse_intent_sealed: "x".repeat(524289) }]) {
File diff suppressed because it is too large. Load diff
Loaded 100 of 390 files, more files were not shown because too many files have changed in this diff. Show more