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.tsproves UPDATE/DELETE returnpermission_deniedforbank_platform_app_user. - 17 live verification assertions via
scripts/verify-deployment.mjs.
Operational notes¶
- PII handling.
input_summaryandoutput_summaryare 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_userdeleting 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.