Phase 24 — Audit log signed URL export¶
Last updated: 2026-05-08
Phase 14 added an audit-log export endpoint that streams the materialized JSONL/JSON through FastAPI. For typical engagements that's fine — audit logs are usually <50 MB. But for very large engagements (thousands of LLM calls, deep-investigate-further loops) the streaming approach holds the HTTP request open while the API materializes shards, which is wasteful and timeout-prone.
Phase 24 adds a delivery=signed_url mode that materializes the audit log into a single S3 object and hands back a presigned URL. The download then happens directly from S3, bypassing the API entirely.
Endpoint¶
GET /auditforge/engagement/{id}/audit-log
?format=jsonl|json
&delivery=stream|signed_url # default: stream
&expires_in=600 # default: 600 sec; 60 ≤ expires_in ≤ 3600
delivery=stream (default) preserves the existing Phase 14 behavior — the response is streaming application/x-ndjson (jsonl) or a JSON body (json). delivery=signed_url materializes the assembled payload to S3 and returns:
{
"engagement_id": "eng-abc123",
"format": "jsonl",
"signed_url": "https://s3.amazonaws.com/...?X-Amz-Signature=...",
"size_bytes": 4_823_104,
"shard_count": 12,
"event_count": 1842,
"expires_in": 600,
"expires_at": "2026-05-08T13:30:00Z",
"s3_bucket": "metis-shared",
"s3_key": "auditforge/exports/eng-abc123-20260508T132000Z.jsonl",
"empty": false
}
Empty audit log → signed_url: null, empty: true, size_bytes: 0 (no S3 round-trip — saves a put on engagements that have not run yet).
Where the export lives¶
Materialized exports go to auditforge/exports/<engagement_id>-<YYYYMMDDTHHMMSSZ>.<ext> in the engagement's bucket (source_bucket if set; otherwise the platform shared bucket). One object per export request; timestamped to keep history if the same engagement is exported multiple times.
The bucket has versioning + AES-256 encryption + public-access-block (per Phase 7 bucket-provisioning standards), so the materialized export inherits those controls. The presigned URL grants read access to that single key for the requested duration only.
Auth + audit trail¶
Same auth as the streaming export: admin token or session token with engagement-scope access. The signed-URL response leaks the bucket and key, so for engagements with isolated buckets the URL itself reveals the partner's bucket name — that's by design (the partner needs to download from it). The presigned URL itself is the time-bounded credential.
A future iteration could add the export request to the engagement's audit log so "this URL was generated at T by user U" becomes part of the chain-of-custody trail. For now, the request is logged via auditforge_audit_log_signed_url_failed only on failure; success is silent.
UX¶
EngagementDetail header gets a small URL button next to the Audit log link. Clicking calls getAuditLogSignedUrl(engagement.id, "jsonl", 600), copies the resulting URL to the clipboard, and pops an alert with the expiry + size. Clipboard fails fall back to a prompt() dialog so the partner can manually copy.
Cost¶
Each signed-URL request does one S3 PUT of the materialized payload. For a typical 5 MB JSONL log, that's $0.000005 — well below threshold for any cost concern. The presigned URL itself is free.
Files¶
app/auditforge_endpoints.py—delivery+expires_inparams onGET /engagement/{id}/audit-log; signed-URL branch materializes payload, callss3.put_object+s3.generate_presigned_urlfrontend/src/api/auditforge.ts—getAuditLogSignedUrl+AuditLogSignedUrlResponsetypesfrontend/src/components/EngagementDetail.tsx—URLbutton + clipboard handlertests/test_auditforge_endpoints.py— 5 tests: invalid delivery 422, invalid expires_in 422, empty short-circuit, 404 missing engagement, happy-path with seeded shard