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_approvalCHECK.
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:
chk_no_self_approvalCHECK(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.trg_checker_proposals_guardBEFORE 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_recordedevents (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
platformschema in the consolidatedbankDB plusplatform_{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-3returns 501TIER_3_NOT_IMPLEMENTEDon 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 409IDEMPOTENCY_CONFLICT. - review_note mandatory on reject. A rejection without
review_notereturns 400VALIDATION_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_atdefault of 48h is platform-wide today). - Split runtime to
platform_app_userfor read endpoints; keepplatform_migrate_useronly for the ensureSchema cold-start.
Status¶
- Build: this commit.
- Deploy: pending CI green + SST deploy.