Skip to content

MOD-047 — Agent action logger

Purpose

Append-only audit ledger for every action — AI agent (FR-285) AND human staff (back-office). Every action becomes a row in audit.agent_actions with required correlation/identity/payload fields, append-only at the SQL layer (REVOKE UPDATE/DELETE), 7-year retention. Daily summary published as CloudWatch metrics for anomaly detection (FR-464).

FR scope: FR-285, FR-286, FR-287, FR-288, FR-462, FR-463, FR-464, FR-465, NFR-013, NFR-019, NFR-024.

Policies satisfied (LOG mode): GOV-005 (FAR accountability), GOV-006 (internal audit), AML-001 (AML programme execution), CON-002 (complaints / IDR).

Architecture

   Producer services
   (back-office, AI agent runtimes, MOD-001..)
   EventBridge bank-platform bus
   ┌────────┴───────────┐
   │ agent.action_taken │
   │ staff.action_taken │
   └────────┬───────────┘
   ┌────────────────────────┐
   │ action-logger Lambda   │  ≤1.5s p99 (FR-285)
   │  1. ensureSchema       │
   │  2. validate (FR-462,  │
   │     FR-465)            │
   │  3. INSERT             │
   └────────┬───────────────┘
   audit.agent_actions  (append-only, 7y retention)


   ┌────────────────────────┐  daily 14:00 UTC = 02:00 NZST
   │ daily-summary Lambda   │  (FR-464)
   │  → BankPlatform/       │
   │    AgentActions metrics│
   └────────────────────────┘

What MOD-047 owns

Resource Purpose
audit.agent_actions table Append-only Postgres ledger, all required fields NOT NULL, CHECK constraints enforce actor-XOR + write-type-attribution
action-logger Lambda Receives EB events, validates, inserts
daily-summary Lambda + EventBridge schedule FR-464 — once-daily aggregation → CloudWatch metrics
2× EventBridge rules agent.action_taken, staff.action_taken on bank-platform bus
3× CloudWatch alarms error rate, p99 latency, daily summary failure
5× SSM downstream contract paths Lambda pointers, audit table FQN, CloudWatch namespace

Schema — audit.agent_actions

Column Notes
id uuid PK Generated on insert if absent
trace_id, correlation_id NOT NULL (FR-462, FR-463)
module_id, jurisdiction, event_type NOT NULL
actor_kind XOR-checked against agent_id/staff_id
agent_id / staff_id exactly one set per actor_kind
action_type WRITE_* and DECISION_* are write-type → FR-465 attribution required
input_summary, output_summary NOT NULL — caller's responsibility to redact PII
duration_ms NOT NULL ≥ 0
level NOT NULL
party_id / account_id nullable BUT write-type CHECK requires at least one
occurred_at, recorded_at timestamptz, both default now()

Indexes: trace_id, correlation_id, (actor_kind, actor_id, occurred_at DESC), partial on party_id, account_id, module_id.

Append-only enforcement

REVOKE UPDATE, DELETE ON audit.agent_actions FROM bank_platform_app_user. Live policy test (__tests__/policy/gov-immutability.test.ts) proves it. NFR-024 / FR-286 / GOV-005/006 / AML-001 / CON-002 all rely on this single SQL-level guarantee.

SSM contract

Read

Path Owner
/bank/{env}/network/vpc-id, /private-subnet-ids MOD-104
/bank/{env}/kms/operational/arn MOD-104
/bank/{env}/eventbridge/bank-platform/arn MOD-104
/bank/{env}/sns/alerts/arn MOD-104
/bank/{env}/neon/*, bank-neon/{env}/bank_platform/* MOD-103

Write

Path Value
/bank/{env}/mod047/lambda-arn, /lambda-name Action logger pointers
/bank/{env}/mod047/summary-lambda-name Daily summary Lambda
/bank/{env}/mod047/audit-table audit.agent_actions
/bank/{env}/mod047/cloudwatch-namespace BankPlatform/AgentActions

Producer contract

Any module that performs an auditable action publishes an EventBridge event:

{
  "Source": "bank.<domain>",
  "DetailType": "agent.action_taken" | "staff.action_taken",
  "EventBusName": "/bank/{env}/eventbridge/bank-platform",
  "Detail": {
    "trace_id": "...",
    "correlation_id": "...",
    "module_id": "MOD-NNN",
    "jurisdiction": "NZ" | "AU" | "XX",
    "event_type": "domain.specific.event",
    "actor_kind": "agent" | "staff" | "system",
    "agent_id": "...",          // when actor_kind=agent
    "staff_id": "...",          // when actor_kind=staff
    "action_type": "READ_BALANCE" | "WRITE_*" | "DECISION_*",
    "input_summary": "PII-redacted summary",
    "output_summary": "PII-redacted summary",
    "duration_ms": 12,
    "level": "INFO",
    "party_id": "...",          // required for WRITE_* / DECISION_*
    "account_id": "...",        // alternative for WRITE_*
    "occurred_at": "2026-04-28T...Z"
  }
}

The Lambda validates per FR-462 (mandatory fields) + FR-465 (write-type attribution). Validation errors are re-thrown so EventBridge's 3-attempt retry surfaces persistent producer bugs.

Tests

  • 20 unit tests — required-field validation, actor-XOR, write-type attribution (FR-465), error message shape, event-pattern config invariants.
  • Live policy test — gov-immutability.test.ts proves UPDATE/DELETE return permission_denied for bank_platform_app_user.
  • 17 live verification assertions via scripts/verify-deployment.mjs.

Operational notes

  • PII handling. input_summary and output_summary are free-text. Producers MUST redact PII before publishing — MOD-047 doesn't try to mask. The audit log is consulted by compliance, not for general-purpose customer-facing reporting.
  • 7-year retention. Out-of-scope cleanup task: a scheduled Lambda or pg_cron job runs as migrate_user deleting rows older than 7 years. Add when audit volume crosses the relevance threshold.
  • Query API for auditors (FR-287). Not yet a separate Lambda; auditors query the table directly via a read-only role for V1. When MOD-076 dashboards or a dedicated audit UI need a programmatic interface, expose a Lambda behind MOD-075.