Tasks: Configurable Audit Log Storage Backend
Input: Design documents from docs/docs/specs/2026-06-18-audit-logs-local-storage/
Prerequisites: plan.md ✓, spec.md ✓, research.md ✓, data-model.md ✓, contracts/ ✓
Organization: Tasks are grouped by user story (US1 = local disk, US2 = S3, US3 = MongoDB fallback/migration).
Format: [ID] [P?] [Story] Description
- [P]: Can run in parallel (different files, no dependencies on incomplete tasks)
- [Story]: Which user story this task belongs to
Phase 1: Setup (Shared Infrastructure)
Purpose: Create the new file structure and install dependencies before any backend is implemented.
- T001 Create
ai_platform_engineering/utils/audit_backends/directory with__init__.py - T002 Create
ui/src/lib/audit/directory withindex.tsstub - T003 Create
ui/src/lib/audit/backends/directory placeholder - T004 Add
boto3to Python dependencies inpyproject.toml(if not already present) - T005 Add
@aws-sdk/client-s3toui/package.jsonand runnpm install
Phase 2: Foundational (AuditBackend Abstraction — Blocking)
Purpose: Define the protocol/interface and factory that all backends implement. ALL user-story phases depend on this phase.
⚠️ CRITICAL: No user story work can begin until this phase is complete.
-
T006 Implement
AuditBackendProtocol andget_audit_backend()factory inai_platform_engineering/utils/audit_backend.py- Protocol with single
write(event: Dict[str, Any]) -> Nonemethod - Factory reads
AUDIT_LOG_BACKENDenv var; raisesValueErrorfor unknown values - Module-level singleton with thread-safe double-checked locking
- Logs startup message:
[audit] backend=<name> ...(no credentials) - Default:
mongodbwhen env var is unset
- Protocol with single
-
T007 Implement
AuditBackendinterface andgetAuditBackend()factory inui/src/lib/audit/backend.ts- Interface with
write(event: Record<string, unknown>): void - Factory reads
AUDIT_LOG_BACKEND; throwsErrorfor unknown values - Module-level singleton (initialized on first call)
- Logs startup message via
console.info(no credentials) - Default:
mongodbwhen env var is unset
- Interface with
-
T008 [P] Add
AUDIT_LOG_BACKEND,AUDIT_LOG_LOCAL_PATH,AUDIT_LOG_S3_BUCKET,AUDIT_LOG_S3_PREFIX,AUDIT_LOG_S3_REGION,AUDIT_LOG_S3_ENDPOINT_URLtoui/src/lib/config.tsserver config -
T009 [P] Add same env vars to
ui/env.exampleandai_platform_engineering/env docs
Checkpoint: Abstraction is in place — backend implementations can now be built in parallel.
Phase 3: User Story 1 — Local Disk Backend (Priority: P1) 🎯 MVP
Goal: All audit events write to NDJSON files on local disk when AUDIT_LOG_BACKEND=local. No MongoDB connection required.
Independent Test: Start the stack with AUDIT_LOG_BACKEND=local AUDIT_LOG_LOCAL_PATH=/tmp/audit-test. Trigger a tool action. Confirm .ndjson files appear under /tmp/audit-test/YYYY/MM/DD/. No MongoDB error in logs.
-
T010 [US1] Implement
LocalBackendinai_platform_engineering/utils/audit_backends/local_backend.pywrite(event): serialize event to JSON line, append to<path>/YYYY/MM/DD/<type>-<YYYYMMDD>-<uuid>.ndjson- Create directory tree with
os.makedirs(exist_ok=True)on first write - UUID generated once at backend init (per-process stable file name)
- Catch all
OSError; log warning withlogger.warning; never raise - Serialize
datetimevalues to ISO-8601 strings
-
T011 [US1] Implement
LocalBackendinui/src/lib/audit/backends/local-backend.tswrite(event): serialize to JSON line, append to<path>/YYYY/MM/DD/<type>-<YYYYMMDD>-<uuid>.ndjson- Create directory with
fs.mkdir({recursive: true})on first write - UUID generated once at backend init
- Catch all errors; log with
console.warn; never throw - Serialize
Datevalues to ISO strings
-
T012 [US1] Wire
LocalBackendinto Python factory inai_platform_engineering/utils/audit_backend.py- Return
LocalBackend(path=AUDIT_LOG_LOCAL_PATH)whenAUDIT_LOG_BACKEND=local
- Return
-
T013 [US1] Wire
LocalBackendinto TypeScript factory inui/src/lib/audit/backend.ts- Return
new LocalBackend(path)whenAUDIT_LOG_BACKEND=local
- Return
-
T014 [US1] Swap
_persist_to_mongo(event)forget_audit_backend().write(event)inai_platform_engineering/utils/audit_logger.py- Remove
_persist_to_mongofunction - Remove
_ensure_indexesand all MongoDB-specific imports from this file - Existing
MongoBackend(T019) encapsulates that logic
- Remove
-
T015 [US1] Swap
persistAuthzDecisionToMongo+persistToUnifiedAuditEventscalls for a singlegetAuditBackend().write(...)call inui/src/lib/rbac/audit.ts- Merge the unified
audit_eventsshape (includeuser_email,type,sourcefields) - Keep both private persist functions but call them only from
MongoBackend(T020)
- Merge the unified
-
T016 [US1] Replace direct
getCollection('credential_audit_events').insertOne(...)withgetAuditBackend().write(...)inui/src/lib/credentials/audit.ts -
T017 [P] [US1] Write unit tests for
LocalBackend(Python) intests/test_audit_backend.py- Test: file created at correct path
- Test: second write appends (file not recreated)
- Test: directory auto-created
- Test: write error is caught and logged, does not raise
-
T018 [P] [US1] Write unit tests for
LocalBackend(TypeScript) inui/src/lib/audit/__tests__/local-backend.test.ts- Mock
fs/promises; verifymkdir+appendFilecalls - Test error path: FS error is caught, no throw
- Mock
Phase 4: User Story 2 — S3 Backend (Priority: P2)
Goal: All audit events write to a configured S3 bucket as gzip-compressed NDJSON when AUDIT_LOG_BACKEND=s3.
Independent Test: Configure AUDIT_LOG_BACKEND=s3 with a valid bucket (or local MinIO). Trigger events. Confirm objects appear in <prefix>/YYYY/MM/DD/. Confirm no MongoDB writes.
-
T019 [US2] Implement
S3Backendinai_platform_engineering/utils/audit_backends/s3_backend.pywrite(event): serialize event to JSON, gzip in-memory, PUT tos3://<bucket>/<prefix>/YYYY/MM/DD/<type>-<ts>-<uuid>.ndjson.gz- Build
boto3S3 client withendpoint_url=AUDIT_LOG_S3_ENDPOINT_URLif set (MinIO/GCS compat) - Standard credential chain (env vars, IRSA
AWS_ROLE_ARN/AWS_WEB_IDENTITY_TOKEN_FILE, instance profile) — no extra code needed - Catch all
ClientError/BotoCoreError; log warning; never raise
-
T020 [US2] Implement
S3Backendinui/src/lib/audit/backends/s3-backend.tswrite(event): serialize to JSON, gzip withzlib.gzip,PutObjectCommandto S3- Use
@aws-sdk/client-s3with optionalendpointoverride for MinIO/GCS - Standard credential provider chain (picks up
AWS_ROLE_ARN+AWS_WEB_IDENTITY_TOKEN_FILEfor IRSA automatically) - Catch all errors; log with
console.warn; never throw
-
T021 [US2] Wire
S3Backendinto Python factory inai_platform_engineering/utils/audit_backend.py- Return
S3Backend(bucket, prefix, region, endpoint_url)whenAUDIT_LOG_BACKEND=s3 - Raise
ValueErrorat factory time ifAUDIT_LOG_S3_BUCKETis unset
- Return
-
T022 [US2] Wire
S3Backendinto TypeScript factory inui/src/lib/audit/backend.ts- Return
new S3Backend(bucket, prefix, region, endpointUrl)whenAUDIT_LOG_BACKEND=s3 - Throw
Errorat factory time ifAUDIT_LOG_S3_BUCKETis unset
- Return
-
T023 [P] [US2] Write unit tests for
S3Backend(Python) intests/test_audit_backend.py- Mock
boto3.client; verifyput_objectcalled with correct key pattern and gzip content - Test: S3 error is caught and logged, does not raise
- Mock
-
T024 [P] [US2] Write unit tests for
S3Backend(TypeScript) inui/src/lib/audit/__tests__/s3-backend.test.ts- Mock
@aws-sdk/client-s3; verifyPutObjectCommandcalled with correct key and gzip body - Test error path: S3 error is caught, no throw
- Mock
Phase 5: User Story 3 — MongoDB Fallback (Priority: P3)
Goal: Extract existing MongoDB write logic into MongoBackend so the factory is complete and AUDIT_LOG_BACKEND unset/mongodb behaves exactly as before.
Independent Test: Leave AUDIT_LOG_BACKEND unset. Confirm audit events still reach audit_events and authorization_decision_records in MongoDB. Existing admin-audit-logs.test.ts suite passes unchanged.
-
T025 [US3] Implement
MongoBackendinai_platform_engineering/utils/audit_backends/mongo_backend.py- Extract logic from the old
_persist_to_mongoand_ensure_indexesinaudit_logger.pyverbatim write(event): inserts intoaudit_eventscollection (existing behaviour)
- Extract logic from the old
-
T026 [US3] Implement
MongoBackendinui/src/lib/audit/backends/mongo-backend.ts- Extract
persistAuthzDecisionToMongo+persistToUnifiedAuditEventslogic verbatim fromrbac/audit.ts write(event): dual-inserts intoauthorization_decision_records+audit_events(existing behaviour)
- Extract
-
T027 [US3] Wire
MongoBackendinto Python factory as default inai_platform_engineering/utils/audit_backend.py -
T028 [US3] Wire
MongoBackendinto TypeScript factory as default inui/src/lib/audit/backend.ts -
T029 [P] [US3] Update existing Python tests in
tests/test_audit_logger.py- Replace direct MongoDB mock with
mock.patch('ai_platform_engineering.utils.audit_backend.get_audit_backend') - Verify
backend.write()is called with the correct event dict; no directpymongomock needed
- Replace direct MongoDB mock with
-
T030 [P] [US3] Verify existing TypeScript tests in
ui/src/app/api/__tests__/admin-audit-logs.test.tsstill pass without modification (MongoDB read paths are unchanged)
Phase 6: Polish & Cross-Cutting
- T031 Export
get_audit_backend,AuditBackendfromai_platform_engineering/utils/audit_backends/__init__.py - T032 Export
getAuditBackend,AuditBackendfromui/src/lib/audit/index.ts - T033 Add
AUDIT_LOG_BACKENDand related vars todocker-compose/env files with defaults (mongodb) - T034 Add
AUDIT_LOG_BACKEND,AUDIT_LOG_LOCAL_PATH,AUDIT_LOG_S3_BUCKET,AUDIT_LOG_S3_PREFIX,AUDIT_LOG_S3_REGIONtocharts/ai-platform-engineering/charts/supervisor-agent/values.yamlunderextraEnv(commented-out examples) - T035 Add
serviceAccount.annotationsIRSA example (commented out) tocharts/ai-platform-engineering/charts/supervisor-agent/values.yaml - T036 Run
uv run ruff check ai_platform_engineering/utils/audit_backend.py ai_platform_engineering/utils/audit_backends/and fix any issues - T037 Run
npm run lintinui/and fix any TypeScript issues - T038 Update
ai_platform_engineering/utils/README to document the new backend abstraction and env vars
Dependencies
Phase 1 (Setup)
└─► Phase 2 (Abstraction — BLOCKS all)
├─► Phase 3 (US1 Local) ─────────┐
├─► Phase 4 (US2 S3) ──────────┤
└─► Phase 5 (US3 Mongo) ──────────┘
│
Phase 6 (Polish)
Phase 3, 4, and 5 can be executed in parallel once Phase 2 is complete. Within each phase, tasks marked [P] can run in parallel.
Parallel Execution (Phase 2 onwards)
Once T006 + T007 are done, the following can run concurrently:
- Stream A (Python local): T010 → T012 → T014 → T017
- Stream B (TypeScript local): T011 → T013 → T015 → T016 → T018
- Stream C (Python S3): T019 → T021 → T023
- Stream D (TypeScript S3): T020 → T022 → T024
- Stream E (MongoDB extraction): T025 → T027 → T029 / T026 → T028 → T030
Implementation Strategy
MVP (deliver US1 first — unblocks dev environment immediately):
- Phase 1 + Phase 2 (T001–T009)
- Phase 3 (T010–T018) — local disk backend, wire into both Python and TypeScript
- Smoke test:
AUDIT_LOG_BACKEND=localstack starts, events appear on disk
Increment 2 (US3 before US2 — ensures no regression):
- Phase 5 (T025–T030) — extract MongoBackend, verify existing tests pass
Increment 3 (US2 — production-ready):
- Phase 4 (T019–T024) — S3 backend
Polish (T031–T038) — after all backends verified.
Summary
| Phase | Story | Tasks | Notes |
|---|---|---|---|
| 1 Setup | — | T001–T005 | File structure + deps |
| 2 Foundation | — | T006–T009 | Abstraction layer |
| 3 Local disk | US1 P1 | T010–T018 | MVP |
| 4 S3 | US2 P2 | T019–T024 | Production |
| 5 MongoDB | US3 P3 | T025–T030 | No-regression |
| 6 Polish | — | T031–T038 | Helm, docs, lint |
| Total | 38 tasks |