Skip to content

Egress: proxy audit events missing per-invocation seq (gap-detection blind spot) #341

Description

@initializ-mk

Problem

Follow-up to #338 / PR #339. Subprocess egress-proxy audit events ("source":"proxy") now carry task_id + correlation_id, but they still have no seq (the per-invocation monotonic sequence counter). Observed live:

{"event":"egress_blocked","correlation_id":"018840c40510fbb7",
 "task_id":"aibuilderdemo-1784401740725530000",
 "fields":{"domain":"initializ.ai","mode":"allowlist","source":"proxy"}}   ← no "seq"

Compare the in-process web_fetch egress event in the same invocation, which has "seq":8. Consumers detect lost/out-of-order events by walking seq within a (correlation_id, task_id) group (see docs/security/audit-logging.md#schema-contract), so proxy events are invisible to that gap-detection.

Root cause

seq is assigned by EmitFromContext pulling a per-invocation counter off the request context:

forge-core/runtime/audit.go:900
    if event.Sequence == 0 {
        event.Sequence = NextSequence(ctx)   // counter installed by EnsureSequenceCounter at request entry
    }

The egress proxy is a separate 127.0.0.1 forward proxy with no request ctx (that's the whole reason #338 had to recover task_id/correlation_id out-of-band from the proxy credentials). So it emits via plain Emit, which skips the counter. This is already documented as an intentional exception:

docs/security/audit-logging.md — "Egress proxy OnAttempt with source=proxy … seq + trace cross-link remain unavailable (no ctx)."

Unlike task_id/correlation_id, seq cannot simply ride in the proxy credentials: it's a live monotonic counter shared across all of an invocation's emissions, not a static value the subprocess can echo back. A stale/duplicated seq would be worse than none.

Options

  • Counter registry keyed by (correlation_id, task_id). The runner already recovers those two IDs on the proxy OnAttempt. If the per-invocation *atomic.Int64 counters are also reachable by that key (a registry the runner maintains, populated at request entry alongside EnsureSequenceCounter), the proxy callback can advance the real invocation counter and stamp a correct, gap-free seq. Needs a lifecycle for the registry (evict at invocation_complete).
  • Accept the gap, make it explicit. Keep proxy events seq-less but document/flag that source=proxy events are outside the seq chain, and have gap-detection tooling skip them rather than false-alarm. Lower effort; no correctness gain.

Acceptance criteria

  • Either: source=proxy events carry a correct, monotonic seq within their (correlation_id, task_id) group; or the seq-less-by-design contract is explicit enough that gap detection doesn't false-positive.
  • No regression to the Egress: subprocess proxy audit events missing task_id / correlation_id #338 attribution or the plain-Emit exceptions for genuinely scope-less events (startup banners, scheduler ticks).
  • docs/security/audit-logging.md updated to match whichever path is chosen.

Files

  • forge-cli/runtime/runner.go — proxy OnAttempt callback (where a registry lookup would live)
  • forge-core/runtime/audit.go / audit_schema.goNextSequence / EnsureSequenceCounter
  • docs/security/audit-logging.md — plain-Emit exception table + schema contract

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestforge-coreAffects the forge-core library (runtime, security, types, llm, mcp, auth)securitySecurity vulnerability fixes

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions