Adding an opa plugin#427
Conversation
|
As for security scoring - OPA relies on Low score ≠ vulnerability. We can engage with the OPA community about them if we choose. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an OPA-backed authorization plugin to AuthBridge: configuration, SDK lifecycle and bundle activation, four decision paths for inbound/outbound request/response, include-set-driven input shaping for extensions, comprehensive tests and docs, and compile-time registration in envoy/lite/proxy with go.mod updates. ChangesOPA Authorization Plugin
Sequence Diagram(s)sequenceDiagram
participant Pipeline as AuthBridgePipeline
participant OPAPlugin as OPA plugin
participant OPASDK as OPASDK
participant BundleServer as BundleServer
Pipeline->>OPAPlugin: Configure / Init
OPAPlugin->>OPASDK: startOPA(bundle: bundles/<agentID>.tar.gz)
OPASDK->>BundleServer: GET bundle_url/bundles/<agentID>.tar.gz
BundleServer-->>OPASDK: bundle tar.gz
OPASDK-->>OPAPlugin: Ready
Pipeline->>OPAPlugin: OnRequest/OnResponse(input)
OPAPlugin->>OPASDK: Evaluate(path, input)
OPASDK-->>OPAPlugin: decision (allow|deny|undefined|error)
OPAPlugin-->>Pipeline: continue|reject (record outcome)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 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/go.mod`:
- Around line 34-40: Remove the duplicate require entry for
github.com/goccy/go-json v0.10.3 in the go.mod (the repeated
"github.com/goccy/go-json v0.10.3 // indirect" lines); keep a single entry and
then run go mod tidy to reconcile module metadata. Locate the duplicate in the
authlib go.mod, delete the redundant line, and verify with go mod tidy that no
other duplicates remain.
In `@authbridge/authlib/plugins/opa/plugin.go`:
- Around line 247-250: The OnRequest branch that returns pipeline.DenyStatus
when p.decider == nil must also emit a session event so not-ready denials are
observable; locate the OPA.OnRequest method, and before returning
pipeline.DenyStatus(503, "upstream.unreachable", "opa policy engine not
initialized") add a call to the plugin's session-event emitter (use the existing
observability helper used elsewhere in this plugin, e.g., p.emitSessionEvent /
p.EmitSessionEvent or the module's session event helper) with action "deny" and
the same reason/metadata so the denial is recorded in session observability.
- Around line 198-208: The code in buildOPAConfig constructs the bundle resource
using p.agentID directly ("bundles/%s.tar.gz"), which allows path traversal or
invalid characters; update buildOPAConfig to validate or sanitize p.agentID
before use (e.g., enforce a strict whitelist regexp like /^[A-Za-z0-9._-]+$/ or
similar) and reject or error on invalid values, or alternatively
percent-encode/URL-escape the agent id when interpolating into the resource
string; ensure the check/escape is applied to p.agentID before creating the
fmt.Sprintf("bundles/%s.tar.gz", p.agentID) value so the OPA bundle resource
cannot be manipulated by malicious input.
- Around line 93-100: There is a data race because OPA fields p.decider and
p.agentID are written in background goroutines (Init/startOPA) and read in
OnRequest/OnResponse; fix by protecting these fields with a sync.Mutex on the
OPA struct or replacing them with an atomic.Value and always read/write via the
chosen concurrency primitive (update Init, startOPA, OnRequest, OnResponse to
use it consistently). In buildOPAConfig validate/sanitize the agentID before
using it to build the bundle path (reject any agentID containing '/', '\\', ".."
or path separators or return an error) so fmt.Sprintf("bundles/%s.tar.gz",
p.agentID) cannot escape directories. Finally ensure the “not-ready” path in
OnRequest emits observability like OnResponse does (call
pctx.Skip("opa_not_ready") or pctx.Record(...) before returning
pipeline.DenyStatus(...)) so session events are recorded when p.decider == nil.
In `@authbridge/authlib/plugins/opa/README.md`:
- Around line 388-395: The README.md contains multiple fenced code blocks
lacking language identifiers (causing MD040); update each backtick fence in the
shown sections (around the example file trees at lines referenced) to include a
language tag such as text (e.g., change ``` to ```text) for every tree/list
block (the three blocks at 388-395 and the other blocks around 414-419 and
423-432) so markdownlint and docs CI pass; ensure you only add the language
identifier to the opening fences and leave the inner content unchanged.
In `@authbridge/authlib/plugins/README.md`:
- Line 30: The table row for the `opa` plugin is in the wrong section; remove
the `| `opa` | OPA policy evaluation on inbound and outbound requests via bundle
download |` row from the "Reusable building blocks for plugin authors" table and
add the same row into the "Built-in plugins" table (preserving table formatting
and column order), and then run the repository markdown spellcheck action to
ensure no MD lint errors remain; locate these edits by searching for the `opa`
plugin entry and the table headers "Reusable building blocks for plugin authors"
and "Built-in plugins" in README.md.
In `@authbridge/cmd/authbridge-lite/go.mod`:
- Around line 65-69: The go.mod contains duplicate require entries for the same
golang.org/x/* modules; open go.mod and remove the duplicated require lines so
each module (golang.org/x/crypto, golang.org/x/net, golang.org/x/sync,
golang.org/x/sys, golang.org/x/text) appears only once with the intended
version, then run `go mod tidy` to verify and update the file; ensure you edit
the duplicate entries in the go.mod require block (look for the repeated module
names) rather than changing other files.
In `@authbridge/cmd/authbridge-lite/main.go`:
- Around line 41-44: The file comment about "Auth gates only: drop the parsers
and token-broker." is out of sync with the imports: jwtvalidation
(github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/jwtvalidation),
opa (github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/opa) and
tokenexchange
(github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/tokenexchange)
are still being compiled; either update the comment to accurately list the
included plugins (jwtvalidation, opa, tokenexchange) or remove the tokenexchange
import if token-broker/tokenexchange should indeed be dropped—locate the import
block in main.go and make the corresponding change so the comment and the
imported plugin set match.
In `@authbridge/cmd/authbridge-proxy/go.mod`:
- Around line 65-69: The go.mod contains duplicate require entries for the
golang.org/x modules (golang.org/x/crypto, golang.org/x/net, golang.org/x/sync,
golang.org/x/sys, golang.org/x/text); remove the redundant duplicates so each of
those module requirements appears only once in the require block, keeping the
desired version (e.g., v0.48.0, v0.49.0, v0.19.0, v0.41.0, v0.34.0) and running
`go mod tidy` afterwards to ensure module graph consistency (look for the
require lines referencing golang.org/x/crypto, golang.org/x/net,
golang.org/x/sync, golang.org/x/sys, golang.org/x/text).
🪄 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: 4d76c28e-2767-411d-b54a-f7d213fa3645
⛔ Files ignored due to path filters (4)
authbridge/authlib/go.sumis excluded by!**/*.sumauthbridge/cmd/authbridge-envoy/go.sumis excluded by!**/*.sumauthbridge/cmd/authbridge-lite/go.sumis excluded by!**/*.sumauthbridge/cmd/authbridge-proxy/go.sumis excluded by!**/*.sum
📒 Files selected for processing (12)
authbridge/authlib/go.modauthbridge/authlib/plugins/README.mdauthbridge/authlib/plugins/opa/README.mdauthbridge/authlib/plugins/opa/plugin.goauthbridge/authlib/plugins/opa/plugin_test.goauthbridge/authlib/plugins/plugins_test.goauthbridge/cmd/authbridge-envoy/go.modauthbridge/cmd/authbridge-envoy/main.goauthbridge/cmd/authbridge-lite/go.modauthbridge/cmd/authbridge-lite/main.goauthbridge/cmd/authbridge-proxy/go.modauthbridge/cmd/authbridge-proxy/main.go
|
@davidhadas can you do a conflict resolution before I start reviewing this PR? I think some of the changes in this PR has already been merged in earlier PRs |
- Add new OPA plugin that integrates Open Policy Agent for authorization decisions - Plugin supports synchronous policy evaluation with configurable timeout - Includes comprehensive test coverage - Update plugin registry and documentation - Fix observability for agent ID escaping and synchronization Signed-off-by: David Hadas <david.hadas@gmail.com>
|
@huang195, I believe it is now resolved |
7794039 to
6768456
Compare
|
@huang195 as for OpenSSF Scorecard |
huang195
left a comment
There was a problem hiding this comment.
Review summary
Clean, well-structured plugin — registration is identical across all three data-plane binaries, OPA v1.4.2 is pinned consistently with no version skew, Capabilities() correctly omits ReadsBody (it consumes parser extensions, not the raw body), url.PathEscape guards the agent-id path segment, and Shutdown properly stops the SDK and the bundle poller. I verified the headline: genuine OPA evaluation errors fail closed (403) — exactly right for an authz gate.
The blocking items are all about fail-open behavior and credential hygiene on what is a security-critical authorization plugin:
- #1/#2 — a startup window and a no-bundle-loaded case where requests are allowed with no policy applied.
- #3 — the raw
Authorizationbearer token is placed into the OPA policy input unnecessarily.
Details inline. The rest are suggestions/nits.
Areas reviewed: plugin logic + fail-mode semantics + concurrency + input construction, registration across the 3 binaries, go.mod/go.sum deps, READMEs. Commits: 1 feat (DCO ✓). CI: all green.
Minor, non-inline: abctl/go.sum carries the full OPA transitive hash set although abctl doesn't register the plugin (a go mod tidy workspace artifact — harmless); and p.agentID is written in the Init goroutine and read in startOPA/buildOPAConfig as a plain field (benign in practice, but an unsynchronized cross-goroutine access).
| } | ||
|
|
||
| func (p *OPA) OnRequest(ctx context.Context, pctx *pipeline.Context) pipeline.Action { | ||
| dec := p.decider.Load() |
There was a problem hiding this comment.
must-fix — startup fail-open window. OnRequest gates only on p.decider.Load() != nil; it never consults p.ready. Per the OPA SDK docs, sdk.New with a Ready channel set returns immediately (non-blocking) and signals readiness asynchronously by closing the channel — so startOPA stores the decider (line 195) before any bundle is active, and p.ready flips only later. A request arriving in that gap queries a decision path with no activated policy → the SDK returns an undefined-decision error → IsUndefinedErr (line 279) → Skip("no_policy_rule") → Continue (allow). So a request in the pre-bundle window is allowed with zero policy applied.
The README says the readiness probe holds traffic off the pod, but a security gate shouldn't fail open in its own logic if the probe is missing, racy, or the caller is in-process. Fix: gate OnRequest on p.ready.Load() (deny 503 until ready), or pass Block: true to sdk.New so the decider is only stored once the first bundle activates.
There was a problem hiding this comment.
- Ready() stays false until the bundle activates — p.ready.Store(true) only fires after <-readyCh (line 198). The pipeline's Ready() (line 263 of pipeline.go) iterates all plugins implementing Readier; OPA's Ready() returns false → pipeline reports not-ready → /readyz
returns 503. - OnRequest is never called before the bundle loads — because Kubernetes won't route traffic to the pod until /readyz passes. The listeners start immediately (line 261-262 of main.go), but the readiness probe on :9091/readyz gates Kubernetes Service endpoints. No endpoint → no
traffic → OnRequest never fires.
This means we seem to not have a race condition as described.
The REAME seems wrong
Still, as protective coding, let add !p.ready.Load() to the OnRequest gate
| Input: input, | ||
| }) | ||
| if err != nil { | ||
| if sdk.IsUndefinedErr(err) { |
There was a problem hiding this comment.
must-fix — "no bundle active" is indistinguishable from "no rule defined." This IsUndefinedErr → allow branch fires both when (a) a bundle loaded but lacks a rule for this path (the intended "no opinion → skip" design) and (b) no bundle ever loaded (bundle server unreachable at startup). Combined with #1, a bundle-server outage at startup degrades to allow-all for any request that reaches the plugin, rather than fail-closed. Consider a distinct "engine up but no policy active" state that denies, so a broken policy-distribution path can't be mistaken for an operator's deliberate omission of a rule.
There was a problem hiding this comment.
again, we are not suppose to see requests is Ready() is not up. - i.e. if bundle not loaded.
seems the previous fix suffice
| "method": pctx.Method, | ||
| "path": pctx.Path, | ||
| "host": pctx.Host, | ||
| "headers": flattenHeaders(pctx.Headers), |
There was a problem hiding this comment.
must-fix — raw Authorization bearer token placed into the policy input. buildInput sets headers: flattenHeaders(pctx.Headers) with no redaction, so the bearer token (and any cookie) end up in the OPA input document. Identity is already exposed structurally via input.identity (subject / client_id / scopes), so the raw token is unnecessary — and it becomes a leak surface for operator-authored Rego and for OPA decision logs if those are enabled. Recommend stripping authorization, cookie, and proxy-authorization from the flattened headers by default (or gating full headers behind an include key, like the body fields already are).
(For accuracy: the policy is evaluated in-process, so this isn't sent over the wire to the bundle server — the exposure is the Rego + decision-log surface, not the network.)
| func (p *OPA) OnResponse(ctx context.Context, pctx *pipeline.Context) pipeline.Action { | ||
| dec := p.decider.Load() | ||
| if dec == nil { | ||
| pctx.Skip("opa_not_ready") |
There was a problem hiding this comment.
suggestion — response-path asymmetry. With a nil decider, OnResponse does Skip("opa_not_ready") → allow, whereas OnRequest denies 503. Defensible since response evaluation is the "rare, expensive" fine-graining path — but it means an outbound/response data-exfiltration policy is silently unenforced during the startup window. Worth confirming this is intended if response-side policies are meant to be load-bearing for egress protection.
There was a problem hiding this comment.
both sides should be treated equally
| return nil | ||
| } | ||
|
|
||
| func (p *OPA) buildOPAConfig() ([]byte, string, error) { |
There was a problem hiding this comment.
suggestion — unauthenticated/un-pinned bundle fetch. buildOPAConfig emits only services.kagenti.url; there's no tls block (custom CA) or credentials block, and no bundle-signature verification. TLS relies on OPA SDK defaults (standard cert validation — good, not InsecureSkipVerify), but there's no way to pin a private CA for an internal bundle server or to authenticate to it. For a component that fetches authorization policy, a rogue or MITM bundle server could ship an allow-all bundle. At minimum document this trust assumption; ideally add a tls / bundle_signing config passthrough.
There was a problem hiding this comment.
This is an early version of the OPA focusing on function. TLS is intentionally left out for this first commit. IT is suitable for clusters in which HTTP is considered sufficient and for evaluating the new OPA functionality. Later PR will handle TLS, Certificates etc. It is planed that for spiffe, we will use mTLS with spiffe certs. for service accounts we will use TLS with tokens. But again, this is not in this PR.
| | Path | Phase | Purpose | | ||
| |------|-------|---------| | ||
| | `authbridge/inbound/request` | Inbound request | Primary authorization — validate caller identity, enforce access control | | ||
| | `authbridge/inbound/response` | Inbound response | Fine-grained response evaluation (rare, expensive) — inspect response body/headers | |
There was a problem hiding this comment.
nit — doc drift. This row says the inbound/response path can "inspect response body/headers," but OnResponse only populates status_code and headers in the input and never reads pctx.ResponseBody. The input-field reference and JSON example further down are accurate; just drop "body" here.
There was a problem hiding this comment.
The buildInput function doesn't directly include any raw HTTP body. Instead, it includes parsed protocol content from the extensions (A2A parts/content, MCP params/result, inference messages) — which are populated by upstream parser plugins that did
read the body.
So on both request and response paths, buildInput already has access to the parsed protocol extensions. On the response path specifically:
- pctx.Extensions.MCP with mcp.result (gated by include)
- pctx.Extensions.A2A with content (gated by include)
- pctx.Extensions.Inference with completion (gated by include)
The response path calls buildInput(pctx, p.inc) at line 321 — so it already gets all the parsed protocol data from upstream parsers, gated by the same include mechanism. Then it adds response.status_code and response.headers on top.
The code already provides response body content through the parsed extensions — the same include keys (mcp.result, a2a.content, inference.completion) work on both request and response paths. The raw HTTP body isn't exposed (and shouldn't be — it would be unparsed bytes), but the
meaningful structured content is available through the parser extensions.
Readme updated
huang195
left a comment
There was a problem hiding this comment.
Re-review — all three blocking items from my previous review are resolved:
- Startup fail-open (#1):
OnRequestnow gates on!p.ready.Load()and denies 503 until the first bundle activates. The fail-open window is closed. - No-bundle vs no-rule (#2): covered by the same ready-gate — requests can't reach the
IsUndefinedErr→skip branch before a policy is active. - Token in policy input (#3):
flattenHeadersnow redactsauthorization,proxy-authorization,cookie, andset-cookie.
The response path (OnResponse) is now symmetric with the request path (denies 503 when not ready), and the README decision-path table no longer claims response-body inspection. Thanks for the detailed walk-through of the readiness-probe flow.
Accepting the TLS/bundle-auth deferral (#5) as out of scope for this first functional PR, per your note that mTLS/SPIFFE lands later — worth a one-line "no transport security yet" caveat in the README's trust section when convenient, but non-blocking.
Areas reviewed: plugin logic + fail-mode semantics, header redaction, registration across the 3 binaries, READMEs. CI: all green. Approving.
Assisted-By: Claude Code
Summary
This is an OPA Plugin that is working against an OPA bundle service using the standard OPA SDK
Related issue(s)
rossoctl/rossoctl#1789
#fixes #544
opa
OPA (Open Policy Agent) plugin for AuthBridge. Downloads policy bundles from a
Kagenti Bundle Server based on the agent's identity and evaluates requests and
responses against the loaded policy using four fixed decision paths.
How it works
/shared/client-id.txt(mounted by the kagenti-operator from a Keycloak-credentials Secret).
fetch
bundles/<agent-id>.tar.gzfrom the bundle server.polling for updates (respecting
ETag/If-None-Matchfor lightweight304 responses).
path based on traffic direction and phase. If the policy denies, the plugin
returns HTTP 403. If the path is undefined (rule not present in the bundle),
the plugin skips evaluation — treating the absence of a rule as "no opinion".
The plugin reports not-ready (
Ready() == false) until the first bundle issuccessfully loaded. The Kubernetes readiness probe holds traffic off the pod
until then, so requests never arrive before the policy is active.
Decision paths
The plugin uses four fixed OPA decision paths, one for each evaluation point:
authbridge/inbound/requestauthbridge/inbound/responseauthbridge/outbound/requestauthbridge/outbound/responseEach path is independent. A bundle only needs to include the rules it cares
about — undefined paths are skipped (treated as allow). Most deployments will
only define
authbridge/inbound/request.Configuration
Default configuration
Most deployments only need
bundle_url. The lean default input is sufficientfor tool-access-control and model-restriction policies:
With this config, policies can decide based on caller identity, tool names,
model names, hosts, and methods — without any bulk content crossing the OPA
evaluation boundary.
Example: content-filtering policy
If a policy needs to inspect the actual user prompt (e.g., block tasks
containing sensitive keywords), extend the input with
a2a.content:This adds
a2a.parts[].content,a2a.artifact, anda2a.error_messagetothe OPA input so the policy can match against user-submitted text.
Example: full MCP arguments + LLM conversation audit
A deployment that needs to write policies against tool argument values and
the full conversation history:
Config fields
bundle_urlagent_id_file/shared/client-id.txtagent_idagent_id_fileis ignoredpolling_min_delay10polling_max_delay120include[]The
includemechanismThe OPA input document is lean by default — it contains only structural
metadata needed for authorization decisions (method, path, host, identity, tool
names, model names). Bulk content fields (conversation history, full tool
arguments, prompt text) are excluded unless explicitly requested via the
includeconfig list.Each entry in
includeis a named group key that unlocks additional fields:mcp.params.nameparams.namein MCP inputmcp.params.uriparams.uriin MCP inputmcp.params.<key>mcp.params.cursor)mcp.paramsparamsmap (all keys)mcp.resultresultmap (response path)mcp.errorerrorobject (code + message + data)a2a.contentparts[].content,artifact,error_messageinference.messagesmessages[]arrayinference.completioncompletiontextinference.tools.detaildescription+parametersinference.tool_callstool_calls[]withargumentsDefault-on keys (
mcp.params.name,mcp.params.uri) are always included evenwith an empty
includelist.OPA input document
Default (lean) input
{ "direction": "inbound", "method": "POST", "path": "/api/v1/invoke", "host": "my-agent", "headers": { "authorization": "Bearer eyJ...", "content-type": "application/json" }, "identity": { "subject": "user-123", "client_id": "caller-agent", "scopes": ["openid", "profile"] }, "agent": { "client_id": "my-agent" }, "a2a": { "method": "invoke", "session_id": "sess-123", "task_id": "task-789", "role": "user" }, "mcp": { "method": "tools/call", "params": { "name": "create_issue", "uri": "file:///workspace/main.go" } }, "inference": { "model": "gpt-4", "stream": false, "max_tokens": 4000, "tools": ["create_issue", "list_issues"] } }Notes:
mcp.paramscontains only the default-on keys (name,uri) that arepresent in the original params. Add more via
include.inference.toolsis a string array of tool names only (not full objects).Use
inference.tools.detailinincludefor descriptions and parameters.With
include: ["a2a.content", "mcp.params", "inference.messages", "inference.tools.detail"]{ "a2a": { "method": "invoke", "session_id": "sess-123", "task_id": "task-789", "role": "user", "parts": [ { "kind": "text", "content": "Create a GitHub issue for bug XYZ" } ], "artifact": "Issue #42 created", "error_message": "" }, "mcp": { "method": "tools/call", "params": { "name": "create_issue", "arguments": {"title": "Bug XYZ", "body": "..."} } }, "inference": { "model": "gpt-4", "stream": false, "max_tokens": 4000, "messages": [ {"role": "user", "content": "Help me create an issue"} ], "tools": [ {"name": "create_issue", "description": "Creates a GitHub issue", "parameters": {"type": "object"}} ] } }On the response path the document also includes:
{ "response": { "status_code": 200, "headers": { "content-type": "application/json" } } }Input field reference
direction"inbound"or"outbound"methodGET,POST, ...)pathhostHostheader valueheaders,)identityagenta2amcpinferenceresponsePolicy contract
Each decision path evaluates to an
allowrule. The plugin supports tworeturn shapes:
Boolean -- the simplest form:
Object with reason -- for detailed deny messages:
Example policies
Tool access control (MCP) — inbound request
LLM model restriction (Inference) — outbound request
Dangerous tool combination block (Inference) — outbound request
Task content filtering (A2A) — requires
include: ["a2a.content"]Multi-path bundle example
A single bundle can contain rules for multiple decision paths:
The plugin interprets the decision as follows:
truefalse{"allow": true}{"allow": false}{"allow": false, "reason": "..."}Bundle layout
The bundle server must serve a standard OPA bundle at the path
bundles/<agent-id>.tar.gz. A minimal bundle contains a single.regofilefor the inbound request path:
A full bundle covering all four decision paths:
Only include the paths you need — the plugin skips evaluation for any
undefined path.
See the OPA bundle documentation
for the full specification.
Pipeline ordering
The plugin declares
After: ["jwt-validation", "a2a-parser", "mcp-parser", "inference-parser"](soft ordering). When these plugins are present in the same pipeline, OPA runs
after them so
input.identity,input.a2a,input.mcp, andinput.inferenceare populated. If any are absent, OPA still runs — the corresponding input
fields will be missing and the policy must handle that case.
Deny behavior
request arrives anyway (e.g. in tests), the plugin denies with 503.
Session events
The plugin records
Invocationentries for every decision:allowpolicy_allowed/response_policy_alloweddenypolicy_denied/response_policy_denieddenydecision_error/response_decision_errorskipopa_not_readyskipno_policy_ruleThese appear in the session events API (
:9094) and inabctl.Summary by CodeRabbit
New Features
Documentation
Tests
Chores