From b265ce32bd8d5eb68312eda35308aad32fe0a771 Mon Sep 17 00:00:00 2001 From: Evaline Ju <69598118+evaline-ju@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:53:13 -0700 Subject: [PATCH 1/3] :recycle::construction_worker: Use common tags for plugin inclusion/exclusion Signed-off-by: Evaline Ju <69598118+evaline-ju@users.noreply.github.com> --- .github/workflows/build.yaml | 79 +++++++++++++++--------- .github/workflows/ci.yaml | 4 +- .github/workflows/release-binaries.yaml | 9 ++- CLAUDE.md | 14 +++-- authbridge/CLAUDE.md | 8 ++- authbridge/README.md | 6 +- authbridge/cmd/README.md | 7 ++- authbridge/demos/README.md | 5 +- authbridge/go.work | 1 + authbridge/scripts/lite-tags/go.mod | 3 + authbridge/scripts/lite-tags/main.go | 82 +++++++++++++++++++++++++ local-build-and-test.sh | 11 ++-- 12 files changed, 169 insertions(+), 60 deletions(-) create mode 100644 authbridge/scripts/lite-tags/go.mod create mode 100644 authbridge/scripts/lite-tags/main.go diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7fa2965aa..cd56756c1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -47,16 +47,14 @@ jobs: dockerfile: cmd/authbridge-proxy/Dockerfile # AuthBridge proxy-sidecar LITE image — the SAME authbridge-proxy - # binary + Dockerfile, built with exclude_plugin_* tags so only - # jwt-validation + token-exchange compile in (drops the OPA SDK - # and the parsers, roughly halving the binary). A build variant, - # not a separate binary. Same listener layout as the full proxy - # image; not yet referenced by the operator's default config. + # binary + Dockerfile, built with a trimmed plugin set (roughly + # halving the binary). A build variant, not a separate binary. + # GO_BUILD_TAGS is derived at step time from + # authbridge/scripts/lite-tags rather than declared here, so + # adding a plugin doesn't require editing this file. - name: authbridge-lite context: ./authbridge dockerfile: cmd/authbridge-proxy/Dockerfile - build_args: | - GO_BUILD_TAGS=exclude_plugin_a2aparser,exclude_plugin_ibac,exclude_plugin_inferenceparser,exclude_plugin_mcpparser,exclude_plugin_opa,exclude_plugin_sparc,exclude_plugin_tokenbroker,exclude_plugin_toolprune # AuthBridge proxy-sidecar CPEX image — authbridge-proxy built # with -tags cpex (links libcpex_ffi.a from a pinned CPEX @@ -79,6 +77,15 @@ jobs: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # 1b. Set up Go — needed by the buildargs step for authbridge-lite, + # which derives GO_BUILD_TAGS from plugin source at build time. + # Cheap to install for every matrix row (~1s of cache pull); no-op + # for rows that don't need it. + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: authbridge/scripts/lite-tags/go.mod + # 2. Set up QEMU for multi-arch builds - name: Set up QEMU uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4 @@ -121,26 +128,41 @@ jobs: # Add 'latest' tag for version tags, workflow_dispatch, and pushes to main type=raw,value=latest,enable=${{ (github.ref_type == 'tag' && startsWith(github.ref_name, 'v')) || github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }} - # 6b. Resolve build-args. authbridge-cpex needs CPEX_FFI_VERSION - # (the release tag) and CPEX_FFI_ABI (the FFI ABI integer the - # linked lib must report) — both read from the files next to its - # Dockerfile and asserted against the tarball at build time. Other - # images leave this empty (an undeclared build-arg is ignored). + # 6b. Resolve per-image build-args. + # authbridge-cpex: CPEX_FFI_VERSION (release tag) and CPEX_FFI_ABI + # (integer the linked lib must report), read from the files + # next to its Dockerfile and asserted at build time. + # authbridge-lite: GO_BUILD_TAGS, derived from plugin source + # (authbridge/scripts/lite-tags) so the list stays in sync + # without hand-maintenance. + # Other images leave args empty (an undeclared build-arg is + # ignored). - name: Resolve build args id: buildargs run: | - if [[ "${{ matrix.image_config.name }}" == "authbridge-cpex" ]]; then - VERSION="$(tr -d '[:space:]' < authbridge/cmd/authbridge-cpex/CPEX_FFI_VERSION)" - ABI="$(tr -d '[:space:]' < authbridge/cmd/authbridge-cpex/CPEX_FFI_ABI)" - { - echo "args<> "$GITHUB_OUTPUT" - else - echo "args=" >> "$GITHUB_OUTPUT" - fi + case "${{ matrix.image_config.name }}" in + authbridge-cpex) + VERSION="$(tr -d '[:space:]' < authbridge/cmd/authbridge-cpex/CPEX_FFI_VERSION)" + ABI="$(tr -d '[:space:]' < authbridge/cmd/authbridge-cpex/CPEX_FFI_ABI)" + { + echo "args<> "$GITHUB_OUTPUT" + ;; + authbridge-lite) + TAGS=$(go -C authbridge/scripts/lite-tags run .) + { + echo "args<> "$GITHUB_OUTPUT" + ;; + *) + echo "args=" >> "$GITHUB_OUTPUT" + ;; + esac # 7. Build and push image - name: Build and push ${{ matrix.image_config.name }} @@ -148,13 +170,10 @@ jobs: with: context: ${{ matrix.image_config.context }} file: ${{ matrix.image_config.context }}/${{ matrix.image_config.dockerfile }} - # Merge both build-arg sources into a single key: static per-image - # args from the matrix (e.g. authbridge-lite GO_BUILD_TAGS) plus the - # dynamically resolved args (authbridge-cpex CPEX_FFI_*). A duplicate - # `build-args:` key is invalid YAML and fails the whole workflow; only - # one image sets each source, so concatenating them is safe. + # Dynamically-resolved args from step 6b: authbridge-cpex + # (CPEX_FFI_*) and authbridge-lite (GO_BUILD_TAGS from + # scripts/lite-tags). Other images resolve to empty. build-args: | - ${{ matrix.image_config.build_args }} ${{ steps.buildargs.outputs.args }} push: true platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f9d55baed..cee834b79 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -107,9 +107,7 @@ jobs: - name: Build + test lite variant (exclude_plugin_* tags) if: matrix.binary == 'authbridge-proxy' run: | - TAGS="exclude_plugin_a2aparser,exclude_plugin_ibac,exclude_plugin_inferenceparser" - TAGS="$TAGS,exclude_plugin_mcpparser,exclude_plugin_opa,exclude_plugin_sparc,exclude_plugin_tokenbroker" - TAGS="$TAGS,exclude_plugin_toolprune" + TAGS=$(go -C ../../scripts/lite-tags run .) go build -v -tags "$TAGS" ./... go test -v -race -cover -tags "$TAGS" ./... diff --git a/.github/workflows/release-binaries.yaml b/.github/workflows/release-binaries.yaml index 6b3426ab6..d13c67cdc 100644 --- a/.github/workflows/release-binaries.yaml +++ b/.github/workflows/release-binaries.yaml @@ -52,10 +52,9 @@ jobs: # authbridge-proxy variants: ":". Empty # suffix is the default plugin set. One variant per opt-in # plugin (or one combined "full") — never enumerate combos. - lite_tags="exclude_plugin_a2aparser,exclude_plugin_ibac" - lite_tags="${lite_tags},exclude_plugin_inferenceparser" - lite_tags="${lite_tags},exclude_plugin_mcpparser,exclude_plugin_opa" - lite_tags="${lite_tags},exclude_plugin_sparc,exclude_plugin_tokenbroker" + # Lite tags are derived from plugin source; see + # authbridge/scripts/lite-tags. + lite_tags=$(go -C authbridge/scripts/lite-tags run .) declare -a proxy_variants=( ":" "lite:${lite_tags}" @@ -112,7 +111,7 @@ jobs: add '' add '`authbridge-proxy` ships in variants matching the container images:' add 'unqualified (default plugin set, matches the `authbridge` image),' - add '`-lite` (auth-only, matches `authbridge-lite`), plus one variant per' + add '`-lite` (trimmed plugin set, matches `authbridge-lite`), plus one variant per' add 'opt-in plugin currently offered for try-out (today: `-sessionbudget`).' add 'Variants track opt-in plugins one-for-one; arbitrary combinations are not published.' add '' diff --git a/CLAUDE.md b/CLAUDE.md index ff4dec73e..a0c24d6ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -125,7 +125,7 @@ Two mode-specific binaries (proxy, envoy), one Dockerfile each; the `authbridge- |--------|------|-----------|---------| | `cmd/authbridge-proxy/` | proxy-sidecar (default) | HTTP forward + reverse proxies | full (incl. parsers) | | `cmd/authbridge-envoy/` | envoy-sidecar | gRPC ext_proc on :9090 | full (incl. parsers) | -| `authbridge-lite` _(image: proxy + `exclude_plugin_*`)_ | proxy-sidecar | HTTP forward + reverse proxies | auth-only (jwt-validation + token-exchange; OPA + parsers dropped) | +| `authbridge-lite` _(image: proxy + `exclude_plugin_*`)_ | proxy-sidecar | HTTP forward + reverse proxies | trimmed plugin set (see `authbridge/scripts/lite-tags`) | **Go modules:** - `authbridge/authlib/` — pure library: validation, exchange, cache, bypass, spiffe, routing, auth, config, all listener implementations, all plugins. @@ -139,7 +139,7 @@ Two mode-specific binaries (proxy, envoy), one Dockerfile each; the `authbridge- | Workflow | Trigger | Purpose | |----------|---------|---------| | `ci.yaml` | PR to main/release-* | Pre-commit, Go fmt/vet/build/test for authlib and the cmd/authbridge-* binaries; Python tests | -| `build.yaml` | Tag push (`v*`) or manual | Multi-arch Docker builds for: proxy-init, authbridge (proxy-sidecar combined), authbridge-envoy (envoy-sidecar combined), authbridge-lite (proxy Dockerfile built with `exclude_plugin_*` tags — auth-only) | +| `build.yaml` | Tag push (`v*`) or manual | Multi-arch Docker builds for: proxy-init, authbridge (proxy-sidecar combined), authbridge-envoy (envoy-sidecar combined), authbridge-lite (proxy Dockerfile built with `exclude_plugin_*` tags from `authbridge/scripts/lite-tags`) | | `security-scans.yaml` | PR to main | Dependency review, shellcheck, YAML lint, Hadolint, Bandit, Trivy, CodeQL | | `scorecard.yaml` | Weekly / push to main | OpenSSF Scorecard security health metrics | | `spellcheck_action.yml` | PR | Spellcheck on markdown files | @@ -170,7 +170,7 @@ All images are pushed to `ghcr.io/rossoctl/cortex/` from |-------|--------|-------------| | **`authbridge`** | **`authbridge/cmd/authbridge-proxy/Dockerfile`** | **proxy-sidecar combined image (default mode): authbridge-proxy (full plugin set incl. parsers) + spiffe-helper. No Envoy.** | | `authbridge-envoy` | `authbridge/cmd/authbridge-envoy/Dockerfile` | envoy-sidecar combined image: Envoy + authbridge-envoy (ext_proc, full plugin set) + spiffe-helper | -| `authbridge-lite` | `authbridge/cmd/authbridge-proxy/Dockerfile` (+ `GO_BUILD_TAGS=exclude_plugin_*`) | proxy-sidecar combined image built auth-only (jwt-validation + token-exchange; OPA + parsers dropped) + spiffe-helper. A build variant of `authbridge`, not a separate binary; not yet referenced by the operator's default config | +| `authbridge-lite` | `authbridge/cmd/authbridge-proxy/Dockerfile` (+ `GO_BUILD_TAGS=exclude_plugin_*`) | proxy-sidecar combined image with a trimmed plugin set (see `authbridge/scripts/lite-tags`), plus spiffe-helper. A build variant of `authbridge`, not a separate binary; not yet referenced by the operator's default config | | `authbridge-cpex` | `authbridge/cmd/authbridge-cpex/Dockerfile` | proxy-sidecar build with the CPEX plugin: authbridge-proxy built with `-tags cpex`, links `libcpex_ffi.a` from a pinned CPEX release (CGO_ENABLED=1). Routes hooks through the CPEX framework (APL DSL + named CPEX policy plugins). FFI ABI version is read from `authbridge/cmd/authbridge-cpex/CPEX_FFI_VERSION` | | `proxy-init` | `authbridge/proxy-init/Dockerfile.init` | Alpine + iptables init container (envoy-sidecar + proxy-sidecar enforce-redirect modes) | @@ -252,9 +252,11 @@ cd authbridge/proxy-init && make docker-build-init # Combined sidecars (proxy-sidecar default / envoy-sidecar) cd authbridge && podman build -f cmd/authbridge-proxy/Dockerfile -t authbridge:latest . cd authbridge && podman build -f cmd/authbridge-envoy/Dockerfile -t authbridge-envoy:latest . -# authbridge-lite: same proxy Dockerfile, built with exclude_plugin_* tags (auth-only) -cd authbridge && podman build -f cmd/authbridge-proxy/Dockerfile \ - --build-arg GO_BUILD_TAGS="exclude_plugin_a2aparser,exclude_plugin_ibac,exclude_plugin_inferenceparser,exclude_plugin_mcpparser,exclude_plugin_opa,exclude_plugin_sparc,exclude_plugin_tokenbroker,exclude_plugin_toolprune" \ +# authbridge-lite: same proxy Dockerfile, built with the trimmed +# plugin set derived from plugin source by +# authbridge/scripts/lite-tags. +cd authbridge && LITE_TAGS=$(go -C scripts/lite-tags run .) && podman build -f cmd/authbridge-proxy/Dockerfile \ + --build-arg GO_BUILD_TAGS="${LITE_TAGS}" \ -t authbridge-lite:latest . ``` diff --git a/authbridge/CLAUDE.md b/authbridge/CLAUDE.md index 275101981..2432f6ec1 100644 --- a/authbridge/CLAUDE.md +++ b/authbridge/CLAUDE.md @@ -45,7 +45,7 @@ ships in variants that mirror the container images: | Variant | Tarball name shape | Matches | |---|---|---| | unqualified (default plugins) | `authbridge-proxy___.tar.gz` | `authbridge` image | -| `-lite` (drops the OPA SDK and the protocol parsers) | `authbridge-proxy-lite___.tar.gz` | `authbridge-lite` image | +| `-lite` (trimmed plugin set — see `authbridge/scripts/lite-tags`) | `authbridge-proxy-lite___.tar.gz` | `authbridge-lite` image | | `-sessionbudget` (default + opt-in session-budget) | `authbridge-proxy-sessionbudget___.tar.gz` | no image today | One variant per opt-in plugin currently offered for try-out (today: @@ -411,9 +411,11 @@ make load-image # Uses KIND_CLUSTER_NAME env var (default: r cd .. podman build -f cmd/authbridge-proxy/Dockerfile -t authbridge:latest . # proxy-sidecar (default) podman build -f cmd/authbridge-envoy/Dockerfile -t authbridge-envoy:latest . # envoy-sidecar -# authbridge-lite: the proxy Dockerfile built with exclude_plugin_* tags (auth-only) +# authbridge-lite: the proxy Dockerfile built with a trimmed plugin +# set derived from plugin source by scripts/lite-tags. +LITE_TAGS=$(go -C scripts/lite-tags run .) podman build -f cmd/authbridge-proxy/Dockerfile \ - --build-arg GO_BUILD_TAGS="exclude_plugin_a2aparser,exclude_plugin_ibac,exclude_plugin_inferenceparser,exclude_plugin_mcpparser,exclude_plugin_opa,exclude_plugin_sparc,exclude_plugin_tokenbroker,exclude_plugin_toolprune" \ + --build-arg GO_BUILD_TAGS="${LITE_TAGS}" \ -t authbridge-lite:latest . kind load docker-image authbridge:latest --name rossoctl kind load docker-image authbridge-envoy:latest --name rossoctl diff --git a/authbridge/README.md b/authbridge/README.md index feee2482d..7d8fa1424 100644 --- a/authbridge/README.md +++ b/authbridge/README.md @@ -40,13 +40,13 @@ Two container images are published: |-------|----------| | `authbridge` | proxy-sidecar combined: authbridge-proxy binary + bundled spiffe-helper | | `authbridge-envoy` | envoy-sidecar combined: Envoy + ext_proc + bundled spiffe-helper | -| `authbridge-lite` | `authbridge-proxy` built with `exclude_plugin_*` tags — auth-only (jwt-validation + token-exchange; OPA + parsers dropped). A build variant, not a separate binary | +| `authbridge-lite` | `authbridge-proxy` built with `exclude_plugin_*` tags from `authbridge/scripts/lite-tags` (trimmed plugin set). A build variant, not a separate binary | | Mode | Image | Use Case | How It Works | |------|-------|----------|-------------| | `proxy-sidecar` (default) | `authbridge` | HTTP_PROXY-based forward + reverse proxies | Agent routes outbound traffic through forward proxy; reverse proxy validates inbound JWTs | | `envoy-sidecar` | `authbridge-envoy` | Transparent interception via iptables | Envoy intercepts all traffic, delegates auth to authbridge via ext_proc gRPC | -| `lite` | `authbridge-lite` | The `authbridge-proxy` binary built with `exclude_plugin_*` tags (auth-only: jwt-validation + token-exchange) | For size-constrained deployments that don't need protocol-aware session events | +| `lite` | `authbridge-lite` | The `authbridge-proxy` binary built with `exclude_plugin_*` tags from `authbridge/scripts/lite-tags` (trimmed plugin set) | For size-constrained deployments that don't need protocol-aware session events | The operator resolves the mode per workload from `AgentRuntime.Spec.AuthBridgeMode` → namespace ConfigMap → deprecated `rossoctl.io/authbridge-mode` annotation → cluster default (`proxy-sidecar`). See operator#361. @@ -480,7 +480,7 @@ plugin package from being imported and compiled into the binary. - [authlib](authlib/README.md) — Shared auth building blocks (Go library) - [cmd/authbridge-proxy](cmd/authbridge-proxy/) — proxy-sidecar binary (default mode, full plugin set) - [cmd/authbridge-envoy](cmd/authbridge-envoy/) — envoy-sidecar binary (Envoy + ext_proc, full plugin set) -- `authbridge-lite` image — `cmd/authbridge-proxy` built with `exclude_plugin_*` tags (auth-only); a build variant, not a separate binary +- `authbridge-lite` image — `cmd/authbridge-proxy` built with `exclude_plugin_*` tags from `authbridge/scripts/lite-tags` (trimmed plugin set); a build variant, not a separate binary - [proxy-init](proxy-init/README.md) — iptables init container (envoy-sidecar mode only) - [docs/](docs/) — framework architecture and plugin author references diff --git a/authbridge/cmd/README.md b/authbridge/cmd/README.md index 7e366c81f..3e8307e88 100644 --- a/authbridge/cmd/README.md +++ b/authbridge/cmd/README.md @@ -75,6 +75,7 @@ inbound mechanism, and the preset fills only that one's address. iptables init container. - **Size-constrained, no protocol-aware events needed**: use the `authbridge-lite` image — the `authbridge-proxy` binary built with - `exclude_plugin_*` tags (auth-only). Same listener layout, but without - parsers/OPA — abctl will only see denial events and basic auth-level - invocations, not full A2A/MCP/Inference protocol context. + `exclude_plugin_*` tags from `authbridge/scripts/lite-tags` (trimmed + plugin set). Same listener layout, but abctl will only see denial + events and basic auth-level invocations for the plugins the trimmed + set drops. diff --git a/authbridge/demos/README.md b/authbridge/demos/README.md index 3646a1860..a31ce0403 100644 --- a/authbridge/demos/README.md +++ b/authbridge/demos/README.md @@ -6,8 +6,9 @@ more AuthBridge capabilities. > **Note:** These demos use the operator-injected combined sidecar (after > cortex#411 — `authbridge` for proxy-sidecar, `authbridge-envoy` -> for envoy-sidecar, and `authbridge-lite`, the proxy image built auth-only -> via `exclude_plugin_*` tags). The previous `authbridge-unified` image and the per-component +> for envoy-sidecar, and `authbridge-lite`, the proxy image built with a +> trimmed plugin set via `exclude_plugin_*` tags from +> `authbridge/scripts/lite-tags`). The previous `authbridge-unified` image and the per-component > sidecars (`client-registration`, standalone `spiffe-helper`) have been > removed. diff --git a/authbridge/go.work b/authbridge/go.work index 5b18e4912..17354fb31 100644 --- a/authbridge/go.work +++ b/authbridge/go.work @@ -7,5 +7,6 @@ use ( ./cmd/authbridge-envoy ./cmd/authbridge-praxis ./cmd/authbridge-proxy + ./scripts/lite-tags ./storage/redis ) diff --git a/authbridge/scripts/lite-tags/go.mod b/authbridge/scripts/lite-tags/go.mod new file mode 100644 index 000000000..cc15b4916 --- /dev/null +++ b/authbridge/scripts/lite-tags/go.mod @@ -0,0 +1,3 @@ +module github.com/rossoctl/cortex/authbridge/scripts/lite-tags + +go 1.26.5 diff --git a/authbridge/scripts/lite-tags/main.go b/authbridge/scripts/lite-tags/main.go new file mode 100644 index 000000000..3a78c9e67 --- /dev/null +++ b/authbridge/scripts/lite-tags/main.go @@ -0,0 +1,82 @@ +// lite-tags emits the CSV of `exclude_plugin_*` build tags that define the +// authbridge-lite variant. Walks plugins_*.go under cmd/authbridge-proxy, +// finds every default-on plugin (`//go:build !exclude_plugin_`), and +// prints an exclude tag for each one not in liteKeep. +// +// New default-on plugins are excluded from lite automatically. To keep one +// in lite, add its build-tag suffix to liteKeep. +package main + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strings" +) + +// liteKeep names plugins that stay in the lite build. Keys are the exact +// build-tag suffix, which may differ from the plugin's registered name +// (e.g. `litellm_budgettrack` vs `litellm-budget-track`). +var liteKeep = map[string]bool{ + "jwtvalidation": true, + "tokenexchange": true, + "litellm_budgettrack": true, + "staticinject": true, +} + +// Path is relative to this script's module dir. Run via +// `go run ./authbridge/scripts/lite-tags` (authbridge/ with go.work) or +// `cd authbridge/scripts/lite-tags && go run .`. +const pluginsDir = "../../cmd/authbridge-proxy" + +var buildTagPattern = regexp.MustCompile(`^//go:build !exclude_plugin_(\S+)$`) + +func main() { + tags, err := discover(pluginsDir) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Println(strings.Join(tags, ",")) +} + +func discover(dir string) ([]string, error) { + matches, err := filepath.Glob(filepath.Join(dir, "plugins_*.go")) + if err != nil { + return nil, fmt.Errorf("glob %s: %w", dir, err) + } + if len(matches) == 0 { + return nil, fmt.Errorf("no plugins_*.go files under %s", dir) + } + + var tags []string + for _, path := range matches { + name, err := extractExcludeSuffix(path) + if err != nil { + return nil, err + } + if name == "" || liteKeep[name] { + continue + } + tags = append(tags, "exclude_plugin_"+name) + } + sort.Strings(tags) + return tags, nil +} + +// extractExcludeSuffix returns the suffix of a `!exclude_plugin_*` directive, +// or "" if the file has none (e.g. opt-in `include_plugin_*` plugins). +func extractExcludeSuffix(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("read %s: %w", path, err) + } + for _, line := range strings.Split(string(data), "\n") { + if m := buildTagPattern.FindStringSubmatch(strings.TrimSpace(line)); m != nil { + return m[1], nil + } + } + return "", nil +} diff --git a/local-build-and-test.sh b/local-build-and-test.sh index 35a671bd2..e74913f2a 100755 --- a/local-build-and-test.sh +++ b/local-build-and-test.sh @@ -89,15 +89,16 @@ load_image_to_kind ghcr.io/rossoctl/cortex/authbridge-envoy:local echo "✅ Built and loaded: authbridge-envoy:local" echo "" -# Build authbridge-lite: the same authbridge-proxy binary/Dockerfile built -# with exclude_plugin_* tags so only jwt-validation + token-exchange compile -# in (drops the OPA SDK + parsers). A build variant, not a separate binary. +# Build authbridge-lite: the same authbridge-proxy binary/Dockerfile +# built with the trimmed plugin set (see authbridge/scripts/lite-tags). +# A build variant, not a separate binary. echo "==========================================" -echo "Building authbridge-lite (proxy build variant: auth-only plugins)" +echo "Building authbridge-lite (proxy build variant: trimmed plugin set, see authbridge/scripts/lite-tags)" echo "==========================================" cd "${SCRIPT_DIR}/authbridge" +LITE_TAGS=$(go -C scripts/lite-tags run .) ${CONTAINER_RUNTIME} build -f cmd/authbridge-proxy/Dockerfile \ - --build-arg GO_BUILD_TAGS="exclude_plugin_a2aparser,exclude_plugin_ibac,exclude_plugin_inferenceparser,exclude_plugin_mcpparser,exclude_plugin_opa,exclude_plugin_sparc,exclude_plugin_tokenbroker" \ + --build-arg GO_BUILD_TAGS="${LITE_TAGS}" \ -t ghcr.io/rossoctl/cortex/authbridge-lite:local . load_image_to_kind ghcr.io/rossoctl/cortex/authbridge-lite:local echo "✅ Built and loaded: authbridge-lite:local" From 8c7e773dd9c5404830c5163ea9c8d4a8bdb5dd9d Mon Sep 17 00:00:00 2001 From: Evaline Ju <69598118+evaline-ju@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:55:30 -0700 Subject: [PATCH 2/3] :art: Address review comments Signed-off-by: Evaline Ju <69598118+evaline-ju@users.noreply.github.com> --- CLAUDE.md | 2 +- authbridge/CLAUDE.md | 6 +++--- authbridge/cmd/README.md | 2 +- authbridge/scripts/lite-tags/main.go | 20 +++++++++++++++----- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a0c24d6ae..72c1fd336 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -307,7 +307,7 @@ cd authbridge && LITE_TAGS=$(go -C scripts/lite-tags run .) && podman build -f c ## Gotchas and Known Issues -1. **One Go module:** The repo has a single Go module at `authbridge/proxy-init/go.mod` (Go 1.25). +1. **Multiple Go modules:** The repo has several Go modules under `authbridge/` — `authlib/`, each `cmd/*/`, `storage/redis/`, `scripts/lite-tags/`, and the `demos/*/` self-contained ones — linked by `authbridge/go.work`. Local commands from a specific module directory should typically set `GOWORK=off` (as CI does) so the module resolves its own `replace` directives instead of pulling in workspace siblings. 2. **Avoid committing venvs:** Virtual environment directories (e.g. `authbridge/proxy-init/quickstart/venv/`) should be gitignored (the repo's `.gitignore` has a `venv` pattern). Do not create and commit new virtual environments under version control. diff --git a/authbridge/CLAUDE.md b/authbridge/CLAUDE.md index 2432f6ec1..1f5c93d22 100644 --- a/authbridge/CLAUDE.md +++ b/authbridge/CLAUDE.md @@ -28,9 +28,9 @@ binaries with shared auth logic in `authlib/`: version lives in `cmd/authbridge-cpex/CPEX_FFI_VERSION`. The other binaries are pure-Go (CGO_ENABLED=0) and do not import the cpex package. - `authbridge-lite` (**image, not a separate binary**) — `cmd/authbridge-proxy` - built with `exclude_plugin_*` tags so only jwt-validation + token-exchange - compile in (OPA + parsers dropped). For size-optimized deployments that - don't need protocol-aware session events. + built with `exclude_plugin_*` tags for a trimmed plugin set (see + `authbridge/scripts/lite-tags` for the definition). For size-optimized + deployments that don't need protocol-aware session events. Each binary is hardcoded to its deployment shape; mode is no longer selected at runtime. The YAML `mode:` field must match the binary or boot fails. diff --git a/authbridge/cmd/README.md b/authbridge/cmd/README.md index 3e8307e88..8a01e719c 100644 --- a/authbridge/cmd/README.md +++ b/authbridge/cmd/README.md @@ -13,7 +13,7 @@ image is a build variant of the proxy binary (proxy Dockerfile + |---|---|---|---|---| | [`authbridge-proxy/`](authbridge-proxy/) | `proxy-sidecar` (default) | HTTP forward + reverse proxies | full (jwt-validation, token-exchange, a2a-parser, mcp-parser, inference-parser) | `ghcr.io/rossoctl/cortex/authbridge` | | [`authbridge-envoy/`](authbridge-envoy/) | `envoy-sidecar` | gRPC ext_proc on `:9090` (hooked into Envoy) | full | `ghcr.io/rossoctl/cortex/authbridge-envoy` | -| `authbridge-lite` _(build variant of `authbridge-proxy`)_ | `proxy-sidecar` | HTTP forward + reverse proxies | lite — `authbridge-proxy` built with `exclude_plugin_*` tags (jwt-validation + token-exchange only; OPA + parsers dropped) | `ghcr.io/rossoctl/cortex/authbridge-lite` | +| `authbridge-lite` _(build variant of `authbridge-proxy`)_ | `proxy-sidecar` | HTTP forward + reverse proxies | lite — `authbridge-proxy` built with `exclude_plugin_*` tags for a trimmed plugin set (see [`../scripts/lite-tags`](../scripts/lite-tags)) | `ghcr.io/rossoctl/cortex/authbridge-lite` | | [`abctl/`](abctl/) | n/a | n/a | n/a | not published — local TUI for the Session Events API | Each binary directory contains `main.go`, `go.mod`/`go.sum`, diff --git a/authbridge/scripts/lite-tags/main.go b/authbridge/scripts/lite-tags/main.go index 3a78c9e67..9b16ab253 100644 --- a/authbridge/scripts/lite-tags/main.go +++ b/authbridge/scripts/lite-tags/main.go @@ -5,6 +5,11 @@ // // New default-on plugins are excluded from lite automatically. To keep one // in lite, add its build-tag suffix to liteKeep. +// +// Usage: +// +// go -C authbridge/scripts/lite-tags run . # default path, CWD-independent +// go -C authbridge/scripts/lite-tags run . # override the plugins dir package main import ( @@ -26,15 +31,20 @@ var liteKeep = map[string]bool{ "staticinject": true, } -// Path is relative to this script's module dir. Run via -// `go run ./authbridge/scripts/lite-tags` (authbridge/ with go.work) or -// `cd authbridge/scripts/lite-tags && go run .`. -const pluginsDir = "../../cmd/authbridge-proxy" +// defaultPluginsDir is relative to this script's module directory, so +// `go -C authbridge/scripts/lite-tags run .` finds it regardless of the +// caller's CWD. Callers with an unusual layout can pass a plugins path +// as the first argument. +const defaultPluginsDir = "../../cmd/authbridge-proxy" var buildTagPattern = regexp.MustCompile(`^//go:build !exclude_plugin_(\S+)$`) func main() { - tags, err := discover(pluginsDir) + dir := defaultPluginsDir + if len(os.Args) > 1 { + dir = os.Args[1] + } + tags, err := discover(dir) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) From 5873147a6956516f149e941b4affdca95d43fc40 Mon Sep 17 00:00:00 2001 From: Evaline Ju <69598118+evaline-ju@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:49:13 -0700 Subject: [PATCH 3/3] :art::white_check_mark: Address review comments Signed-off-by: Evaline Ju <69598118+evaline-ju@users.noreply.github.com> --- .github/workflows/ci.yaml | 4 +- authbridge/cmd/README.md | 6 +- authbridge/scripts/lite-tags/main.go | 32 +++++++--- authbridge/scripts/lite-tags/main_test.go | 75 +++++++++++++++++++++++ 4 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 authbridge/scripts/lite-tags/main_test.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cee834b79..13cc86c87 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -101,10 +101,10 @@ jobs: run: go build -v ./... # The authbridge-lite image is this same authbridge-proxy binary built - # with exclude_plugin_* tags (only jwt-validation + token-exchange). + # with the trimmed plugin set derived by authbridge/scripts/lite-tags. # Build AND test that tag set on every PR — build.yaml only exercises # it on tag/main pushes, and this guards against lite-only regressions. - - name: Build + test lite variant (exclude_plugin_* tags) + - name: Build + test lite variant if: matrix.binary == 'authbridge-proxy' run: | TAGS=$(go -C ../../scripts/lite-tags run .) diff --git a/authbridge/cmd/README.md b/authbridge/cmd/README.md index 8a01e719c..e81de6c74 100644 --- a/authbridge/cmd/README.md +++ b/authbridge/cmd/README.md @@ -76,6 +76,6 @@ inbound mechanism, and the preset fills only that one's address. - **Size-constrained, no protocol-aware events needed**: use the `authbridge-lite` image — the `authbridge-proxy` binary built with `exclude_plugin_*` tags from `authbridge/scripts/lite-tags` (trimmed - plugin set). Same listener layout, but abctl will only see denial - events and basic auth-level invocations for the plugins the trimmed - set drops. + plugin set). Same listener layout, but without parsers/OPA — abctl + will only see denial events and basic auth-level invocations, not + full A2A/MCP/Inference protocol context. diff --git a/authbridge/scripts/lite-tags/main.go b/authbridge/scripts/lite-tags/main.go index 9b16ab253..3dd2bd777 100644 --- a/authbridge/scripts/lite-tags/main.go +++ b/authbridge/scripts/lite-tags/main.go @@ -6,10 +6,9 @@ // New default-on plugins are excluded from lite automatically. To keep one // in lite, add its build-tag suffix to liteKeep. // -// Usage: -// -// go -C authbridge/scripts/lite-tags run . # default path, CWD-independent -// go -C authbridge/scripts/lite-tags run . # override the plugins dir +// Usage: every call site uses `go -C /scripts/lite-tags run .`, which +// chdirs to this module before exec so the default plugins path resolves +// correctly. Callers can pass an override path as the first argument. package main import ( @@ -31,13 +30,18 @@ var liteKeep = map[string]bool{ "staticinject": true, } -// defaultPluginsDir is relative to this script's module directory, so -// `go -C authbridge/scripts/lite-tags run .` finds it regardless of the -// caller's CWD. Callers with an unusual layout can pass a plugins path -// as the first argument. +// defaultPluginsDir is resolved relative to the process working directory +// (filepath.Glob and os.ReadFile know nothing about module layout). It +// works because every call site uses `go -C run .`, which +// chdirs before exec. const defaultPluginsDir = "../../cmd/authbridge-proxy" -var buildTagPattern = regexp.MustCompile(`^//go:build !exclude_plugin_(\S+)$`) +// buildTagPattern matches `!exclude_plugin_` inside a //go:build +// directive. The pattern does NOT anchor to end-of-line so compound +// directives like `//go:build !exclude_plugin_opa && !nocgo` still yield +// the exclude tag — otherwise the plugin would silently stay in the lite +// build. +var buildTagPattern = regexp.MustCompile(`^//go:build\s+!exclude_plugin_(\w+)`) func main() { dir := defaultPluginsDir @@ -63,6 +67,11 @@ func discover(dir string) ([]string, error) { var tags []string for _, path := range matches { + // Skip test files: `plugins_*_test.go` matches the same glob but + // isn't a plugin build directive. + if strings.HasSuffix(path, "_test.go") { + continue + } name, err := extractExcludeSuffix(path) if err != nil { return nil, err @@ -72,6 +81,11 @@ func discover(dir string) ([]string, error) { } tags = append(tags, "exclude_plugin_"+name) } + // Fail closed if no tags were derived: an empty CSV would produce + // `go build -tags ""` and silently ship a full binary as "lite". + if len(tags) == 0 { + return nil, fmt.Errorf("no exclude tags derived from %s: every default-on plugin is in liteKeep, or the build-tag convention changed", dir) + } sort.Strings(tags) return tags, nil } diff --git a/authbridge/scripts/lite-tags/main_test.go b/authbridge/scripts/lite-tags/main_test.go new file mode 100644 index 000000000..b68c258cb --- /dev/null +++ b/authbridge/scripts/lite-tags/main_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestDiscover_ExpectedTags fails when the shipped lite tag set changes — +// a plugin added/removed or liteKeep edited. Deliberate changes update +// the want string; unintended changes are caught here. +func TestDiscover_ExpectedTags(t *testing.T) { + tags, err := discover(defaultPluginsDir) + if err != nil { + t.Fatalf("discover: %v", err) + } + got := strings.Join(tags, ",") + want := "exclude_plugin_a2aparser,exclude_plugin_ibac,exclude_plugin_inferenceparser,exclude_plugin_mcpparser,exclude_plugin_opa,exclude_plugin_sparc,exclude_plugin_tokenbroker,exclude_plugin_toolprune" + if got != want { + t.Errorf("output changed\n got: %s\nwant: %s", got, want) + } +} + +// TestDiscover_FailsClosedOnEmptyTags — an empty result would flow to +// `go build -tags ""` and ship a full binary as "lite". +func TestDiscover_FailsClosedOnEmptyTags(t *testing.T) { + dir := t.TempDir() + writePlugin(t, dir, "plugins_jwtvalidation.go", "!exclude_plugin_jwtvalidation") + + if _, err := discover(dir); err == nil { + t.Fatal("want error when every plugin is in liteKeep, got nil") + } +} + +// TestDiscover_CompoundBuildDirective — before dropping the $ anchor, +// `!exclude_plugin_X && !Y` silently failed to match and the plugin +// stayed in the lite build. +func TestDiscover_CompoundBuildDirective(t *testing.T) { + dir := t.TempDir() + writePlugin(t, dir, "plugins_examplecompound.go", "!exclude_plugin_examplecompound && !nocgo") + writePlugin(t, dir, "plugins_jwtvalidation.go", "!exclude_plugin_jwtvalidation") // keep-listed, prevents fail-closed + + tags, err := discover(dir) + if err != nil { + t.Fatalf("discover: %v", err) + } + if got := strings.Join(tags, ","); got != "exclude_plugin_examplecompound" { + t.Errorf("got %q, want exclude_plugin_examplecompound", got) + } +} + +// TestDiscover_SkipsTestFiles — the plugins_*.go glob would otherwise +// scan a future plugins_foo_test.go as a plugin definition. +func TestDiscover_SkipsTestFiles(t *testing.T) { + dir := t.TempDir() + writePlugin(t, dir, "plugins_ghost_test.go", "!exclude_plugin_ghost") + writePlugin(t, dir, "plugins_a2aparser.go", "!exclude_plugin_a2aparser") + + tags, err := discover(dir) + if err != nil { + t.Fatalf("discover: %v", err) + } + if got := strings.Join(tags, ","); got != "exclude_plugin_a2aparser" { + t.Errorf("got %q, want exclude_plugin_a2aparser (test file must be skipped)", got) + } +} + +func writePlugin(t *testing.T, dir, name, buildConstraint string) { + t.Helper() + content := "//go:build " + buildConstraint + "\n\npackage main\n" + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { + t.Fatalf("write %s: %v", name, err) + } +}