Skip to content

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.pydelivery + expires_in params on GET /engagement/{id}/audit-log; signed-URL branch materializes payload, calls s3.put_object + s3.generate_presigned_url
  • frontend/src/api/auditforge.tsgetAuditLogSignedUrl + AuditLogSignedUrlResponse types
  • frontend/src/components/EngagementDetail.tsxURL button + clipboard handler
  • tests/test_auditforge_endpoints.py — 5 tests: invalid delivery 422, invalid expires_in 422, empty short-circuit, 404 missing engagement, happy-path with seeded shard