Skip to content

feat: add ext_proc filter crate with Envoy-compatible config#182

Open
usize wants to merge 21 commits into
praxis-proxy:mainfrom
usize:feat/ext-proc-skeleton
Open

feat: add ext_proc filter crate with Envoy-compatible config#182
usize wants to merge 21 commits into
praxis-proxy:mainfrom
usize:feat/ext-proc-skeleton

Conversation

@usize
Copy link
Copy Markdown
Member

@usize usize commented May 10, 2026

Introduce praxis-proxy-ext-proc as a standalone workspace crate at filter/ext-proc/, alongside filter/proto/. Isolated from the filter builtins to contain the gRPC/tonic/proto dependency surface.

The config mirrors Envoy's ExternalProcessor proto shape for easy migration from Envoy deployments:

  • target, failure_mode, message_timeout_ms
  • processing_mode (header/body/trailer send modes)
  • allow_mode_override, observability_mode, disable_immediate_response, max_message_timeout_ms

Features not yet implemented are parsed but rejected during validation with a clear error. The filter is not registered in FilterRegistry::with_builtins(); consumers register it explicitly via registry.register() or register_filters!.

Opus 4.6 aided in codebase exploration and feature implementation.

@usize
Copy link
Copy Markdown
Member Author

usize commented May 10, 2026

Experimenting with saving summarized agent session for prs: https://gist.github.com/usize/ebb03b9963383cf01f433bd5bf7719e3

@usize usize force-pushed the feat/ext-proc-skeleton branch 3 times, most recently from 1619fd4 to 031cb54 Compare May 14, 2026 23:16
@usize usize marked this pull request as ready for review May 14, 2026 23:19
@usize usize requested a review from a team May 14, 2026 23:19
@usize
Copy link
Copy Markdown
Member Author

usize commented May 14, 2026

@shaneutt Aside from config parsing there's not much happening here. Except it does establish the structure of the filter/ext-proc crate. I wanted to check your thoughts on the structure.

@praxis-bot praxis-bot marked this pull request as draft May 14, 2026 23:28
@praxis-bot
Copy link
Copy Markdown
Collaborator

Converted to draft: required checks failing.

@usize usize marked this pull request as ready for review May 15, 2026 00:16
@shaneutt shaneutt self-assigned this May 15, 2026
@shaneutt shaneutt moved this to Review in AI Gateway May 15, 2026
@shaneutt shaneutt added this to the v0.4.0 milestone May 15, 2026
@shaneutt shaneutt requested a review from twghu May 15, 2026 12:15
Comment thread filter/ext-proc/src/lib.rs Outdated
Comment thread filter/ext-proc/src/lib.rs
Comment thread filter/ext-proc/src/lib.rs
Comment thread filter/ext-proc/src/lib.rs
Comment thread filter/ext-proc/src/lib.rs
Comment thread filter/ext-proc/Cargo.toml Outdated
Comment thread Cargo.toml Outdated
@shaneutt shaneutt requested review from twghu and removed request for twghu May 19, 2026 13:29
@shaneutt shaneutt added the holding-pattern Waiting for discussions or contributor updates in order to proceed label May 19, 2026
@praxis-bot
Copy link
Copy Markdown
Collaborator

PR too large

This PR adds 1199 lines (limit: 500).

Large PRs are difficult to review and more likely to introduce subtle bugs. Please break this contribution into smaller, focused PRs that each address a single concern.

See our coding conventions for guidance.

If this PR legitimately requires a large change, a maintainer can add the skip/pr-hygiene label to skip this check.

@praxis-bot praxis-bot closed this May 19, 2026
@github-project-automation github-project-automation Bot moved this from Review to Done in AI Gateway May 19, 2026
@usize usize reopened this May 19, 2026
@praxis-bot praxis-bot closed this May 19, 2026
@usize usize added the skip/pr-hygiene PR size and description bypass label May 19, 2026
@usize usize reopened this May 19, 2026
@usize usize force-pushed the feat/ext-proc-skeleton branch from 836a254 to c2c6d3d Compare May 19, 2026 19:55
@usize usize requested a review from a team May 21, 2026 00:46
)
.into());
}
if cfg.message_timeout_ms == 0 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should this have an upper bound validation?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

