Skip to content

MOD-168 — Maker-checker enforcement engine

Purpose

Platform-wide maker-checker enforcement engine. Single shared service for every back-office module that needs four-eyes authorisation on consequential state-changing commands. Centralises the proposal table, approval workflow, audit trail, and cross-module approval queue surface.

System domain: SD07 (bank-platform). ADRs: ADR-059 (single shared service, replacing per-module proposal tables).

Scope (v1, this build)

  • TIER-2 commands end-to-end: submit → approve / reject / expire / withdraw, with audit events on every transition and system-decision events on approve / reject.
  • TIER-3 is stubbed — submit and approve return 501 TIER_3_NOT_IMPLEMENTED. FR-803's v2 clause governs the full implementation with MOD-062 task-token review-window.
  • Three policy obligations fully satisfied:
  • GOV-003 CHECKER — second-line approval; APPROVED reachable only from PENDING.
  • DT-012 LOG — append-only command context; one-way status transitions; every transition audited.
  • GOV-005 CHECKER — self-approval blocked at both the application layer and the DB chk_no_self_approval CHECK.

Architecture

   POST /checker/proposals (staff JWT)
   ┌──────────────────────────────────────────┐
   │ API Gateway HTTP API (JWT authoriser:    │
   │   staff Cognito pool, MOD-104 SSM)       │
   └──────────────────┬───────────────────────┘
   ┌──────────────────────────────────────────┐
   │ handler Lambda (VPC, platform_migrate)   │
   │   route → submit | list | get |          │
   │           approve | reject               │
   │   - INSERT/UPDATE platform.checker_proposals │
   │   - emit checker_proposal_<transition>   │
   │   - emit system_decision_recorded        │
   │     (approve/reject only)                │
   └──────────────────┬───────────────────────┘
                      │ PutEvents
   ┌──────────────────────────────────────────┐
   │ bank-platform EventBridge bus            │
   │   ↓ checker_proposal_* → MOD-047 audit   │
   │   ↓ system_decision_recorded → MOD-048   │
   └──────────────────────────────────────────┘

   Cron rate(5m) ▶ expiry-sweeper Lambda
        │  UPDATE PENDING past expires_at → EXPIRED
        │  emit checker_proposal_expired (audit only)
   platform.checker_proposals (consolidated `bank` DB)

Data model

platform.checker_proposals in the consolidated bank Neon DB, owned by platform_migrate_user. Columns + indexes per wiki spec. Two DB-level invariants:

  1. chk_no_self_approval CHECK (proposed_by IS DISTINCT FROM reviewed_by) — the GOV-005 backstop. Uses IS DISTINCT FROM so a pending row with NULL reviewed_by passes; once set, it must differ from proposed_by.
  2. trg_checker_proposals_guard BEFORE UPDATE trigger — aborts on mutation of any immutable column (id, command_type, command_domain, tier, target_entity_*, command_payload, summary, proposed_by, proposed_at, idempotency_key, created_at, expires_at, trace_id, jurisdiction) and on any status transition outside the four legal forwards (PENDING → APPROVED / REJECTED / EXPIRED / WITHDRAWN).

Both invariants live in src/shared/db-setup.ts#SETUP_DDL and are mirrored by application-layer guards in src/shared/transitions.ts#LEGAL_TRANSITIONS + checkSelfApproval. The policy tests assert both layers agree.

Module type

Hybrid (IaC for API Gateway + Lambda + EventBridge schedule + SSM contract; Lambda for the request handlers + expiry sweeper).

Dependencies

  • MOD-047 — Agent action logger; consumes the bank-platform.checker_proposal_* audit events off the bank-platform EventBridge bus.
  • MOD-048 — System decision log; consumes the bank-platform.system_decision_recorded events (approve / reject only per FR-802).
  • MOD-062 — Workflow orchestration (Step Functions). Marked optional in MOD-168.yaml; v1 does not use it. v2 wires the TIER-3 review-window via task-token pause/resume.
  • MOD-103 — Neon database platform; provisions the platform schema in the consolidated bank DB plus platform_{app_user,migrate_user,readonly} roles.
  • MOD-104 — AWS shared infrastructure; provides VPC + subnets, KMS keys, EventBridge bus ARN, staff Cognito pool ID (MOD-044's user pool, used by the API Gateway JWT authoriser).

SSM outputs (downstream contract)

Path Value Consumed by
/bank/{env}/mod168/api-url HTTP API base URL Back-office app (SD08), all integrating modules
/bank/{env}/mod168/handler/lambda-arn Handler Lambda ARN Ops, cross-account invokes
/bank/{env}/mod168/handler/lambda-name Handler Lambda name Ops
/bank/{env}/mod168/sweeper/lambda-arn Sweeper Lambda ARN Ops manual invoke for emergency catch-up
/bank/{env}/mod168/audit-table platform.checker_proposals Compliance reports, ad-hoc readonly queries

Or via the typed contract package: @bank-platform/mod-168-maker-checker-enforcement/contract/ssm.

Policy obligations and tests

Policy Mode Test file Assertion
GOV-003 CHECKER __tests__/policy/gov-003-checker.test.ts APPROVED + REJECTED reachable only from PENDING; no terminal can re-transition to APPROVED
DT-012 LOG __tests__/policy/dt-012-log.test.ts SETUP_DDL declares the immutability guards for all 15 append-only columns + lists exactly the four legal status transitions
GOV-005 CHECKER __tests__/policy/gov-005-checker.test.ts checkSelfApproval returns the canonical 422 envelope; chk_no_self_approval CHECK constraint is declared with IS DISTINCT FROM (NULL-safe)

Integration suite (__tests__/integration/proposal-lifecycle.test.ts) exercises the live DB invariants under RUN_INTEGRATION=1.

API surface

See MOD-168-maker-checker-enforcement/docs/CONSUMERS-GUIDE.md. Five endpoints under /checker/proposals with the stable error-code catalogue. JWT authorisation via the shared staff Cognito pool.

Constraints

  • TIER-3 stubbed in v1. A registration with tier: TIER-3 returns 501 TIER_3_NOT_IMPLEMENTED on both submit and approve. Consumers must register as TIER-2 (sacrificing the review window) or wait for v2. The orchestrator's ruling: don't register a TIER-3 command as TIER-2 to work around the stub — the review window is the control.
  • No prod runtime guards. Self-approval is blocked unconditionally. There is no role, env var, or feature flag that can bypass either layer of the GOV-005 enforcement.
  • Idempotency required on submit. Every submit must carry idempotency_key. Re-submit with the same key returns the prior proposal (200, not 201); a conflicting body returns 409 IDEMPOTENCY_CONFLICT.
  • review_note mandatory on reject. A rejection without review_note returns 400 VALIDATION_FAILED. Approval notes are optional.

Open follow-ups (v2)

  • FR-803 v2 implementation: TIER-3 review window via MOD-062 Step Functions task-token pause/resume. Strip the 501 stub on both submit and approve paths.
  • Per-command-type configurable expiry window (expires_at default of 48h is platform-wide today).
  • Split runtime to platform_app_user for read endpoints; keep platform_migrate_user only for the ensureSchema cold-start.

Status

  • Build: this commit.
  • Deploy: pending CI green + SST deploy.