Skip to content

feat: build apisix-runtime with ngx_http_ffi_client - #480

Closed
shreemaan-abhishek wants to merge 3 commits into
api7:masterfrom
shreemaan-abhishek:feat/apisix-runtime-ngx-http-ffi-client
Closed

feat: build apisix-runtime with ngx_http_ffi_client#480
shreemaan-abhishek wants to merge 3 commits into
api7:masterfrom
shreemaan-abhishek:feat/apisix-runtime-ngx-http-ffi-client

Conversation

@shreemaan-abhishek

@shreemaan-abhishek shreemaan-abhishek commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What this does

Adds ngx_http_ffi_client to apisix-runtime.

It is the C HTTP client the AI plugins use for outbound LLM requests. On the
paired gateway PRs (apache/apisix and api7/api7-ee-3-gateway), ai-proxy,
ai-proxy-multi and ai-request-rewrite prefer it over lua-resty-http and
spend roughly a third of the outbound CPU time doing so. The module has to be in
the runtime for any of that to take effect.

The private-repository problem

api7/ngx_http_ffi_client is still private and carries no tags, so:

  • it is pinned by commit through ngx_http_ffi_client_ver;
  • it is fetched with NGX_HTTP_FFI_CLIENT_TOKEN, which reaches the container as
    a BuildKit secret rather than a build arg, so it stays out of image history,
    and the fetch runs with the trace off so it stays out of the build log;
  • a build without the token prints a warning and leaves the module out.

The gateway side resolves the client at runtime and falls back to
lua-resty-http when the module is absent, so a runtime built either way works.
Only the outbound CPU cost differs.

That makes this reviewable and safe today, but it is a workaround. A public
runtime whose contents depend on a secret is not a good end state. The clean
fix is to make api7/ngx_http_ffi_client public, after which the token, the
BuildKit secret and the conditional can all be dropped for a plain
git clone --depth=1 -b <tag> like every other module here. Happy to redo it
that way if that is the direction.

Why this runtime and not api7ee-runtime

The module uses lua-nginx-module's public co-ctx API
(ngx_http_lua_get_co_ctx_data, ngx_http_lua_set_co_ctx_cleanup,
ngx_http_lua_cleanup_co_ctx_pending_operation), which landed in 0.10.29.

  • apisix-runtime builds OpenResty 1.29.2.4, which bundles ngx_lua 0.10.31rc2.
    It has them.
  • release/api7ee-runtime is pinned to OpenResty 1.21.4.4, which bundles
    0.10.25. It does not, and the module fails to compile there with three
    implicit-declaration errors.

Both the gateway repos pin APISIX_RUNTIME=1.3.11, which is this runtime, so
this branch is the one that matters.

Verification

  • Built OpenResty 1.29.2.4 with --add-module pointing at the pinned commit:
    compiles clean, and the module's own Test::Nginx suite passes in full
    (415 assertions across 15 files).
  • Built the same way against OpenResty 1.21.4.4 to confirm the version floor
    above: fails on the three missing symbols.
  • Confirmed the authenticated fetch of the pinned commit works and leaves no
    credential in the resulting .git/config.
  • Confirmed the BuildKit secret plumbing end to end: with the secret the token
    reaches the RUN step, without it the step sees an empty value and the build
    continues, and docker history on the resulting image contains no trace of it.
  • Ran make build-apisix-runtime-deb with a real token to check the wiring
    through the Makefile and Dockerfile.

Summary by CodeRabbit

  • New Features

    • Added optional support for the private ngx_http_ffi_client module when a valid access token is provided during runtime builds.
    • Runtime packages and Docker builds can now include the module across Debian and RPM targets.
    • Builds without the token continue successfully and use lua-resty-http instead.
  • Documentation

    • Added build instructions, configuration details, fallback behavior, and information about module version pinning.
  • Bug Fixes

    • Release packaging now validates that the required access token is configured before building packages.

ai-proxy, ai-proxy-multi and ai-request-rewrite send every outbound LLM request
through ngx_http_ffi_client when the runtime carries it, and cost about a third
of the outbound CPU time they do on lua-resty-http. The module has to be in the
runtime for that to happen.

api7/ngx_http_ffi_client is still private and carries no tags, so it is pinned
by commit and fetched with NGX_HTTP_FFI_CLIENT_TOKEN. The token reaches the
container as a BuildKit secret rather than a build arg, so it stays out of image
history, and the fetch runs with the trace off so it stays out of the build log.
A build without the token warns and leaves the module out; ai-proxy falls back
to lua-resty-http on such a runtime, so both runtimes work.

