Skip to content

refactor(agent): forward workflow executor routes generically [PRD-567]#319

Open
matthv wants to merge 6 commits into
mainfrom
feature/prd-567-generic-executor-proxy
Open

refactor(agent): forward workflow executor routes generically [PRD-567]#319
matthv wants to merge 6 commits into
mainfrom
feature/prd-567-generic-executor-proxy

Conversation

@matthv

@matthv matthv commented Jun 16, 2026

Copy link
Copy Markdown
Member

What

The agent proxies the workflow executor with one explicitly declared route per executor route (GET /_internal/workflow-executions/:run_id, POST .../trigger). Each new executor route therefore requires a matching agent route and an agent release — and the agent is customer-deployed, so a new executor capability is unreachable until the parc is upgraded.

This replaces the two hand-written routes with a single catch-all that forwards any sub-path and any verb to the executor's /runs/* namespace. Adding an executor route no longer requires any agent change.

fixes PRD-567

Design (decisions agreed with the ticket author)

  • Catch-all ALL /_internal/workflow-executions/*path/runs/<path>, all verbs.
  • Request forwarded verbatim: all client headers (minus hop-by-hop / Host / Content-Length), raw body (no reshaping), query string and method as-is.
  • Security boundary: the wildcard can only ever map into /runs — path-traversal (..), encoded dots (%2e), absolute paths and null bytes are rejected, so executor routes outside /runs (e.g. mcp-oauth-credentials) stay unreachable through the proxy.
  • ForestController passes the raw request context (method/query_string/body) to the route closure; this is additive — other routes ignore the extra keys.
  • Single large timeout (no per-verb), buffered, executor errors pass through verbatim.

Tests

rspec workflow_executor_proxy_spec22/22, including 7 path-traversal security cases (the "internal route not exposed" guard). Rubocop clean. The one pre-existing forest_controller_spec failure (exception_handler prod logging) is unrelated (fails on main too).

Scope

Phase 1 of 3 — this is the Ruby v2 agent. The Node @forestadmin/agent and the Ruby v1 liana (forest-rails) carry the identical coupling and will get the same generic proxy in follow-up PRs.

🤖 Generated with Claude Code

Note

Replace explicit workflow executor routes with a generic catch-all proxy in WorkflowExecutorProxy

  • Replaces two specific GET/POST routes with a single catch-all route (/_internal/workflow-executions/*path) that forwards any HTTP verb and sub-path to the executor's /runs/* endpoint.
  • Forwards raw request bodies (no JSON reshaping), preserves query strings verbatim, and forwards all headers except hop-by-hop/host/body-framing headers; executor response headers are also returned to the client.
  • Adds path safety validation in unsafe_path? to reject traversal, encoded-dot, backslash, and null-byte paths with a NotFoundError.
  • Updates ForestController to pass method, query_string, and body to route closures, and to merge top-level data[:headers] into the HTTP response.
  • Risk: all proxied requests now use a unified 120s timeout (previously verb-dependent), and Faraday transport errors (including SSL) now map to 503 instead of propagating.

Macroscope summarized 42f1bc9.

Known limitation (by design)

This runtime is JSON-only: responses are rendered as JSON (render json: ... via ForestController). A hypothetical non-JSON executor response (e.g. text/plain) would not pass through verbatim. The executor only returns JSON today, so this does not affect the PRD-567 contract in practice; the @forestadmin/agent Node core (agent-nodejs#1666) handles raw passthrough.

@linear-code

linear-code Bot commented Jun 16, 2026

Copy link
Copy Markdown

PRD-567

@matthv matthv force-pushed the feature/prd-567-generic-executor-proxy branch from a9e0562 to 0cff271 Compare June 16, 2026 14:15
The agent proxied the workflow executor with one explicitly declared route
per executor route, so every new executor route required a matching agent
route and release. Replace the two hand-written routes with a single
catch-all under /_internal/workflow-executions/* that forwards any sub-path
and any verb to the executor's /runs/* namespace.

- request forwarded verbatim: all client headers (minus hop-by-hop / host /
  content-length), raw body (no reshaping), query string and method as-is
- security boundary: the wildcard can only map into /runs (path-traversal,
  encoded-dot and absolute-path inputs are rejected), so executor routes
  outside that namespace stay unreachable through the proxy
- ForestController passes the raw request context (method/query/body) to the
  route closure; other routes ignore the extra keys

fixes PRD-567

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@matthv matthv force-pushed the feature/prd-567-generic-executor-proxy branch from 0cff271 to 5375e20 Compare June 16, 2026 14:20
matthv and others added 2 commits June 22, 2026 10:56
…oxy [PRD-567]

The generic proxy only relayed Content-Type, so response headers set by the
executor (e.g. X-Forest-Executor-Version) were dropped — breaking the version
gate. Forward every executor response header except hop-by-hop ones, symmetric
to the request-header policy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…[PRD-567]

Generalize the response-header forwarding rationale: the agent forwards every
executor header transparently so new executor headers never need an agent
change. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eaders [PRD-567]

Review follow-up on the workflow executor proxy:
- ForestController now honors a root data[:headers] so executor response headers
  (e.g. X-Forest-Executor-Version) reach the HTTP response instead of being
  silently dropped — the proxy returns headers at the root, but the controller
  only read content[:headers] (always nil for the proxy). Additive; other routes
  are unaffected. Covered by a new controller-level spec.
- skip accept-encoding (request) and content-encoding (response): the body is
  re-serialized, so upstream encoding/length no longer match, and forwarding
  accept-encoding disabled Faraday's transparent gzip decompression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qltysh

qltysh Bot commented Jun 23, 2026

Copy link
Copy Markdown

1 new issue

Tool Category Rule Count
qlty Structure Function with high complexity (count = 7): forwarded_response_headers 1

next if value.nil? || value.to_s.empty?

acc[name.to_s] = value.to_s
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with high complexity (count = 7): forwarded_response_headers [qlty:function-complexity]

matthv and others added 2 commits June 23, 2026 10:30
…le [PRD-567]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oxy comments [PRD-567]

Review follow-up:
- forward now rescues Faraday::Error (SSL, etc.) as ServiceUnavailableError, so a
  transport failure surfaces as 503 like ConnectionFailed instead of a generic 500.
  Faraday never raises on executor 4xx/5xx (no raise_error middleware), so this only
  catches reachability failures.
- fix a self-contradictory SKIPPED_HEADERS comment ("re-serialized" vs raw body) and
  two minor comment imprecisions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant