feat(workflow): opt-in auto-propagation of workflow headers on allow-listed hosts (closes #186)#199
Merged
Conversation
…listed hosts (closes #186) Forge already accepted the X-Workflow-* / X-Invocation-Caller headers and stamped them on every audit event during an invocation (FWS-2 / #185), but propagation to downstream agents was manual: each tool calling out via HTTP had to invoke WorkflowContextFromContext(ctx).ApplyToHTTPHeaders(req.Header) itself. Auto-propagation was deliberately off to stop the workflow identity from leaking to third-party APIs — which broke the trace at every agent-to-agent hop where the tool author forgot the manual plumbing. This change keeps the safe default but adds an allow-list so operators can opt specific downstream hosts in. The forge.yaml block mirrors the egress allow-list shape (exact + wildcard suffix), the matcher reuses the same conventions, and the runner wraps the egress transport once at startup so every built-in HTTP tool inherits the auto-apply without per-tool changes. workflow_propagation: allowed_hosts: - "orchestrator.svc" - "*.agents.internal" Implementation details: - forge-core/types/config.go: new WorkflowPropagationConfig struct with AllowedHosts []string, attached to ForgeConfig under workflow_propagation. - forge-core/runtime/workflow_propagation.go: new WorkflowPropagationMatcher (exact + wildcard, port-stripped, case-insensitive — same shape as security.DomainMatcher but independent so runtime doesn't grow a security dep), plus a workflowPropagationTransport that wraps an http.RoundTripper. The wrapper clones the request before stamping headers (RoundTripper contract) and short-circuits to the underlying transport identity-equal when the matcher is empty (zero-overhead default-deploy path). - forge-cli/runtime/runner.go: builds the matcher from cfg.WorkflowPropagation.AllowedHosts and wraps the egress client's transport at construction time, so http_request, webhook_call, and web_search_* all inherit auto-apply. Pinned by TestWorkflowPropagationMatcher_{Matches,IsEmptyAndNilGuard}, TestWorkflowPropagationTransport_{AppliesHeadersOnAllowlistedHost, OmitsHeadersOnUnlistedHost,NoOpWhenContextIsZero, DoesNotMutateOriginalRequest,EndToEnd,PropagatesUnderlyingError}, TestWrapTransportForWorkflowPropagation_EmptyMatcherShortCircuits, TestNewWorkflowPropagationMatcher_RejectsBadInputCleanly. Subprocess egress proxy path is not covered — that's a separate concern (proxy-side header injection) and the issue scopes the in-process Go HTTP tools. Filing a follow-up if downstream agents running their HTTP via the subprocess proxy need the same auto-apply.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
workflow_propagation.allowed_hostsblock toforge.yaml. Outbound HTTP tool calls targeting a matching host automatically receive the workflow correlation headers (X-Workflow-Id/X-Workflow-Execution-Id/X-Workflow-Stage-Id/X-Workflow-Step-Id/X-Invocation-Caller) from the current request context. Hosts not on the list keep the pre-#186 opt-in posture — tools must callApplyToHTTPHeadersexplicitly. Empty / missing block = zero-overhead pass-through (current behavior).Why
Forge already accepted the workflow headers inbound (FWS-2 / #185) and stamped them on every audit event. But propagation to downstream agents was manual: each HTTP tool had to call
WorkflowContextFromContext(ctx).ApplyToHTTPHeaders(req.Header)itself. The skill correctly flagged that auto-propagation is risky (leaks workflow identity to third-party APIs likeapi.openai.com), but the manual approach broke the trace at every agent-to-agent hop where the tool author forgot.This change keeps the safe default and adds an opt-in allow-list — the same shape as the existing egress allow-list — so operators get auto-propagation inside their trust boundary without leaking outside it.
Implementation
forge-core/types/config.go— newWorkflowPropagationConfigwithAllowedHosts []string; attached toForgeConfigunderworkflow_propagation.forge-core/runtime/workflow_propagation.go— newWorkflowPropagationMatcher(exact + wildcard, port-stripped, case-insensitive). Wildcards match strictly-deeper subdomains —*.agents.internalmatchespayments.agents.internalbut NOT the bareagents.internal.WrapTransportForWorkflowPropagationreturns the underlying transport identity-equal when the matcher is empty, so zero-config deploys pay no per-request overhead.forge-cli/runtime/runner.go— builds the matcher fromcfg.WorkflowPropagation.AllowedHostsand wraps the egress client's transport once at startup. Every built-in HTTP tool (http_request,webhook_call,web_search_*) routes throughsecurity.EgressTransportFromContextso all of them inherit the auto-apply without per-tool changes.The wrapper clones the request before stamping headers (
http.RoundTrippercontract — must not mutate the caller's request) so a caller'sreq.Headeris never modified across retries.Safety
workflow_propagationblock (orallowed_hosts: []) returns the egress transport identity-equal — no wrapper, no header mutation, no observable change.api.openai.com) NEVER receives the workflow headers, even with a fully-populatedWorkflowContext. Pinned byTestWorkflowPropagationTransport_OmitsHeadersOnUnlistedHost.req.Headeris preserved. Pinned byTestWorkflowPropagationTransport_DoesNotMutateOriginalRequest.IsZero(direct A2A invocation, no orchestrator), even allow-listed hosts get nothing — we don't stamp empty values. Pinned byTestWorkflowPropagationTransport_NoOpWhenContextIsZero.TestWorkflowPropagationTransport_PropagatesUnderlyingError.Dependency
Builds on FORGE-2 (#185) — the propagated header set includes the new
X-Workflow-Execution-Idfrom that PR.Out of scope
security.EgressTransportFromContext. If that surface needs the same auto-apply, file a follow-up — needs proxy-side header injection, not in scope for this PR per the issue.Test plan
golangci-lint runacross all four modules — 0 issuesgofmt -wacross all modulesgo test ./...inforge-core/andforge-cli/— all greenhttptest.NewServer, underlying error propagation.forge runan agent withworkflow_propagation.allowed_hosts: ["127.0.0.1"], invoke a tool that POSTs to a local echo server, confirm the inbound request carries all five workflow headers; remove the entry, confirm headers are absent.