The module compiles against lua-nginx-module's public co-ctx API, which arrived
in 0.10.29. OpenResty 1.29.2.4 bundles 0.10.31rc2 and has it.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7f33f9ea-945d-4d92-8809-dc1998d43465

📥 Commits

Reviewing files that changed from the base of the PR and between 74ead74 and 1f18875.

📒 Files selected for processing (1)
  • .github/workflows/release-apisix-runtime.yml

📝 Walkthrough

Walkthrough

Runtime packaging now supports the optional private ngx_http_ffi_client module. Build workflows pass its token through BuildKit. The runtime build script fetches or uses a local module copy, configures OpenResty, and installs the Lua binding when available. Release jobs validate the token.

Changes

FFI client module packaging

Layer / File(s) Summary
Token propagation and BuildKit wiring
.github/workflows/package-apisix-runtime-*.yml, Makefile, dockerfiles/Dockerfile.apisix-runtime.*
Packaging workflows and Makefile runtime targets pass NGX_HTTP_FFI_CLIENT_TOKEN through BuildKit. Debian and RPM Dockerfiles read the optional secret into the build environment.
Conditional module build and installation
build-apisix-runtime.sh
The build script pins and validates ngx_http_ffi_client, fetches it with the token or uses a local copy, configures OpenResty conditionally, and installs its Lua binding when available.
Release token validation
.github/workflows/release-apisix-runtime.yml
Debian and RPM release jobs require a non-empty token and pass it into package builds.
Build configuration documentation
README.md
The README documents the token, module, example command, fallback behavior, and pinned module commit.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant Makefile
  participant RuntimeDockerfile
  participant BuildScript
  participant OpenResty
  GitHubActions->>Makefile: Set NGX_HTTP_FFI_CLIENT_TOKEN
  Makefile->>RuntimeDockerfile: Pass BuildKit secret
  RuntimeDockerfile->>BuildScript: Export NGX_HTTP_FFI_CLIENT_TOKEN
  BuildScript->>BuildScript: Fetch or use local ngx_http_ffi_client
  BuildScript->>OpenResty: Apply conditional module configuration
  BuildScript->>BuildScript: Install Lua binding when available
Loading

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Security Check ❌ Error Category 1 (CRITICAL): deb-ubuntu:32, rpm-el:59, and rpm-ubi:30 pass the token to make in pull_request jobs that checkout PR code, so modified build files can exfiltrate it. Do not provide the secret to pull_request jobs. Run PR builds without it, and expose it only in separate trusted push, schedule, or release jobs.
E2e Test Quality Review ⚠️ Warning No E2E test is added: runtime workflows only install the package and check openresty -V/APISIX_RUNTIME_VER; they do not exercise ngx_http_ffi_client or an outbound request. Add an E2E job with a real upstream that verifies the token-enabled module request path and the no-token fallback; assert module presence and request results independently.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding ngx_http_ffi_client support to apisix-runtime.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

The script runs under set -x, so assigning NGX_HTTP_FFI_CLIENT_TOKEN and
testing it with [ -n ... ] both echoed the token into the build log. Only a
derived yes/no now reaches the trace.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@build-apisix-runtime.sh`:
- Around line 42-46: Enforce the approved commit in ngx_http_ffi_client_ver
instead of allowing arbitrary refs: validate that it equals
f13fcfa4e923ad82844bf49d9d3b3d283371ef66, then verify the commit fetched by the
ngx_http_ffi_client build flow before compiling it. Update
build-apisix-runtime.sh at lines 42-46 and 147-172, and keep README.md lines
124-125 consistent with the fixed commit.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 20ac434e-1fdd-49f9-be57-fb78049936e6

📥 Commits

Reviewing files that changed from the base of the PR and between 95406eb and bb13a2c.

📒 Files selected for processing (10)
  • .github/workflows/package-apisix-runtime-deb-openresty-1.21.yml
  • .github/workflows/package-apisix-runtime-deb-ubuntu20.04.yml
  • .github/workflows/package-apisix-runtime-rpm-el.yml
  • .github/workflows/package-apisix-runtime-rpm-ubi.yml
  • .github/workflows/release-apisix-runtime.yml
  • Makefile
  • README.md
  • build-apisix-runtime.sh
  • dockerfiles/Dockerfile.apisix-runtime.deb
  • dockerfiles/Dockerfile.apisix-runtime.rpm

Comment thread build-apisix-runtime.sh
Comment on lines +42 to +46
ngx_http_ffi_client_ver=${ngx_http_ffi_client_ver:-"f13fcfa4e923ad82844bf49d9d3b3d283371ef66"}
if [[ ! "$ngx_http_ffi_client_ver" =~ ^[A-Za-z0-9._/-]+$ ]]; then
echo "ERROR: invalid ngx_http_ffi_client_ver: $ngx_http_ffi_client_ver" >&2
exit 1
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every entry point that can supply ngx_http_ffi_client_ver.
rg -n -C 3 '\bngx_http_ffi_client_ver\b|build_apisix_runtime|build-apisix-runtime\.sh' \
  --glob '*.sh' --glob 'Dockerfile*' --glob 'Makefile' .

# Confirm whether any build path forwards this variable into the build-script environment.
rg -n -C 3 'NGX_HTTP_FFI_CLIENT_TOKEN|ngx_http_ffi_client_ver|--build-arg|--secret' \
  Makefile dockerfiles .github build-apisix-runtime.sh

Repository: api7/apisix-build-tools

Length of output: 21159


Dependency Pin Bypass (CWE-829): Inclusion of Functionality from Untrusted Control Sphere

Enforce the fixed ngx_http_ffi_client commit.

ngx_http_ffi_client_ver accepts refs such as main. When the token is set, git fetch retrieves that ref and compiles it into the runtime. Require the exact approved commit f13fcfa4e923ad82844bf49d9d3b3d283371ef66, verify the fetched commit, and keep README.md consistent.

📍 Affects 2 files
  • build-apisix-runtime.sh#L42-L46 (this comment)
  • build-apisix-runtime.sh#L147-L172
  • README.md#L124-L125
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build-apisix-runtime.sh` around lines 42 - 46, Enforce the approved commit in
ngx_http_ffi_client_ver instead of allowing arbitrary refs: validate that it
equals f13fcfa4e923ad82844bf49d9d3b3d283371ef66, then verify the commit fetched
by the ngx_http_ffi_client build flow before compiling it. Update
build-apisix-runtime.sh at lines 42-46 and 147-172, and keep README.md lines
124-125 consistent with the fixed commit.

@shreemaan-abhishek

Copy link
Copy Markdown
Contributor Author

Pushed 74ead74: the script runs under set -x, so assigning NGX_HTTP_FFI_CLIENT_TOKEN and testing it with [ -n ... ] both echoed the token into the build log. Caught it in a real make build-apisix-runtime-deb run against the first commit. Only a derived yes/no reaches the trace now, and a re-run of the same block confirms zero occurrences of either the token or its base64 form in the output, on both the token and no-token paths.

@shreemaan-abhishek

Copy link
Copy Markdown
Contributor Author

End-to-end check on the artifact this branch produces.

NGX_HTTP_FFI_CLIENT_TOKEN=<token> make build-apisix-runtime-deb \
  app=apisix-runtime runtime_version=0.0.0 \
  image_base=debian image_tag=bookworm-slim arch=linux/amd64

The resulting image carries the module three ways:

  • openresty -V lists --add-module=.../ngx_http_ffi_client-f13fcfa4
  • /usr/local/openresty/lualib/resty/ngx_http_ffi_client.lua is installed
  • the FFI entry points are present in the nginx binary

Then, inside that image, driving the same sequence ai-transport/http.lua uses (pcall(require, ...) -> new() -> connect -> request -> read_body -> set_keepalive) against a local upstream:

client: ngx_http_ffi_client
status: 200
content-type: application/json
body: {"echo":{"model":"m"},"te":"nil"}
keepalive: 1

te: nil is the client declining to put a caller-supplied Transfer-Encoding on the wire, which is what the paired gateway PRs rely on.

The CI jobs on this PR run without the secret, so they exercise the other path: the build warns and produces a runtime with no module, which is the fallback the gateway side handles.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@build-apisix-runtime.sh`:
- Around line 49-54: Unset NGX_HTTP_FFI_CLIENT_TOKEN immediately after the
authenticated fetch block completes, before subsequent commands such as patch.sh
or configure run; leave the existing token-detection logic unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fb5d4616-2daa-44e8-91ee-49b8eb7d67c2

📥 Commits

Reviewing files that changed from the base of the PR and between bb13a2c and 74ead74.

📒 Files selected for processing (1)
  • build-apisix-runtime.sh

Comment thread build-apisix-runtime.sh
Comment on lines +49 to +54
NGX_HTTP_FFI_CLIENT_TOKEN=${NGX_HTTP_FFI_CLIENT_TOKEN:-}
if [ -n "$NGX_HTTP_FFI_CLIENT_TOKEN" ]; then
ngx_http_ffi_client_have_token="yes"
else
ngx_http_ffi_client_have_token="no"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the token lifecycle without executing the build.
rg -n -C 8 \
  'NGX_HTTP_FFI_CLIENT_TOKEN|unset|env -u|git|Authorization|http\.extraHeader|https?://' \
  build-apisix-runtime.sh

Repository: api7/apisix-build-tools

Length of output: 5890


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact credentialed fetch and all subsequent child-process launches.
sed -n '1,12p;145,190p;213,245p' build-apisix-runtime.sh

# Confirm Bash keeps an imported variable exported after the assignment used by the script.
env NGX_HTTP_FFI_CLIENT_TOKEN='probe-token' bash -c '
  NGX_HTTP_FFI_CLIENT_TOKEN=${NGX_HTTP_FFI_CLIENT_TOKEN:-}
  printf "parent-exported=%s\n" "$(export -p | grep -c "NGX_HTTP_FFI_CLIENT_TOKEN")"
  printf "child-value=%s\n" "$(env | sed -n "s/^NGX_HTTP_FFI_CLIENT_TOKEN=//p")"
'

Repository: api7/apisix-build-tools

Length of output: 3714


Sensitive Data Exposure (CWE-526)

Reachability: External

Clear NGX_HTTP_FFI_CLIENT_TOKEN after the authenticated fetch. The variable remains exported and is inherited by later commands such as ./patch.sh and ./configure. Add unset NGX_HTTP_FFI_CLIENT_TOKEN immediately after the fetch block.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 50-50: A credential-bearing variable (e.g. PASSWORD, PASSWD, SECRET, TOKEN, API_KEY) is assigned a hardcoded string literal. Secrets committed to a script are exposed in source control, process listings, and shell history, and cannot be rotated without a code change. Read the value from a secrets manager or an injected environment variable at runtime instead (e.g. PASSWORD="${DB_PASSWORD:?must be set}"), and never commit the literal.
Context: ngx_http_ffi_client_have_token="yes"
Note: [CWE-798] Use of Hard-coded Credentials.

(hardcoded-password-assignment-bash)


[warning] 52-52: A credential-bearing variable (e.g. PASSWORD, PASSWD, SECRET, TOKEN, API_KEY) is assigned a hardcoded string literal. Secrets committed to a script are exposed in source control, process listings, and shell history, and cannot be rotated without a code change. Read the value from a secrets manager or an injected environment variable at runtime instead (e.g. PASSWORD="${DB_PASSWORD:?must be set}"), and never commit the literal.
Context: ngx_http_ffi_client_have_token="no"
Note: [CWE-798] Use of Hard-coded Credentials.

(hardcoded-password-assignment-bash)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build-apisix-runtime.sh` around lines 49 - 54, Unset
NGX_HTTP_FFI_CLIENT_TOKEN immediately after the authenticated fetch block
completes, before subsequent commands such as patch.sh or configure run; leave
the existing token-detection logic unchanged.

The build itself stays lenient so pull requests from forks, which get no
secrets, still pass. A release is different: without the token it would produce
an official runtime with no ngx_http_ffi_client and say so only in a warning
buried in the build log.
@shreemaan-abhishek

shreemaan-abhishek commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Worth recording how the secret behaves here.

Every green check on this PR ran the fallback path. The build log shows:

WARNING: NGX_HTTP_FFI_CLIENT_TOKEN is not set, building apisix-runtime
without ngx_http_ffi_client. ai-proxy falls back to lua-resty-http

with zero --add-module=../ngx_http_ffi_client occurrences.

The reason is simply that NGX_HTTP_FFI_CLIENT_TOKEN does not exist yet, at either repo or org level. Separately, this PR's head branch lives in a fork, and pull_request runs from a fork get no secrets at all, so even once the secret is created this PR's CI will keep taking the fallback path. The token path first runs on a push to master or on a release.

Which left a hole: if the secret is never added, this merges green and every official runtime ships without the module, announced only by a warning buried in a build log. Pushed 1f18875 to close it, so the release workflow fails fast when the secret is missing while the build stays lenient and fork PRs keep passing.

If you would rather releases degrade quietly than fail, that guard is a self-contained commit and easy to drop.

Note also that Actions log masking would not have caught the set -x leak fixed in 74ead74: the token is base64-encoded before it reaches git, and masking covers the literal secret, not transformations of it.

@shreemaan-abhishek

Copy link
Copy Markdown
Contributor Author

Superseded by #481, which carries the identical commits with the head branch on this repository rather than a fork. Closing to keep review in one place.

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