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
Files
forge-cli/runtime/runner.go — proxy OnAttempt callback (where a registry lookup would live)
forge-core/runtime/audit.go / audit_schema.go — NextSequence / EnsureSequenceCounter
docs/security/audit-logging.md — plain-Emit exception table + schema contract
Problem
Follow-up to #338 / PR #339. Subprocess egress-proxy audit events (
"source":"proxy") now carrytask_id+correlation_id, but they still have noseq(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_fetchegress event in the same invocation, which has"seq":8. Consumers detect lost/out-of-order events by walkingseqwithin a(correlation_id, task_id)group (seedocs/security/audit-logging.md#schema-contract), so proxy events are invisible to that gap-detection.Root cause
seqis assigned byEmitFromContextpulling a per-invocation counter off the request context:The egress proxy is a separate
127.0.0.1forward proxy with no request ctx (that's the whole reason #338 had to recovertask_id/correlation_idout-of-band from the proxy credentials). So it emits via plainEmit, which skips the counter. This is already documented as an intentional exception:Unlike
task_id/correlation_id,seqcannot 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
(correlation_id, task_id). The runner already recovers those two IDs on the proxyOnAttempt. If the per-invocation*atomic.Int64counters are also reachable by that key (a registry the runner maintains, populated at request entry alongsideEnsureSequenceCounter), the proxy callback can advance the real invocation counter and stamp a correct, gap-free seq. Needs a lifecycle for the registry (evict atinvocation_complete).source=proxyevents are outside the seq chain, and have gap-detection tooling skip them rather than false-alarm. Lower effort; no correctness gain.Acceptance criteria
source=proxyevents carry a correct, monotonicseqwithin their(correlation_id, task_id)group; or the seq-less-by-design contract is explicit enough that gap detection doesn't false-positive.Emitexceptions for genuinely scope-less events (startup banners, scheduler ticks).docs/security/audit-logging.mdupdated to match whichever path is chosen.Files
forge-cli/runtime/runner.go— proxyOnAttemptcallback (where a registry lookup would live)forge-core/runtime/audit.go/audit_schema.go—NextSequence/EnsureSequenceCounterdocs/security/audit-logging.md— plain-Emitexception table + schema contract