As a matter of taste, while there is definitely a reasonable limit on how long a timeout should be... it's more a matter of taste than a lower bound. Should someone have a timeout of 3600 * 1000? Well.. no. Probably not. How about 300 * 1000? Still high, but seems murkier.

Comment thread filter/ext-proc/src/lib.rs
Comment thread filter/ext-proc/Cargo.toml Outdated
license.workspace = true
repository.workspace = true
readme = "../../README.md"
publish = true
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is premature until all approved

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
publish = true
publish = false

r#"
target: "http://127.0.0.1:50051"
message_timeout_ms: 500
max_message_timeout_ms: 5000
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With the change suggested for from_config() in lib.rs this should validate timeout.

let yaml: serde_yaml::Value = serde_yaml::from_str(
r#"
target: "http://127.0.0.1:50051"
deferred_close_timeout_ms: 10000
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

accepts_custom_status_on_error and accepts_custom_deferred_close_timeout only assert filter.name()

These tests verify that certain configs are accepted, but the only assertion is filter.name() == "ext_proc", which is true for any successfully built filter. They're really just "doesn't error" tests. That's a limitation of the private field / Box interface rather than a test bug, but it's worth noting the tests don't actually confirm the values were stored.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's true. I think that for a skeleton.. this seems okay? As the implementation proceeds, integration tests will hit the private fields. Or we can revisit and see if it's worth making it possible to inspect here.

Copy link
Copy Markdown

@twghu twghu left a comment

Choose a reason for hiding this comment

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

Some nits and suggestions

status_on_error: u16,

/// gRPC endpoint URI (retained for diagnostics).
target: String,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this field in the right location?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think it's worth having for logging purposes?

Comment thread filter/ext-proc/src/lib.rs Outdated
}

async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
tracing::trace!(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should be warn! instead, so if it ever gets triggered (for some reason) it's seen

Suggested change
tracing::trace!(
tracing::warn!(

Comment thread filter/ext-proc/src/tests.rs Outdated
Comment thread filter/ext-proc/Cargo.toml Outdated
license.workspace = true
repository.workspace = true
readme = "../../README.md"
publish = true
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
publish = true
publish = false

usize added 21 commits May 22, 2026 18:05
Introduce `praxis-proxy-ext-proc` as a standalone workspace crate
at filter/ext-proc/, alongside filter/proto/. Isolated from the
filter builtins to contain the gRPC/tonic/proto dependency surface.

The config mirrors Envoy's ExternalProcessor proto shape for easy
migration from Envoy deployments:

- target, failure_mode, message_timeout_ms
- processing_mode (header/body/trailer send modes)
- allow_mode_override, observability_mode,
  disable_immediate_response, max_message_timeout_ms

Features not yet implemented are parsed but rejected during
validation with a clear error. The filter is not registered
in FilterRegistry::with_builtins(); consumers register it
explicitly via registry.register() or register_filters!.

Opus 4.6 aided in codebase exploration and feature implementation.

Signed-off-by: usize <mofoster@redhat.com>
Reject status_on_error values outside the valid HTTP status code range
(100..=599) and message_timeout_ms of 0, which would cause immediate
gRPC timeouts.

Implementation assisted by Opus 4.6

Signed-off-by: usize <mofoster@redhat.com>
Error messages now show the snake_case YAML value the user typed
(e.g. 'buffered') instead of the Rust enum variant ('Buffered').

Implementation assisted by Opus 4.6

Signed-off-by: usize <mofoster@redhat.com>
Use 'filter\/ext-proc' instead of bare 'ext-proc' to avoid matching
unrelated workspace members in the future.

Implementation assisted by Opus 4.6

Signed-off-by: usize <mofoster@redhat.com>
tonic's Channel requires a tokio reactor during Drop, causing panics
when tests share threads without a runtime. Convert all tests that
call ExtProcFilter::from_config to #[tokio::test].

Also replace invalid_failure_mode_errors with invalid_target_uri_errors
since #[serde(default)] silently defaults unknown enum variants in
serde_yaml::from_value, making the original assertion untestable.

Implementation assisted by Opus 4.6

Signed-off-by: usize <mofoster@redhat.com>
failure_mode is a pipeline-level concern owned by FilterEntry, not
a filter-specific config field. strip_structural_keys() already
removes it from the YAML before it reaches from_config, so the
field always silently defaulted regardless of user input.

Remove the field from both the config struct and the filter struct.
The pipeline executor reads failure_mode from FilterEntry and
applies it via PipelineFilter, matching all other filters.

Also removes the now-unused praxis-core direct dependency.

Signed-off-by: usize <mofoster@redhat.com>
Adds tests for validation paths that had no coverage:

- response_header_mode: skip rejection
- response_trailer_mode: send rejection
- all response_body_mode variants (streamed, buffered,
  buffered_partial, full_duplex_streamed)
- allowed_override_modes with non-empty entries

Replaces the single-variant body mode tests with comprehensive
loops that cover all four BodySendMode variants for both request
and response directions.

Signed-off-by: usize <mofoster@redhat.com>
Proves that failure_mode flows through the pipeline infrastructure
rather than being parsed by the filter itself:

- failure_mode in filter YAML is stripped by parse_filter_config
- FilterEntry captures failure_mode: open and closed correctly
- FilterEntry defaults failure_mode to Closed when omitted
- FilterPipeline::build succeeds with ext_proc entries carrying
  different failure_mode values

Signed-off-by: usize <mofoster@redhat.com>
- Remove unused praxis-proto dependency from filter/ext-proc
- Update praxis-ext-proc version from 0.2.0 to 0.3.1 in workspace
  dependencies to match the workspace version

Signed-off-by: usize <mofoster@redhat.com>
This is a workspace-internal crate, not intended for crates.io.

Signed-off-by: usize <mofoster@redhat.com>
If the unimplemented on_request path ever fires in a real deployment
it should be visible in logs, not buried at trace level.

Signed-off-by: usize <mofoster@redhat.com>
Describe the rejection condition rather than just naming the field.

Signed-off-by: usize <mofoster@redhat.com>
The project convention requires inline format args. Field access
expressions are bound to locals first since format! does not
support dotted paths directly.

Signed-off-by: usize <mofoster@redhat.com>
The input type is inferred; only the return type annotation is
needed for the .into() conversion.

Signed-off-by: usize <mofoster@redhat.com>
Enables diagnostic formatting for the filter struct. All fields
including tonic::Channel implement Debug.

Signed-off-by: usize <mofoster@redhat.com>
All fields are Copy enums (HeaderSendMode, BodySendMode), so the
containing struct can be Copy too.

Signed-off-by: usize <mofoster@redhat.com>
Use explicit "not yet implemented" instead of "(skeleton)" which
was unclear to reviewers.

Signed-off-by: usize <mofoster@redhat.com>
Make the ExternalProcessorClient relationship explicit so the next
contributor knows praxis-proto needs to be added to Cargo.toml
when implementing the gRPC callout.

Signed-off-by: usize <mofoster@redhat.com>
Reject max_message_timeout_ms of 0 and values less than
message_timeout_ms, which would make the upper bound meaningless.
Negative values are caught by serde (u64), but a test guards
against future type changes.

Signed-off-by: usize <mofoster@redhat.com>
The Copy derive on ProcessingModeConfig triggered
trivially_copy_pass_by_ref — pass by value instead of reference.
Also reformats chained method calls per nightly rustfmt.

Signed-off-by: usize <mofoster@redhat.com>
The workspace migrated from serde_yaml to yaml_serde in praxis-proxy#381.
Cargo.lock for ext-proc needs to reflect the new package name.

Signed-off-by: usize <mofoster@redhat.com>
@usize usize force-pushed the feat/ext-proc-skeleton branch from c97e938 to 2018b23 Compare May 23, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

holding-pattern Waiting for discussions or contributor updates in order to proceed skip/pr-hygiene PR size and description bypass

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants