Skip to content

Feat(opa): implement singleton OPA SDK to eliminate duplicate bundle …#491

Merged
davidhadas merged 1 commit into
rossoctl:mainfrom
davidhadas:remove_opa_plugin_dependency
Jun 11, 2026
Merged

Feat(opa): implement singleton OPA SDK to eliminate duplicate bundle …#491
davidhadas merged 1 commit into
rossoctl:mainfrom
davidhadas:remove_opa_plugin_dependency

Conversation

@davidhadas

@davidhadas davidhadas commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Previously, the OPA plugin created separate SDK instances for inbound and outbound pipelines, resulting in:

  • Two bundle downloads from the bundle service
  • Duplicate memory usage for policy storage
  • Redundant OPA SDK initialization logs

This commit introduces a process-wide singleton pattern that shares a single OPA SDK instance across both pipelines when they use the same bundle_url and agent_id.

Key changes:

  1. Singleton OPA SDK Management

    • Added sharedSDK struct with reference counting
    • Package-level singleton variable protected by singletonMu mutex
    • First plugin instance creates the SDK, subsequent instances reuse it
    • SDK is stopped only when the last reference is released
  2. OPA Logging Bridge

    • Added slogBridge to integrate OPA SDK logs into authbridge's slog
    • OPA bundle downloads, parse errors, and activation now visible in logs
    • Tagged with "component: opa-sdk" for easy filtering
  3. SPIFFE ID Encoding

    • Changed bundle resource path from bundles/{agent-id}.tar.gz to bundles?spiffe={url-encoded-spiffe-id}
    • Properly URL-encodes SPIFFE IDs (slashes become %2F)
    • Strips "spiffe://" prefix before encoding
    • Supports both SPIFFE ID and legacy agent ID formats
  4. Dependency Relaxation

    • Removed hard Requires: ["jwt-validation"] dependency
    • Removed RequiresAny: ["a2a-parser", "mcp-parser", "inference-parser"]
    • OPA can now run independently without requiring other plugins
  5. Test Infrastructure

    • Added resetSingleton() for test isolation
    • Added newTestOPA() helper to simplify test setup
    • New tests for singleton reuse, ref counting, and shutdown behavior
    • Updated bundle resource path tests for SPIFFE ID encoding

Benefits:

  • Single bundle download per agent (50% reduction in network traffic)
  • Single OPA SDK instance (50% reduction in memory usage)
  • Cleaner logs with OPA SDK messages properly integrated
  • More flexible plugin ordering (no hard dependencies)

Summary by CodeRabbit

  • New Features

    • Enhanced OPA logging with structured fields and configurable log levels integrated with Go's standard logging.
  • Chores

    • Updated OPA bundle fetching mechanism to use SPIFFE ID-based queries instead of path-based agent identifiers.
    • Optimized OPA SDK resource management with process-wide singleton pattern and reference counting.
    • Updated documentation reflecting new bundle configuration approach.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0d306cdf-30fd-426e-913b-816db0f0dc55

📥 Commits

Reviewing files that changed from the base of the PR and between 70ce1d5 and 3ada324.

📒 Files selected for processing (3)
  • authbridge/authlib/plugins/opa/README.md
  • authbridge/authlib/plugins/opa/plugin.go
  • authbridge/authlib/plugins/opa/plugin_test.go
✅ Files skipped from review due to trivial changes (1)
  • authbridge/authlib/plugins/opa/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • authbridge/authlib/plugins/opa/plugin_test.go

📝 Walkthrough

Walkthrough

The OPA plugin is refactored to use a process-wide shared singleton for the OPA SDK with reference counting, introduces slog-based logging for OPA activity, and changes bundle fetching to use SPIFFE ID query parameters instead of path-based URIs.

Changes

OPA Plugin Shared Singleton & SPIFFE Configuration

Layer / File(s) Summary
Logging bridge and shared singleton types
authbridge/authlib/plugins/opa/plugin.go
Add time import and OPA logging alias; introduce slogBridge to translate OPA SDK logs to slog; define sharedSDK with refCount and readiness; refactor OPA struct to hold atomic pointer to shared singleton.
Capability declaration simplification
authbridge/authlib/plugins/opa/plugin.go
Remove Requires and RequiresAny from Capabilities() method.
Initialization and shared SDK startup
authbridge/authlib/plugins/opa/plugin.go
Init logs startup and synchronously calls startOPA() when agent_id is configured. startOPA reuses existing singleton (incrementing refCount) or creates new: builds OPA config with slogBridge logger, constructs SDK, waits on bundle readiness with 30-second timeout, marks shared singleton ready.
Shutdown and readiness state management
authbridge/authlib/plugins/opa/plugin.go
Shutdown decrements refCount and stops SDK only when refCount reaches zero, clearing singleton. Ready returns false when singleton is absent, otherwise mirrors shared readiness flag.
SPIFFE ID extraction and bundle resource configuration
authbridge/authlib/plugins/opa/plugin.go
buildOPAConfig derives SPIFFE ID by trimming spiffe:// prefix and URL-encoding; sets bundle resource to bundles?spiffe=<encoded>. Add decision_logs and logging sections to generated config.
Request and response decision execution via shared singleton
authbridge/authlib/plugins/opa/plugin.go
OnRequest and OnResponse gate on shared singleton existence and readiness; route decisions through s.decider.Decision(...) while preserving deny/allow semantics.
Test helper functions
authbridge/authlib/plugins/opa/plugin_test.go
Add newTestOPA to construct plugin with mock sharedSDK; add resetSingleton for test isolation.
Test updates for capabilities and request/response paths
authbridge/authlib/plugins/opa/plugin_test.go
Update TestCapabilities for empty Requires/RequiresAny. Refactor OnRequest/OnResponse tests to use newTestOPA instead of manual setup.
Configuration and singleton lifecycle tests
authbridge/authlib/plugins/opa/plugin_test.go
Update TestBuildOPAConfig to expect bundles?spiffe=<encoded> queries. Add table-driven SPIFFE encoding tests covering multiple formats and special characters. Add singleton lifecycle tests: reuse/refCount increment, bundleURL preservation, ref-counted Shutdown with SDK Stop on final shutdown.
Documentation updates for SPIFFE bundle queries
authbridge/authlib/plugins/opa/README.md
Update "How it works" and "Bundle layout" sections to describe fetching bundles via bundles?spiffe=<url-encoded-spiffe-id> endpoint derived from /shared/client-id.txt or agent_id.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • kagenti/kagenti-extensions#427: Earlier OPA plugin implementation that introduced the core OPA struct and decision flow now being refactored to use shared singleton and SPIFFE-based bundle queries.

Suggested reviewers

  • huang195
  • mrsabath
  • Alan-Cha
  • maia-iyer

Poem

🐰 A singleton hops through the OPA grove,
SPIFFE IDs encoded with URL love,
Reference counts keep the SDK alive,
slog bridges sing while decisions thrive! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 identifies the primary change: implementing a singleton OPA SDK to eliminate duplicate bundle downloads. It is concise, specific, and accurately reflects the main objective of the PR.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 3

🤖 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 `@authbridge/authlib/plugins/opa/plugin_test.go`:
- Around line 1277-1311: Update TestStartOPA_SingletonDifferentBundleURL to
assert that differing bundle_url does NOT reuse the existing singleton: call
p.startOPA() and expect either an init error or a new sharedSDK instead of
reusing singleton; specifically replace the current reuse assertions with checks
that p.shared != singleton, singleton.refCount remains 1, and the new shared
instance (p.shared) has bundleURL "http://second-service:9090" (or assert a
non-nil error from startOPA if the intended behavior is to fail). Reference
TestStartOPA_SingletonDifferentBundleURL, startOPA, singleton, sharedSDK,
bundleURL, refCount and p.shared when making the change.

In `@authbridge/authlib/plugins/opa/plugin.go`:
- Around line 265-278: The startOPA singleton reuse currently only checks
singleton != nil and can bind plugins with different p.cfg.BundleURL or agentID
to the same SDK, causing policy mismatches; update startOPA to key the shared
SDK by at least {bundleURL, agentID} (or return an error on mismatch) instead of
unconditionally reusing singleton: change the singleton logic used in startOPA
to lookup or store a map keyed by bundleURL+agentID (or compare both
singleton.bundleURL and singleton.agentID and fail fast), ensure singletonMu
still guards the map, increment refCount only for a matching key, and set
p.shared from the matched entry (or return an error if mismatched) so that
p.shared is never assigned a singleton with a different bundle or agent
identity.
- Around line 176-181: The p.shared field is accessed concurrently and can be
nilled in Shutdown, causing a data race and intermittent nil derefs; change
p.shared from a plain pointer to an atomic.Pointer[*sharedSDK] (or protect it
with a single mutex) and update startOPA to Store the pointer and Shutdown to
Store(nil) atomically, then modify Ready, OnRequest, and OnResponse to
atomically Load p.shared into a local variable once (e.g., local :=
p.shared.Load()) and use that snapshot for all checks and dereferences
(decider/ready) for the lifetime of the handler; apply the same pattern to other
reader sites mentioned (the ranges around lines noted) so all reads/writes use
the atomic load/store (or the same mutex) to eliminate the race and possible nil
deref.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8aed0a82-ddac-4de9-952b-5e7f20978d84

📥 Commits

Reviewing files that changed from the base of the PR and between 0680516 and 878a5a1.

📒 Files selected for processing (2)
  • authbridge/authlib/plugins/opa/plugin.go
  • authbridge/authlib/plugins/opa/plugin_test.go

Comment on lines +1277 to +1311
func TestStartOPA_SingletonDifferentBundleURL(t *testing.T) {
resetSingleton()
defer resetSingleton()

// Seed the singleton with bundle_url "http://first-service:8080".
var dec decider = &mockDecider{result: &sdk.DecisionResult{Result: true}}
singletonMu.Lock()
singleton = &sharedSDK{
decider: dec,
bundleURL: "http://first-service:8080",
agentID: "spiffe://example.com/ns/team1/sa/agent1",
refCount: 1,
}
singleton.ready.Store(true)
singletonMu.Unlock()

// Second instance with a DIFFERENT bundle_url should still reuse the
// singleton (warning logged, second config skipped).
p := &OPA{
cfg: opaConfig{BundleURL: "http://second-service:9090"},
agentID: "spiffe://example.com/ns/team1/sa/agent1",
}
if err := p.startOPA(); err != nil {
t.Fatalf("startOPA failed: %v", err)
}
if p.shared != singleton {
t.Fatal("expected second instance to reuse singleton despite different bundle_url")
}
if singleton.refCount != 2 {
t.Errorf("expected refCount=2, got %d", singleton.refCount)
}
// The singleton should retain the FIRST bundle_url.
if singleton.bundleURL != "http://first-service:8080" {
t.Errorf("expected singleton to keep first bundle_url, got %q", singleton.bundleURL)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This test is asserting the wrong reuse contract.

The PR objective says the SDK is shared when bundle_url and agent_id match, but this test expects reuse when bundle_url differs. Keeping that expectation will hide the policy-isolation bug in startOPA; it should assert a separate instance or an init failure instead.

🤖 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 `@authbridge/authlib/plugins/opa/plugin_test.go` around lines 1277 - 1311,
Update TestStartOPA_SingletonDifferentBundleURL to assert that differing
bundle_url does NOT reuse the existing singleton: call p.startOPA() and expect
either an init error or a new sharedSDK instead of reusing singleton;
specifically replace the current reuse assertions with checks that p.shared !=
singleton, singleton.refCount remains 1, and the new shared instance (p.shared)
has bundleURL "http://second-service:9090" (or assert a non-nil error from
startOPA if the intended behavior is to fail). Reference
TestStartOPA_SingletonDifferentBundleURL, startOPA, singleton, sharedSDK,
bundleURL, refCount and p.shared when making the change.

Comment thread authbridge/authlib/plugins/opa/plugin.go
Comment thread authbridge/authlib/plugins/opa/plugin.go

@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: 2

🧹 Nitpick comments (1)
.github/workflows/auto-sync-and-merge.yml (1)

48-52: 💤 Low value

Consider parameterizing the hardcoded username.

The username davidhadas is hardcoded, reducing maintainability. Consider using a repository variable or workflow input.

♻️ Suggested refactor using repository variable
+env:
+  FORK_OWNER: ${{ vars.FORK_OWNER || 'davidhadas' }}
+
 jobs:
   sync-and-merge:

Then in the jq filter:

-          --jq '.[] | select(.headRepositoryOwner.login=="davidhadas") | .headRefName')
+          --jq '.[] | select(.headRepositoryOwner.login=="'"$FORK_OWNER"'") | .headRefName')
🤖 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 @.github/workflows/auto-sync-and-merge.yml around lines 48 - 52, The jq
filter currently hardcodes the username ("davidhadas") inside the gh pr list
pipeline (variable prs); change this to use a parameter or environment variable
(e.g., a workflow input or repo variable like USERNAME) and reference it in the
jq expression instead of the literal string; update the gh pr list invocation
that produces prs to interpolate the variable into the jq filter (ensuring
proper quoting/escaping for shell and jq) and add the corresponding workflow
input or env default so the username can be configured without editing the
workflow.
🤖 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 @.github/workflows/auto-sync-and-merge.yml:
- Around line 58-77: The step currently expands ${{
steps.prlist.outputs.branches }} directly into the shell which can allow shell
metacharacters from branch names to be interpreted; instead assign that output
to a quoted environment variable (e.g., BRANCHES) and consume it safely in the
loop (preserve the existing while IFS= read -r branch; do ... done <<<
"$BRANCHES") so the content is not re-parsed by the shell; update the job to
pass BRANCHES: ${{ steps.prlist.outputs.branches }} in env and replace the
unquoted heredoc expansion with the quoted environment variable in the merge
loop (keep git checkout/merge/push logic intact).
- Around line 15-21: Update the Checkout step to pin actions/checkout to the
immutable SHA by replacing "uses: actions/checkout@v4" with "uses:
actions/checkout@4823ef7dded2f227249189a5d8cc3efa39f98340" and add
"persist-credentials: false" under the step's with: block to disable credential
persistence; also remove the trailing blank-line whitespace after the "Fetch all
branches" step (the git fetch origin '+refs/heads/*:refs/remotes/origin/*' run)
so there is no extra whitespace line.

---

Nitpick comments:
In @.github/workflows/auto-sync-and-merge.yml:
- Around line 48-52: The jq filter currently hardcodes the username
("davidhadas") inside the gh pr list pipeline (variable prs); change this to use
a parameter or environment variable (e.g., a workflow input or repo variable
like USERNAME) and reference it in the jq expression instead of the literal
string; update the gh pr list invocation that produces prs to interpolate the
variable into the jq filter (ensuring proper quoting/escaping for shell and jq)
and add the corresponding workflow input or env default so the username can be
configured without editing the workflow.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 70bcb820-d7eb-4a6b-8bec-6ffbb7c98ff3

📥 Commits

Reviewing files that changed from the base of the PR and between 878a5a1 and 70ce1d5.

📒 Files selected for processing (1)
  • .github/workflows/auto-sync-and-merge.yml

Comment on lines +15 to +21
- name: Checkout fork
uses: actions/checkout@v4
with:
token: ${{ secrets.AUTO_MERGE_TOKEN }}

- name: Fetch all branches
run: git fetch origin '+refs/heads/*:refs/remotes/origin/*'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What is the latest commit SHA for actions/checkout v4?

💡 Result:

The tag v4 for actions/checkout is a mutable reference, meaning it can point to different commit SHAs over time as the repository is updated [1]. As of June 10, 2026, the v4 branch in the actions/checkout repository points to the latest commit, which is 4823ef7dded2f227249189a5d8cc3efa39f98340 [2]. Because tags like v4 are mutable, it is a common best practice in GitHub Actions to pin your workflow references to a specific, immutable full-length commit SHA (e.g., 4823ef7dded2f227249189a5d8cc3efa39f98340) rather than a version tag [1][3]. This ensures that your workflow behavior remains consistent and is not affected by future updates to the tagged reference [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/auto-sync-and-merge.yml"
echo "== File = $FILE =="
nl -ba "$FILE" | sed -n '1,120p'
echo
echo "== Trailing whitespace check (show lines) =="
# Print only lines with trailing whitespace
awk '{ if (match($0, /[ \t]+$/)) print NR ":" $0 }' "$FILE" | head -n 200

Repository: kagenti/kagenti-extensions

Length of output: 186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/auto-sync-and-merge.yml"
echo "== File = $FILE =="
awk '{printf "%4d:%s\n", NR, $0}' "$FILE" | sed -n '1,140p'

echo
echo "== Trailing whitespace check (lines with trailing spaces/tabs) =="
awk '{ if ($0 ~ /[ \t]+$/) printf "%4d:%s\n", NR, $0 }' "$FILE" | head -n 200

Repository: kagenti/kagenti-extensions

Length of output: 2947


Pin actions/checkout to an immutable SHA, disable credential persistence, and remove trailing whitespace.

Pin actions/checkout@v4 to actions/checkout@4823ef7dded2f227249189a5d8cc3efa39f98340 (mutable tag). Add persist-credentials: false under the Checkout fork step’s with: block. Remove trailing whitespace on the blank line after the Fetch all branches git fetch origin ... step (line 22).

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 15-18: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 16-16: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 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 @.github/workflows/auto-sync-and-merge.yml around lines 15 - 21, Update the
Checkout step to pin actions/checkout to the immutable SHA by replacing "uses:
actions/checkout@v4" with "uses:
actions/checkout@4823ef7dded2f227249189a5d8cc3efa39f98340" and add
"persist-credentials: false" under the step's with: block to disable credential
persistence; also remove the trailing blank-line whitespace after the "Fetch all
branches" step (the git fetch origin '+refs/heads/*:refs/remotes/origin/*' run)
so there is no extra whitespace line.

Source: Linters/SAST tools

Comment on lines +58 to +77
- name: Merge upstream main into each PR branch
run: |
echo "Branches to update:"
echo "${{ steps.prlist.outputs.branches }}"

while IFS= read -r branch; do
if [ -z "$branch" ]; then
continue
fi

echo "Updating branch: $branch"

git checkout "$branch"
git merge main --no-edit || exit 1
git push origin "$branch"

echo "Merged and pushed for $branch"
done <<< "${{ steps.prlist.outputs.branches }}"
env:
GITHUB_TOKEN: ${{ secrets.AUTO_MERGE_TOKEN }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mitigate template injection by using environment variables.

Direct expansion of ${{ steps.prlist.outputs.branches }} in the shell script is vulnerable to injection if branch names contain shell metacharacters. While the source is filtered to a specific owner, branch names can contain characters like $(), ;, or backticks that would be interpreted by the shell.

Pass the value through an environment variable instead, which prevents shell interpretation of the content.

🔒 Proposed fix
     - name: Merge upstream main into each PR branch
+      env:
+        GITHUB_TOKEN: ${{ secrets.AUTO_MERGE_TOKEN }}
+        BRANCHES: ${{ steps.prlist.outputs.branches }}
       run: |
         echo "Branches to update:"
-        echo "${{ steps.prlist.outputs.branches }}"
+        echo "$BRANCHES"

         while IFS= read -r branch; do
           if [ -z "$branch" ]; then
             continue
           fi

           echo "Updating branch: $branch"

           git checkout "$branch"
           git merge main --no-edit || exit 1
           git push origin "$branch"

           echo "Merged and pushed for $branch"
-        done <<< "${{ steps.prlist.outputs.branches }}"
-      env:
-        GITHUB_TOKEN: ${{ secrets.AUTO_MERGE_TOKEN }}
+        done <<< "$BRANCHES"
🧰 Tools
🪛 zizmor (1.25.2)

[info] 61-61: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 75-75: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 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 @.github/workflows/auto-sync-and-merge.yml around lines 58 - 77, The step
currently expands ${{ steps.prlist.outputs.branches }} directly into the shell
which can allow shell metacharacters from branch names to be interpreted;
instead assign that output to a quoted environment variable (e.g., BRANCHES) and
consume it safely in the loop (preserve the existing while IFS= read -r branch;
do ... done <<< "$BRANCHES") so the content is not re-parsed by the shell;
update the job to pass BRANCHES: ${{ steps.prlist.outputs.branches }} in env and
replace the unquoted heredoc expansion with the quoted environment variable in
the merge loop (keep git checkout/merge/push logic intact).

Source: Linters/SAST tools

@davidhadas
davidhadas force-pushed the remove_opa_plugin_dependency branch 2 times, most recently from 3e5249e to ce9910d Compare June 10, 2026 19:12

@huang195 huang195 left a comment

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.

Review Summary

Clean refactor with strong test coverage — the singleton + ref-counting logic is correct (lock held across SDK init, ref-count drives shutdown, double-Shutdown is guarded by shared.Swap(nil)), and the new tests cover reuse, ref-counting, shutdown, and SPIFFE encoding.

Two things worth settling before merge:

  1. The bundles?spiffe= resource is a contract change with the Kagenti Bundle Server (a separate component) — confirm the server speaks the query form, and update the plugin README.md (line 12) which still documents bundles/<agent-id>.tar.gz.
  2. The singleton's reuse guard keys on bundle_url rather than agent_id, which is the field that actually selects the policy bundle — fine under one-agent-per-process, but worth making the invariant explicit so a future multi-agent process fails loud.

Dependency relaxation (ordering vs jwt-validation/parsers) and the ready-goroutine leak are softer notes inline.

Areas reviewed: Go (plugin + tests), docs · Commits: 1, signed-off ✅ · CI: all green
Conventions: title feat(opa): is lowercase — the org rule (embedded in your PR template) requires exact-case Feat; and the body has no ## Summary heading. Minor, but the title may block at the ruleset.

No code blockers under the current deployment model — approving, contingent on confirming the bundle-server contract.

Assisted-By: Claude Code

"authz": map[string]any{
"service": "kagenti",
"resource": fmt.Sprintf("bundles/%s.tar.gz", escapedAgentID),
"resource": fmt.Sprintf("bundles?spiffe=%s", escapedSPIFFEID),

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.

suggestion (verify before merge) — The bundle resource changes from bundles/<id>.tar.gz to bundles?spiffe=<url-encoded-id>. This is a contract change with the Kagenti Bundle Server (a separate component). If the server isn't already serving the query form, every bundle download 404s → OPA never becomes ReadyOnRequest/OnResponse deny everything. Please confirm the server supports bundles?spiffe= before merge.

Also: the plugin's own README.md:12 still documents bundles/<agent-id>.tar.gz — update it to match the new path.

(Minor: a ? embedded in OPA's bundle resource field is unusual — worth a sanity check that the OPA SDK forwards it to the service URL un-re-escaped.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The contract indeed has intentionally changed.
The Readme need to be updated.

defer singletonMu.Unlock()

if singleton != nil {
if singleton.bundleURL != p.cfg.BundleURL {

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.

suggestion — This reuse guard compares bundleURL, but the value that actually selects the policy bundle is the agent identity (bundles?spiffe=<agentID>), and different agents share the same bundle_url (the service base). So for a same-URL/different-agent case this warning never fires and the second instance silently reuses the first agent's SDK — meaning agent B would be evaluated against agent A's policy.

That's safe today (one agent per AuthBridge process; inbound + outbound share one agentID), but the guard is checking the wrong field for the invariant it protects. Suggest keying on agentID (or asserting it matches) so that if a process ever serves more than one agent, it fails loud rather than cross-applying policy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Seems that we need to keep it as is - we cant have two different urls on the config. This is what we protect here.

The entire design is for a single identity.
Agent id is not part of the config, it comes from the client id file - there is one file for one agent and therefore one id which is used for both upstream and downstream. This is what we support for now and if this changes, we ned to rethink the OPA singleton use and other aspects of the design.

@@ -108,8 +189,6 @@ func (p *OPA) Name() string { return "opa" }

func (p *OPA) Capabilities() pipeline.PluginCapabilities {

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.

suggestion — Dropping Requires: [jwt-validation] and RequiresAny: [parsers] removes the pipeline's guarantee that identity claims and parsed request data are populated before OPA evaluates. A pipeline without jwt-validation was previously rejected; now OPA runs with missing claims in buildInput, which can silently weaken policy decisions. Worth confirming the default pipeline still orders jwt-validation/parsers ahead of opa, and that policies tolerate absent claims defensively.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OPA is used also in outbound where jwt-validationis missing
OPA also rely on a2a parser and mcp parser etc. but non is mandatory.
Use cases where OPA is needed although non of the above are used is not impossible.
Therefore failing the agent due to a pipeline config that has OPA without specific other plugins seems counter productive. OPA should use what it has. Rego inside may fail requests or responses inbound or outbound if info is missing on the context (leading to missing info on the input document. This is left for the admins to decide instead of failing the agent and not allowing it to start.

s.ready.Store(true)
slog.Info("opa: bundle loaded and policy activated", "agent_id", agentID)
return
case <-time.After(30 * time.Second):

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.

nit — This readiness goroutine isn't tied to Shutdown/bgCancel. If the plugin shuts down before the bundle becomes ready, the trailing <-readyCh blocks forever and the goroutine leaks. Consider selecting on a cancel/done channel alongside readyCh.

@huang195

Copy link
Copy Markdown
Member

@davidhadas we can merge now or would you like to make some changes based on the reviews? Will wait until next week in case you want to make some changes.

@davidhadas

Copy link
Copy Markdown
Contributor Author

Let me fix the readme and readiness goroutine

…downloads

Previously, the OPA plugin created separate SDK instances for inbound and
outbound pipelines, resulting in:
- Two bundle downloads from the bundle service
- Duplicate memory usage for policy storage
- Redundant OPA SDK initialization logs

This commit introduces a process-wide singleton pattern that shares a single
OPA SDK instance across both pipelines when they use the same bundle_url and
agent_id.

Key changes:

1. **Singleton OPA SDK Management**
   - Added `sharedSDK` struct with reference counting
   - Package-level `singleton` variable protected by `singletonMu` mutex
   - First plugin instance creates the SDK, subsequent instances reuse it
   - SDK is stopped only when the last reference is released

2. **OPA Logging Bridge**
   - Added `slogBridge` to integrate OPA SDK logs into authbridge's slog
   - OPA bundle downloads, parse errors, and activation now visible in logs
   - Tagged with "component: opa-sdk" for easy filtering

3. **SPIFFE ID Encoding**
   - Changed bundle resource path from `bundles/{agent-id}.tar.gz` to
     `bundles?spiffe={url-encoded-spiffe-id}`
   - Properly URL-encodes SPIFFE IDs (slashes become %2F)
   - Strips "spiffe://" prefix before encoding
   - Supports both SPIFFE ID and legacy agent ID formats

4. **Dependency Relaxation**
   - Removed hard `Requires: ["jwt-validation"]` dependency
   - Removed `RequiresAny: ["a2a-parser", "mcp-parser", "inference-parser"]`
   - OPA can now run independently without requiring other plugins

5. **Test Infrastructure**
   - Added `resetSingleton()` for test isolation
   - Added `newTestOPA()` helper to simplify test setup
   - New tests for singleton reuse, ref counting, and shutdown behavior
   - Updated bundle resource path tests for SPIFFE ID encoding

Benefits:
- Single bundle download per agent (50% reduction in network traffic)
- Single OPA SDK instance (50% reduction in memory usage)
- Cleaner logs with OPA SDK messages properly integrated
- More flexible plugin ordering (no hard dependencies)

Signed-off-by: David Hadas <david.hadas@gmail.com>
@davidhadas
davidhadas force-pushed the remove_opa_plugin_dependency branch from ce9910d to 3ada324 Compare June 11, 2026 20:31
@davidhadas davidhadas changed the title feat(opa): implement singleton OPA SDK to eliminate duplicate bundle … Feat(opa): implement singleton OPA SDK to eliminate duplicate bundle … Jun 11, 2026
@davidhadas
davidhadas merged commit f1c5e76 into rossoctl:main Jun 11, 2026
21 of 22 checks passed
@github-project-automation github-project-automation Bot moved this from New /:ToDo to Done in Rossoctl Issue Prioritization Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants