Skip to content

refactor(plugins): Push protocol classification down to parsers#452

Merged
huang195 merged 9 commits into
kagenti:mainfrom
huang195:fix/parser-classification
Jun 1, 2026
Merged

refactor(plugins): Push protocol classification down to parsers#452
huang195 merged 9 commits into
kagenti:mainfrom
huang195:fix/parser-classification

Conversation

@huang195
Copy link
Copy Markdown
Contributor

@huang195 huang195 commented May 30, 2026

Summary

Move protocol-classification logic out of guardrail plugins (IBAC today) and into the parsers that own each protocol. Adds a single IsAction bool field on each protocol extension and a pctx.Classification() helper that aggregates the verdict across all populated extensions. IBAC's three protocol-specific bypass steps collapse to a single classification gate.

Why

Before: IBAC encoded MCP-protocol vocabulary inline (isMCPHousekeeping listing every housekeeping method, isTransportRetrieval for body-less GET/HEAD/OPTIONS detection, the body-less DELETE+Mcp-Session-Id check from PR #451). Every new guardrail (rate limiter, audit logger, secret scanner, …) and every new protocol (A2A subroutes, OTEL, gRPC) would multiply this work.

After: each parser owns its protocol's "is this an action vs protocol mechanics" answer; guardrails read one verdict and decide. Adding a guardrail is now zero protocol-specific work; adding a protocol's classification rules touches only that parser.

What changes

Five logical commits, each independently reviewable:

  1. feat(pipeline): Add IsAction classification on protocol extensions — the wire. Adds IsAction bool to MCPExtension, A2AExtension, InferenceExtension. Adds pctx.Classification() (anyAction, anyBypass bool) aggregating across populated extensions. No behavior change yet — every parser still leaves IsAction at the zero value.

  2. feat(mcp-parser): Classify methods + recognize transport via path config — the largest change. mcp-parser now:

    • Sets IsAction = true for tools/call, prompts/get, resources/read; everything else stays at default false.
    • Gains a paths config field (default ["/mcp"]) for body-less transport detection.
    • On body-less DELETE + Mcp-Session-Id on a configured path, populates a synthetic MCPExtension{Method: "$transport/terminate"} (replaces the logic from PR fix(ibac): Bypass MCP session-terminate DELETE as transport_stream #451).
    • On body-less GET on a configured path, populates a synthetic MCPExtension{Method: "$transport/stream"} (replaces the body-less GET case from PR fix(ibac): Bypass transport-layer calls and let agents take self-actions #450).
    • The $ prefix on synthetic methods is reserved (real MCP methods follow category/action with no $), so operators can tell at a glance these aren't methods that appeared on the wire.
  3. feat(a2a-parser): Classify outbound A2A methods — sets IsAction = true for message/send and message/stream; protocol/discovery methods inherit default false.

  4. feat(inference-parser): Mark all populated requests as actions — every populated InferenceExtension is an outbound LLM call; sets IsAction = true unconditionally. The "don't judge inference by default" decision lives separately in IBAC's judge_inference operator-config flag — classification (parser concern) and operator policy (guardrail concern) stay cleanly separated.

  5. refactor(ibac): Replace inline protocol logic with classification gate — the simplification. Removes:

    • isMCPHousekeeping (now an mcp-parser concern; PR fix(ibac): Bypass MCP session-terminate DELETE as transport_stream #451's resources/subscribe/resources/unsubscribe additions follow).
    • isTransportRetrieval and the body-less DELETE+header check (now mcp-parser's synthetic transport extensions).
    • Steps 5 (mcp_housekeeping) and 5b (transport_stream) from OnRequest.

    Adds the new step 4: classification gate driven by pctx.Classification(). Updates Capabilities() to declare RequiresAny: ["mcp-parser", "a2a-parser", "inference-parser"] (pipeline boot-fails if no parser is in the chain — without one, IBAC has no classification to read and would silently no-op on every request).

End-state guardrail logic

isAction, isBypass := pctx.Classification()
if isBypass {
    pctx.Skip("protocol_mechanics")
    return Continue
}
if !isAction {
    return Continue  // defense in depth: not our traffic, pass through
}
// classified action — proceed to operator-config bypasses + intent + judge

That's it. No protocol-specific code in IBAC. Same five lines work for any future guardrail.

Test plan

  • go test ./... in authbridge/authlib — all 29 packages pass.
  • Per-parser classification coverage:
    • mcp-parser: per-method table covering 14 methods (3 actions + 11 housekeeping); body-less DELETE+header → $transport/terminate; body-less GET → $transport/stream; non-configured-path / no-header / body-less-POST cases all decline to claim; paths config defaults + custom + bad-glob rejection.
    • a2a-parser: classification table for message/send, message/stream, plus a representative non-action.
    • inference-parser: assert IsAction = true unconditionally.
    • pipeline: Classification() on empty pctx, single-extension action, single-extension bypass, multi-extension agreement, multi-extension mixed.
  • IBAC: TestOnRequest_ClassificationBypass_ProtocolMechanics, TestOnRequest_NoClassification_PassesThrough, TestOnRequest_MCPActionReachesJudge, TestOnRequest_DescribeActionHasNoLeadingWhitespace (regression-locks the listener pctx.Method plumbing from PR fix(ibac): Bypass transport-layer calls and let agents take self-actions #450). Existing tests updated to populate IsAction explicitly where they expect to reach the judge.
  • Live verification on the exgentic-a2a-tool-calling-gsm8k agent (procedure same as PR fix(ibac): Bypass transport-layer calls and let agents take self-actions #450/fix(ibac): Bypass MCP session-terminate DELETE as transport_stream #451):
    • Rebuild + push; load into Kind; restart pod; rerun ./deploy-and-evaluate.sh --benchmark gsm8k --agent tool_calling --plugin-preset ibac-only.
    • Confirm in :9094/v1/sessions/<id> that MCP setup methods record skip/protocol_mechanics, tools/call is judged, body-less GET on /mcp records skip/protocol_mechanics (synthetic $transport/stream), DELETE on /mcp with Mcp-Session-Id records skip/protocol_mechanics (synthetic $transport/terminate).
    • Compare gsm8k accuracy against pre-PR baseline — should be identical.
  • Negative test: deploy a pipeline with IBAC but no parsers; expect boot failure with a RequiresAny error.

Compatibility / breaking changes

  • RequiresAny on IBAC. Any existing IBAC-only pipeline (no parsers in the same outbound chain) will fail to boot after this change. Such pipelines were silently no-op anyway — IBAC had nothing to read — so the breakage catches misconfig rather than introducing it.
  • No config changes for typical deployments. mcp-parser's paths config defaults to ["/mcp"], which matches the standard MCP Streamable HTTP setup used by all in-tree demos (weather-agent, github-issue, ibac). Operators with non-default endpoint paths add explicit entries.
  • Wire format additive. Each protocol extension gains one JSON field (isAction); abctl renders unknown JSON opaquely so existing UI keeps working.
  • #451 (still open) becomes redundant if this PR lands first. The MCP session-terminate logic moves from IBAC into mcp-parser as the synthetic $transport/terminate extension.

Out of scope (deferred)

  • Lifting transport-shape detection into a shared helper or pseudo-plugin. Not needed: parsers own all classification.
  • Spec-evolution lint or audit tooling for the action-method lists in each parser. Worth doing as a follow-up given default-false on IsAction means a missing entry produces silent under-judging.
  • abctl visual treatment of the $-prefixed synthetic methods (a "synthetic transport" badge or different color). Functional today as opaque literal string.
  • Adding classification to SecurityExtension / DelegationExtension / Custom. Those aren't protocol parsers; classification doesn't apply.

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • Protocol parsers now classify extensions as user "action" vs "protocol mechanics"; classifications are aggregated for guardrail gating
    • MCP parser: configurable endpoint paths, body-less transport detection, and action classification; A2A and Inference parsers surface action verdicts
    • IBAC: classification gate, configurable unclassified/no-intent policies, and boot-time requirement for at least one parser
  • Tests

    • Added unit tests for classification aggregation and parser classification paths
  • Documentation

    • New plugin-author classification contract and updated architecture/IBAC docs
  • Chores

    • Demo config and build tweaks; demo agent now mints missing context IDs automatically

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Parsers add an IsAction boolean to MCP/A2A/Inference extensions. Context.Classification() aggregates those flags into anyAction and anyBypass. MCP/A2A/Inference set IsAction per method heuristics; IBAC uses the aggregation to skip protocol mechanics, pass through unclassified traffic, or judge actions.

Changes

Request classification contract and integration

Layer / File(s) Summary
Extension classification contract and documentation
authbridge/authlib/pipeline/extensions.go, authbridge/docs/framework-architecture.md, authbridge/docs/plugin-reference.md, authbridge/CLAUDE.md
Adds IsAction to MCPExtension, A2AExtension, InferenceExtension with contract docs; plugin-reference and framework docs describe guardrail consumption via pctx.Classification().
Classification aggregation method and tests
authbridge/authlib/pipeline/context.go, authbridge/authlib/pipeline/extensions_test.go
Adds Context.Classification() returning anyAction, anyBypass by inspecting populated extensions; tests cover no-extension, action, bypass, and mixed cases.
A2A and Inference parser classification
authbridge/authlib/plugins/a2aparser/plugin.go, authbridge/authlib/plugins/a2aparser/plugin_test.go, authbridge/authlib/plugins/inferenceparser/plugin.go, authbridge/authlib/plugins/inferenceparser/plugin_test.go
A2A parser tags message/send and message/stream as actions via isA2AAction(); Inference parser marks populated inference extensions as actions; tests validate both.
MCP parser configuration and method classification
authbridge/authlib/plugins/mcpparser/plugin.go
Adds Configure() with path globbing and defaults, body-less transport detection (synthetic $transport/stream/$transport/terminate) scoped to configured paths, and isMCPAction() to classify JSON-RPC methods (tools/call, prompts/get, resources/read) with request logging of isAction.
MCP parser test coverage: configuration, classification, and transport detection
authbridge/authlib/plugins/mcpparser/plugin_test.go
Extensive tests for default/custom paths, invalid config rejection, action-method classification table, body-less transport detection cases, and helper for applying JSON config in tests.
IBAC integration: classification-driven gating and pipeline enforcement
authbridge/authlib/plugins/ibac/plugin.go, authbridge/authlib/plugins/ibac/plugin_test.go, authbridge/docs/ibac-plugin.md
IBAC Capabilities() now RequiresAny of MCP/Inference parsers; OnRequest calls pctx.Classification() to record protocol_mechanics skips for bypasses, apply unclassified_policy, pass through unclassified traffic by default, and send action-classified traffic to the judge. Tests and docs updated to reflect the new flow and no_intent_policy/unclassified_policy knobs.

Demos and tooling

Layer / File(s) Summary
IBAC demo config patch
authbridge/demos/ibac/k8s/ibac-patch.yaml
Sets unclassified_policy: "judge" in the demo outbound ibac config.
Demo Makefile freshness build
authbridge/demos/ibac/Makefile
build-abctl target now rebuilds when source files are newer and runs the build from the abctl source directory.
A2A demo: session context-id minting
authbridge/demos/ibac/agent/main.go
Handler now mints a UUID when params.message.contextId is omitted.

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant IBAC
  participant Classification
  participant Judge
  participant Skip
  Request->>IBAC: OnRequest
  IBAC->>Classification: pctx.Classification()
  alt anyBypass == true
    Classification->>Skip: protocol_mechanics skip
  else anyAction == false
    Classification->>IBAC: pass through (no skip)
  else anyAction == true
    IBAC->>Judge: proceed to judge/evaluate
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • pdettori
  • cwiklik

Poem

🐰 I hopped through logs and parsed the play,
Flagging true actions along the way.
IsAction raised, the judge can see,
Skips the noise, lets real acts be.
Hooray — a rabbit watched the policy!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.05% 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 'refactor(plugins): Push protocol classification down to parsers' is clear and directly summarizes the main architectural change—moving classification logic from guardrails into protocol parsers.
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

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
authbridge/docs/ibac-plugin.md (1)

29-36: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Threat-model walkthrough now contradicts the new classification gate.

This section (and the Limitations note at Lines 330-333) states that the plain-HTTP POST evil-server/collect — explicitly "plain HTTP, not MCP, not inference traffic" — is judged and denied with 403 ibac.blocked when IBAC is enabled. But the new step-4 classification gate you added above passes unclassified traffic through silently: a request with no parser-populated extension yields !anyAction and Continue without judging. As written, the headline example can no longer be caught by IBAC, so the doc is internally inconsistent.

Update the Threat Model and Limitations to reflect that IBAC now only fires on parser-classified traffic, or clarify how plain-HTTP exfiltration is expected to be classified.

🤖 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/docs/ibac-plugin.md` around lines 29 - 36, The threat-model doc is
inconsistent with the new classification gate (!anyAction => Continue) so update
the Threat Model and Limitations to state that IBAC (checked in OnRequest) only
evaluates requests that parsers populate as actions; explicitly mention that
plain-HTTP requests without parser enrichment will pass through (not be judged)
unless a parser or MCP enrichment classifies them, and adjust the example that
previously showed DenyStatus(403, "ibac.blocked") for a plain POST to either
show it being classified first (e.g., via parser/MCP enrichment) or change the
outcome to Continue; ensure references to pctx.Session.LastIntent() and the
judge LLM pathway remain accurate when describing the classified path.
authbridge/authlib/plugins/ibac/plugin_test.go (1)

290-304: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This test no longer exercises the inference_bypass path it names.

makePCtx injects a default MCP{IsAction:true}, and this test leaves the new Inference extension at IsAction:false. In Classification() that yields anyAction=true (MCP) and anyBypass=true (Inference), so the request short-circuits at the step-4 protocol_mechanics gate and never reaches the step-5 inference-bypass branch. The assertions (Continue, calls==0) pass either way, masking the gap.

Mirror what inference-parser actually does (IsAction:true) so the request passes the classification gate, and assert the skip reason.

💚 Proposed fix
 	pctx := makePCtx(t)
-	pctx.Extensions.Inference = &pipeline.InferenceExtension{Model: "gpt-4o-mini"}
+	// inference-parser sets IsAction=true on populated extensions; keep
+	// anyBypass=false so we reach the step-5 inference operator-policy gate.
+	pctx.Extensions.MCP = nil
+	pctx.Extensions.Inference = &pipeline.InferenceExtension{Model: "gpt-4o-mini", IsAction: true}
 	action := invokeOnRequest(p, pctx)

 	if action.Type != pipeline.Continue {
 		t.Errorf("got %v, want Continue (inference bypassed by default)", action.Type)
 	}
 	if fj.calls != 0 {
 		t.Errorf("judge invoked on inference; want 0")
 	}
+	inv := lastInvocation(t, pctx)
+	if inv.Reason != "inference_bypass" {
+		t.Errorf("Invocation reason = %q, want 'inference_bypass'", inv.Reason)
+	}
🤖 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/ibac/plugin_test.go` around lines 290 - 304, The
test TestOnRequest_InferenceBypassByDefault currently leaves the Inference
extension IsAction=false so Classification() treats MCP.IsAction=true and
Inference.IsAction=false as anyBypass=true and short-circuits before the
inference-bypass branch; update the test to mirror the inference-parser by
setting pctx.Extensions.Inference.IsAction = true (or construct the PCtx so the
Inference extension has IsAction:true) so the request passes the classification
gate and reaches the inference-bypass logic in OnRequest; then add an assertion
that the returned action includes the expected skip reason (verifying the
inference-bypass path was exercised) and keep the existing checks for
action.Type and fj.calls.
🤖 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.

Outside diff comments:
In `@authbridge/authlib/plugins/ibac/plugin_test.go`:
- Around line 290-304: The test TestOnRequest_InferenceBypassByDefault currently
leaves the Inference extension IsAction=false so Classification() treats
MCP.IsAction=true and Inference.IsAction=false as anyBypass=true and
short-circuits before the inference-bypass branch; update the test to mirror the
inference-parser by setting pctx.Extensions.Inference.IsAction = true (or
construct the PCtx so the Inference extension has IsAction:true) so the request
passes the classification gate and reaches the inference-bypass logic in
OnRequest; then add an assertion that the returned action includes the expected
skip reason (verifying the inference-bypass path was exercised) and keep the
existing checks for action.Type and fj.calls.

In `@authbridge/docs/ibac-plugin.md`:
- Around line 29-36: The threat-model doc is inconsistent with the new
classification gate (!anyAction => Continue) so update the Threat Model and
Limitations to state that IBAC (checked in OnRequest) only evaluates requests
that parsers populate as actions; explicitly mention that plain-HTTP requests
without parser enrichment will pass through (not be judged) unless a parser or
MCP enrichment classifies them, and adjust the example that previously showed
DenyStatus(403, "ibac.blocked") for a plain POST to either show it being
classified first (e.g., via parser/MCP enrichment) or change the outcome to
Continue; ensure references to pctx.Session.LastIntent() and the judge LLM
pathway remain accurate when describing the classified path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b3aa6b65-30cc-4d8c-b86c-b3161f695d78

📥 Commits

Reviewing files that changed from the base of the PR and between b9ee2ed and 5a3c012.

📒 Files selected for processing (15)
  • authbridge/CLAUDE.md
  • authbridge/authlib/pipeline/context.go
  • authbridge/authlib/pipeline/extensions.go
  • authbridge/authlib/pipeline/extensions_test.go
  • authbridge/authlib/plugins/a2aparser/plugin.go
  • authbridge/authlib/plugins/a2aparser/plugin_test.go
  • authbridge/authlib/plugins/ibac/plugin.go
  • authbridge/authlib/plugins/ibac/plugin_test.go
  • authbridge/authlib/plugins/inferenceparser/plugin.go
  • authbridge/authlib/plugins/inferenceparser/plugin_test.go
  • authbridge/authlib/plugins/mcpparser/plugin.go
  • authbridge/authlib/plugins/mcpparser/plugin_test.go
  • authbridge/docs/framework-architecture.md
  • authbridge/docs/ibac-plugin.md
  • authbridge/docs/plugin-reference.md

huang195 added a commit to huang195/kagenti-extensions that referenced this pull request May 30, 2026
Four review-feedback follow-ups, all documentation only — no
behavior changes.

1. mcpparser/plugin.go isMCPAction: add an MCP spec-revision
   anchor (2025-03-26 Streamable HTTP) so future maintainers
   adding a new action method have a date stamp to compare
   against rather than re-deriving the action set.

2. mcpparser/plugin.go synthetic methods: soften the "$ prefix
   is reserved" framing. JSON-RPC 2.0 §6 only reserves "rpc.*";
   "$" is our own design choice motivated by visual distinguish-
   ability, not by spec reservation. Note the fallback escape
   hatch (switch to "_transport/*") if MCP ever starts using "$".

3. pipeline/context.go Classification doc: surface the parser-
   disjointness assumption that makes the multi-extension-mixed
   case rare, and explicitly note that callers (defense-in-depth
   gates vs audit-style guardrails) may want different
   precedence between anyAction and anyBypass when both are true.

4. ibac/plugin_test.go makePCtx: add a CONTRACT CHANGE NOTE
   documenting the opt-in → opt-out flip introduced by the
   parser-classification refactor (was: tests opt in by setting
   Extensions.MCP; now: tests opt out by setting it nil) so a
   future test author doesn't get confused about why their "no
   extension" test case is judging.

Pure comment churn; tests and behavior unchanged.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Copy link
Copy Markdown
Contributor

@mrsabath mrsabath left a comment

Choose a reason for hiding this comment

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

Strong refactor — pushing classification into parsers and collapsing IBAC's three protocol-specific gates into one verdict is the right shape, and the pctx.Classification() contract is clean. CI green, DCO clean across 6 commits, and the synthetic $transport/* extension idiom for body-less MCP retrievals reads well.

Must-fix (both unaddressed CodeRabbit findings)

1. authbridge/docs/ibac-plugin.md — threat model contradicts the new gate (out-of-diff, lines ~23-39 and ~320-340).

The headline IBAC scenario describes a plain-HTTP POST evil-server/collect ("plain HTTP, not MCP, not inference traffic") being denied with 403 ibac.blocked. With the new step-4 classification gate, that exact request has no parser-populated extension, so pctx.Classification() returns (false, false), the !anyAction branch fires, and IBAC Continues silently with no Skip recorded.

The operational-flow update at lines 107-145 is correct, but it didn't propagate up to the threat model walkthrough or the Limitations note. Security-relevant doc accuracy issue — readers will assume IBAC catches plain-HTTP exfiltration when the gate now lets it through.

Fix: either rewrite the headline example to use MCP-shaped exfil (mcp-parser populates an MCPExtension{IsAction:true}, IBAC actually fires) or explicitly call out plain-HTTP exfil as out-of-scope under the new gate, with a pointer to the runtime classification semantics.

2. authbridge/authlib/plugins/ibac/plugin_test.goTestOnRequest_InferenceBypassByDefault (out-of-diff, line ~299).

makePCtx (modified in this PR) sets Extensions.MCP = {Method: "tools/call", IsAction: true} by default. The test adds Inference{Model: "gpt-4o-mini"} which leaves IsAction: false. Under the new Classification():

  • MCP populated with IsAction:trueanyAction=true
  • Inference populated with IsAction:falseanyBypass=true

Result: step-4 short-circuits with skip/protocol_mechanics. The step-5 inference-bypass branch is never reached. Assertions Continue + calls==0 still pass for the wrong reason. A regression in the inference-bypass logic would not be caught.

The 944838d commit fixed TestOnRequest_InferenceJudgedWhenEnabled (line ~322) but missed this sibling. Fix:

pctx.Extensions.Inference = &pipeline.InferenceExtension{Model: "gpt-4o-mini", IsAction: true}
// existing assertions ...
inv := lastInvocation(t, pctx)
if inv.Reason != "inference_bypass" {
    t.Errorf("Invocation reason = %q, want %q", inv.Reason, "inference_bypass")
}

Suggestion

Default fail-open on missing intent is a security-posture change not in the breaking-changes list.

The PR body's "Compatibility / breaking changes" section calls out RequiresAny and the additive wire-format fields, but no_intent_policy: "allow" as the new default flips the prior hardcoded 403 ibac.no_intent (fail closed) to skip/no_user_context + Continue (fail open). Operators upgrading without setting no_intent_policy: "deny" will see previously-blocked traffic now passing through. Worth a row in the breaking-changes list and a callout in ibac-plugin.md Limitations.

Inline comment below covers a smaller RequiresAny modeling concern.

Areas reviewed: Go pipeline framework (Classification, RequiresAny), plugin classification contract, IBAC refactor, mcp-parser synthetic transport extensions, threat-model doc, plugin reference doc, demo pipeline composition (demos/ibac/k8s/ibac-patch.yaml), prior CodeRabbit feedback follow-through.
Commits: 6, all signed off ✓
CI status: All 19 checks passing.

// the parser-driven design). Boot-fail if no parser is present
// rather than ship a misconfigured pipeline.
RequiresAny: []string{"mcp-parser", "a2a-parser", "inference-parser"},
ReadsBody: true,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: RequiresAny validation is same-chain (per pipeline/plugin.go:65-76). IBAC runs in the outbound chain, but a2a-parser runs in the inbound chain in every in-tree config — see demos/ibac/k8s/ibac-patch.yaml:

inbound_prepend:
  - name: a2a-parser   # <-- inbound
outbound_append:
  - name: inference-parser
  - name: mcp-parser
  - name: ibac          # <-- outbound

Including a2a-parser in IBAC's RequiresAny means an outbound chain [a2a-parser, ibac] would pass the validator, but a2a-parser running outbound populates nothing IBAC reads (no Session.Intents to feed step 6). The realistic outbound parsers are mcp-parser and inference-parser.

Two options: (a) drop a2a-parser from RequiresAny (more accurate — only the parsers that actually populate extensions in the outbound chain count), or (b) keep it but document in the doc-comment block above that this entry exists only to satisfy the boot-time validator — the functional a2a-parser dependency comes from the inbound chain and remains a runtime check governed by no_intent_policy.

huang195 added a commit to huang195/kagenti-extensions that referenced this pull request May 31, 2026
… posture docs

Four review-feedback follow-ups in one commit, scoped to IBAC plus
its demo manifest and reference docs. Build + test pass.

1. Must-fix kagenti#1 — TestOnRequest_InferenceBypassByDefault assertion
   was passing for the wrong reason. With makePCtx populating
   MCPExtension{IsAction:true} by default, the test's
   InferenceExtension{IsAction:false} (zero value) caused
   step 4 (classification gate) to skip with protocol_mechanics
   before step 5 (inference-policy) could run. A regression in the
   inference-policy logic would have gone uncaught.

   Fix: set IsAction:true on the Inference extension (matches what
   inference-parser does in production). Add an explicit assertion
   on inv.Reason == "inference_bypass" so the test verifies the
   right step fired. Same fix shape as the earlier 944838d
   correction to TestOnRequest_InferenceJudgedWhenEnabled, which
   missed this sibling.

2. Must-fix kagenti#2 — IBAC demo's plain-HTTP exfiltration scenario
   (the headline ibac-plugin.md threat model) silently passed
   through the new defense-in-depth gate, since no parser claims
   plain HTTP and step 4 short-circuits with !anyAction.

   Fix: introduce unclassified_policy as a config field on the
   ibac plugin, parallel in shape to the existing no_intent_policy.

   - Default "passthrough": current behavior. Defense in depth —
     IBAC only judges what a parser classified.
   - "judge": falls through to inference policy + intent + judge
     even when no parser claimed the request. Catches plain-HTTP
     outbound at the cost of one judge round-trip per unclassified
     request.

   Demo's k8s/ibac-patch.yaml sets unclassified_policy: "judge"
   so the headline scenario keeps working. Production deployments
   using MCP/A2A/inference exclusively get cleaner defense-in-depth
   behavior without changing config; deployments that want plain-
   HTTP egress coverage opt in.

3. Inline suggestion — RequiresAny: ["mcp-parser", "a2a-parser",
   "inference-parser"] was misleading. RequiresAny is a same-chain
   check; IBAC runs OUTBOUND, but a2a-parser runs INBOUND in every
   in-tree config. Including it would let an outbound chain
   [a2a-parser, ibac] pass validation while a2a-parser populates
   nothing IBAC reads.

   Fix: drop a2a-parser. RequiresAny: ["mcp-parser", "inference-parser"].
   Doc-comment in Capabilities() explains the cross-chain a2a
   dependency stays runtime, governed by no_intent_policy.

4. Suggestion — operator-facing default-posture documentation.
   docs/ibac-plugin.md gains a "Default security posture"
   subsection under Limitations covering both fail-open knobs
   (unclassified_policy, no_intent_policy) and how operators choose
   between them. Config-table rows added for both. Threat-coverage
   notes added: plain-HTTP exfil is covered when unclassified_policy:
   "judge"; MCP-shaped exfil is covered by default. The PR's
   compatibility section is intentionally NOT amended for
   no_intent_policy — that flag landed in kagenti#450 (already in main),
   not kagenti#452, and the durable home for the operator guidance is the
   plugin's reference doc.

Tests:
- TestOnRequest_InferenceBypassByDefault now asserts inv.Reason.
- New TestOnRequest_UnclassifiedPolicy_Judge covers the policy=judge
  branch (unclassified body-having POST → reaches judge → reject).
- New TestConfigure_UnclassifiedPolicy_DefaultsToPassthrough.
- New TestConfigure_UnclassifiedPolicy_RejectsUnknownValue.
- New TestConfigure_UnclassifiedPolicy_AcceptsExplicitValues
  (parameterized over {passthrough, judge}).
- Existing tests pass unchanged — default behavior preserved.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@authbridge/docs/ibac-plugin.md`:
- Around line 188-190: The Request Flow step 4 description is out of sync with
the new `unclassified_policy` option: update the flow text so that when
`!anyAction` you conditionally branch on `unclassified_policy` (e.g., if
`unclassified_policy == "passthrough"` then Continue silently, if `"judge"` then
fall through to the judge) instead of always continuing; reference the
`unclassified_policy` setting and the `!anyAction` check in the step text and
briefly describe both branching outcomes so operators see the runtime behavior.
🪄 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: 5365839d-8eae-4236-bc67-597e23cfc956

📥 Commits

Reviewing files that changed from the base of the PR and between 944838d and 2c35546.

📒 Files selected for processing (4)
  • authbridge/authlib/plugins/ibac/plugin.go
  • authbridge/authlib/plugins/ibac/plugin_test.go
  • authbridge/demos/ibac/k8s/ibac-patch.yaml
  • authbridge/docs/ibac-plugin.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • authbridge/authlib/plugins/ibac/plugin_test.go

Comment thread authbridge/docs/ibac-plugin.md
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
authbridge/demos/ibac/Makefile (1)

119-121: ⚡ Quick win

Include module files in staleness detection for abctl rebuilds.

On Line 119, rebuild freshness only checks *.go under ../../cmd/abctl. If go.mod/go.sum changes, the binary can be stale but still be treated as up-to-date.

Suggested patch
 build-abctl:  ## Build abctl into /tmp (rebuilds when source is newer; used by show-result)
 	`@ABCTL_SRC_DIR`=../../cmd/abctl; \
+	ABCTL_MOD_DIR=../..; \
 	if [ -x /tmp/abctl-ibac-demo ]; then \
 	  NEWER=$$(find $$ABCTL_SRC_DIR -name '*.go' -newer /tmp/abctl-ibac-demo -print -quit 2>/dev/null); \
-	  if [ -z "$$NEWER" ]; then \
+	  if [ -z "$$NEWER" ] && [ ! "$$ABCTL_MOD_DIR/go.mod" -nt /tmp/abctl-ibac-demo ] && [ ! "$$ABCTL_MOD_DIR/go.sum" -nt /tmp/abctl-ibac-demo ]; then \
 	    echo "[*] /tmp/abctl-ibac-demo is up to date with the abctl source; skipping. \`rm /tmp/abctl-ibac-demo\` to force rebuild."; \
 	    exit 0; \
 	  fi; \

Also applies to: 128-128

🤖 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/demos/ibac/Makefile` around lines 119 - 121, The staleness check
for rebuilding abctl only finds '*.go' files (variable NEWER using
$$ABCTL_SRC_DIR) so changes to go.mod or go.sum are ignored; update the find
invocation that computes NEWER (and the similar block later) to also consider
go.mod and go.sum (e.g., include -name 'go.mod' -o -name 'go.sum' or use -type f
with specific patterns) so the /tmp/abctl-ibac-demo freshness test correctly
detects module file changes and forces rebuilds when go.mod/go.sum change.
🤖 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.

Nitpick comments:
In `@authbridge/demos/ibac/Makefile`:
- Around line 119-121: The staleness check for rebuilding abctl only finds
'*.go' files (variable NEWER using $$ABCTL_SRC_DIR) so changes to go.mod or
go.sum are ignored; update the find invocation that computes NEWER (and the
similar block later) to also consider go.mod and go.sum (e.g., include -name
'go.mod' -o -name 'go.sum' or use -type f with specific patterns) so the
/tmp/abctl-ibac-demo freshness test correctly detects module file changes and
forces rebuilds when go.mod/go.sum change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c48cccca-1895-4577-a74d-aea227b5e6b4

📥 Commits

Reviewing files that changed from the base of the PR and between 2c35546 and c053423.

📒 Files selected for processing (1)
  • authbridge/demos/ibac/Makefile

huang195 added 9 commits June 1, 2026 07:58
Add an IsAction bool field to MCPExtension, A2AExtension, and
InferenceExtension. Add Context.Classification() that aggregates the
field across all populated extensions and returns (anyAction,
anyBypass). Both false means no parser populated anything — i.e.
the request is unclassified.

This is the wire for a forthcoming refactor that pushes protocol-
specific classification logic out of guardrail plugins (IBAC today)
and into the parsers that own each protocol. After this commit, no
behavior changes — every parser still leaves IsAction at the zero
value, every guardrail still uses its existing bypass logic.
Subsequent commits move the per-protocol classification down into
mcp-parser / a2a-parser / inference-parser, then flip IBAC to read
the aggregated verdict via pctx.Classification().

Default-false IsAction reflects defense-in-depth: when the parser
can't confidently classify a method as a user-meaningful action,
guardrails err toward letting traffic through rather than judging
it. Parsers MUST explicitly set IsAction=true on the small set of
known action methods (e.g. tools/call, message/send); everything
else inherits the zero value and is treated as protocol mechanics
or unclassified.

The two-boolean return distinguishes "explicitly classified as
bypass" (anyBypass=true) from "no parser claimed this request"
(both false). Callers can act on the distinction:

  - IBAC pattern: anyBypass → Skip+Continue; !anyAction → Continue
    (defense-in-depth pass-through); else fall through to judge.
  - A general gate would treat both states as "skip me, no judgment
    available."

Tests: Classification on empty pctx, single-extension action,
single-extension bypass, multi-extension agreement, multi-extension
mixed (both flags true). All in pipeline/extensions_test.go.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Three new behaviors, all owned by mcp-parser so guardrails don't
have to know about them:

1. Body-having JSON-RPC requests now carry a classification verdict
   in MCPExtension.IsAction. The action methods are tools/call,
   prompts/get, resources/read — small list, easy to keep auditable.
   Everything else (initialize, *list, subscribe, unsubscribe,
   notifications/*, completion/complete, logging/setLevel) inherits
   the zero value false, which guardrails treat as protocol
   mechanics to skip.

2. Body-less DELETE on a configured MCP path with the Mcp-Session-Id
   header is recognized as MCP Streamable HTTP session termination.
   The parser populates a synthetic MCPExtension{Method:
   "$transport/terminate", IsAction: false}. The Mcp-Session-Id
   header is set by the MCP client SDK (never user input), so it's
   the precise distinguisher from a real "DELETE /api/users/42"
   action call — body-less DELETE without the header doesn't get
   claimed.

3. Body-less GET on a configured MCP path is recognized as MCP
   Streamable HTTP server-to-client SSE channel-open. Synthetic
   MCPExtension{Method: "$transport/stream", IsAction: false}.

The "$" prefix on the synthetic methods is reserved — real MCP
methods follow a category/action naming convention with no "$" —
so operators reading abctl can tell at a glance these aren't
methods that appeared on the wire.

New mcp-parser config:

    # mcp-parser plugin config
    paths:
      - "/mcp"            # default; matches MCP Streamable HTTP setups

Path-shape detection is scoped to the configured paths list.
Without this scope, a body-less GET to any host would risk being
mis-classified as MCP transport. Default ["/mcp"] covers the
standard MCP Python SDK setup; operators with non-default endpoint
paths add explicit entries.

This commit changes mcp-parser's behavior but does NOT change any
guardrail's behavior — IBAC still uses its existing inline checks
on this branch. The IBAC migration to read Classification() lands
in a later commit; this commit is the prerequisite that gives the
classification something to read.

Tests:
- Default paths config matches /mcp.
- Configure rejects bad path globs and unknown fields.
- Per-method classification table: 14 methods (3 action + 11 non-
  action) including all the housekeeping methods previously listed
  in IBAC's isMCPHousekeeping.
- Body-less DELETE+Mcp-Session-Id on configured path → synthetic
  $transport/terminate.
- Body-less GET on configured path → synthetic $transport/stream.
- Body-less DELETE without Mcp-Session-Id on configured path → no
  extension (could be a real resource-delete call).
- Body-less request on non-configured path → no extension.
- Body-less POST on configured path → no extension (not a
  recognized MCP transport shape).
- Custom paths config scopes detection to operator-specified
  endpoints.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Set A2AExtension.IsAction = true for the user-meaningful agent-to-
agent methods message/send and message/stream; everything else
(agent-card discovery, future protocol/discovery methods) inherits
the zero-value false.

On the inbound side a2a-parser's classification doesn't drive
guardrail behavior today (IBAC is outbound-only) but the field is
set on inbound for consistency and for any future inbound
guardrail.

Tests assert IsAction is set correctly for both action methods plus
a representative non-action method.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Every populated InferenceExtension represents an outbound LLM call,
which is on the wire an action — the agent is reaching out to its
LLM. Set IsAction=true unconditionally when populating the extension.

The "don't judge inference by default" semantics live in IBAC's
judge_inference operator-config flag, not in the classification.
This commit keeps classification (parser concern) cleanly separate
from policy (guardrail concern): an audit logger could legitimately
record inference traffic even when IBAC bypasses it, because the
underlying request was an action regardless of how IBAC chooses to
treat it.

Test asserts the always-true behavior on a representative chat-
completions request.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
The previous IBAC encoded MCP-protocol knowledge inline:
isMCPHousekeeping (the housekeeping-method list), isTransportRetrieval
(GET/HEAD/OPTIONS detection for body-less requests), and the body-less
DELETE+Mcp-Session-Id check. As more guardrails (rate limiter, audit
logger, etc.) and more protocols (A2A subroutes, OTEL, gRPC) land,
each guardrail would have to re-derive the same protocol mechanics.

Replace all three protocol-specific bypass steps with a single
classification gate driven by pctx.Classification(). The parsers
(mcp-parser, a2a-parser, inference-parser) own each protocol's
bypass-vs-action vocabulary; IBAC just reads the aggregated verdict.

Behavior changes:

  - Step 5 (mcp_housekeeping bypass), step 5b (transport_stream
    bypass): both deleted. mcp-parser now classifies known methods
    AND the body-less MCP transport patterns (SSE channel-open via
    GET, session-terminate via DELETE+Mcp-Session-Id), populating
    MCPExtension.IsAction or a synthetic $transport/* extension as
    appropriate.

  - New step 4 (classification gate) reads (anyAction, anyBypass)
    from pctx.Classification(). anyBypass → Skip("protocol_mechanics");
    !anyAction → Continue silently (defense-in-depth pass-through);
    action-classified traffic falls through to the existing inference
    operator-policy step and judge.

  - Step 5 in the new ordering (was 4) is the inference_bypass
    operator policy ("don't judge inference by default"). Stays in
    IBAC because it's policy, not classification — inference-parser
    correctly classifies LLM calls as actions; this step decides
    whether to honor that classification for inference traffic.

Capabilities change:

  - Capabilities now declares RequiresAny: ["mcp-parser", "a2a-parser",
    "inference-parser"] in place of After: ["mcp-parser"]. Without
    a parser there's no classification, and IBAC would silently
    no-op on every request — boot-fail catches this misconfig
    rather than letting it ship.

Test cleanup:

  - Removed TestOnRequest_MCPHousekeepingBypass — moved to mcp-parser.
  - Removed TestOnRequest_TransportStreamBypass_* — body-less GET/
    HEAD/OPTIONS recognition moved to mcp-parser as the synthetic
    $transport/stream extension on configured paths.
  - Renamed TestOnRequest_MCPToolsCallIsNotHousekeeping to
    TestOnRequest_MCPActionReachesJudge — same intent, new framing.
  - Added TestOnRequest_ClassificationBypass_ProtocolMechanics
    asserting the single gate skips on anyBypass=true.
  - Added TestOnRequest_NoClassification_PassesThrough asserting
    the defense-in-depth pass-through with no recorded Skip.
  - Updated makePCtx default to populate MCPExtension{IsAction: true}
    so existing tests reach the judge as they expect.

Documentation:

  - authbridge/CLAUDE.md gains a "Plugin classification" paragraph
    under the plugins overview pointing to the per-parser contract.
  - docs/framework-architecture.md gets a "Classification on the
    slot types" paragraph alongside the existing ContentSource
    discussion; the extension struct definitions now show IsAction.
  - docs/plugin-reference.md gets a "Classifying requests as actions
    vs protocol mechanics" section parallel to "Exposing content to
    guardrails", with the per-parser table and a guardrail-side
    consumption example.
  - docs/ibac-plugin.md flow rewritten to reflect the new step
    ordering: reentrancy → bypass_paths → bypass_hosts →
    classification → inference_bypass → intent → judge → verdict.
    Pipeline-composition section updated to mention RequiresAny.

Net effect at the guardrail layer: ~50 lines of IBAC-specific
protocol/transport knowledge replaced by ~10 lines of classification-
reading. The mechanism scales linearly with parser count, not with
request-shape count.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Four review-feedback follow-ups, all documentation only — no
behavior changes.

1. mcpparser/plugin.go isMCPAction: add an MCP spec-revision
   anchor (2025-03-26 Streamable HTTP) so future maintainers
   adding a new action method have a date stamp to compare
   against rather than re-deriving the action set.

2. mcpparser/plugin.go synthetic methods: soften the "$ prefix
   is reserved" framing. JSON-RPC 2.0 §6 only reserves "rpc.*";
   "$" is our own design choice motivated by visual distinguish-
   ability, not by spec reservation. Note the fallback escape
   hatch (switch to "_transport/*") if MCP ever starts using "$".

3. pipeline/context.go Classification doc: surface the parser-
   disjointness assumption that makes the multi-extension-mixed
   case rare, and explicitly note that callers (defense-in-depth
   gates vs audit-style guardrails) may want different
   precedence between anyAction and anyBypass when both are true.

4. ibac/plugin_test.go makePCtx: add a CONTRACT CHANGE NOTE
   documenting the opt-in → opt-out flip introduced by the
   parser-classification refactor (was: tests opt in by setting
   Extensions.MCP; now: tests opt out by setting it nil) so a
   future test author doesn't get confused about why their "no
   extension" test case is judging.

Pure comment churn; tests and behavior unchanged.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
… posture docs

Four review-feedback follow-ups in one commit, scoped to IBAC plus
its demo manifest and reference docs. Build + test pass.

1. Must-fix kagenti#1 — TestOnRequest_InferenceBypassByDefault assertion
   was passing for the wrong reason. With makePCtx populating
   MCPExtension{IsAction:true} by default, the test's
   InferenceExtension{IsAction:false} (zero value) caused
   step 4 (classification gate) to skip with protocol_mechanics
   before step 5 (inference-policy) could run. A regression in the
   inference-policy logic would have gone uncaught.

   Fix: set IsAction:true on the Inference extension (matches what
   inference-parser does in production). Add an explicit assertion
   on inv.Reason == "inference_bypass" so the test verifies the
   right step fired. Same fix shape as the earlier 944838d
   correction to TestOnRequest_InferenceJudgedWhenEnabled, which
   missed this sibling.

2. Must-fix kagenti#2 — IBAC demo's plain-HTTP exfiltration scenario
   (the headline ibac-plugin.md threat model) silently passed
   through the new defense-in-depth gate, since no parser claims
   plain HTTP and step 4 short-circuits with !anyAction.

   Fix: introduce unclassified_policy as a config field on the
   ibac plugin, parallel in shape to the existing no_intent_policy.

   - Default "passthrough": current behavior. Defense in depth —
     IBAC only judges what a parser classified.
   - "judge": falls through to inference policy + intent + judge
     even when no parser claimed the request. Catches plain-HTTP
     outbound at the cost of one judge round-trip per unclassified
     request.

   Demo's k8s/ibac-patch.yaml sets unclassified_policy: "judge"
   so the headline scenario keeps working. Production deployments
   using MCP/A2A/inference exclusively get cleaner defense-in-depth
   behavior without changing config; deployments that want plain-
   HTTP egress coverage opt in.

3. Inline suggestion — RequiresAny: ["mcp-parser", "a2a-parser",
   "inference-parser"] was misleading. RequiresAny is a same-chain
   check; IBAC runs OUTBOUND, but a2a-parser runs INBOUND in every
   in-tree config. Including it would let an outbound chain
   [a2a-parser, ibac] pass validation while a2a-parser populates
   nothing IBAC reads.

   Fix: drop a2a-parser. RequiresAny: ["mcp-parser", "inference-parser"].
   Doc-comment in Capabilities() explains the cross-chain a2a
   dependency stays runtime, governed by no_intent_policy.

4. Suggestion — operator-facing default-posture documentation.
   docs/ibac-plugin.md gains a "Default security posture"
   subsection under Limitations covering both fail-open knobs
   (unclassified_policy, no_intent_policy) and how operators choose
   between them. Config-table rows added for both. Threat-coverage
   notes added: plain-HTTP exfil is covered when unclassified_policy:
   "judge"; MCP-shaped exfil is covered by default. The PR's
   compatibility section is intentionally NOT amended for
   no_intent_policy — that flag landed in kagenti#450 (already in main),
   not kagenti#452, and the durable home for the operator guidance is the
   plugin's reference doc.

Tests:
- TestOnRequest_InferenceBypassByDefault now asserts inv.Reason.
- New TestOnRequest_UnclassifiedPolicy_Judge covers the policy=judge
  branch (unclassified body-having POST → reaches judge → reject).
- New TestConfigure_UnclassifiedPolicy_DefaultsToPassthrough.
- New TestConfigure_UnclassifiedPolicy_RejectsUnknownValue.
- New TestConfigure_UnclassifiedPolicy_AcceptsExplicitValues
  (parameterized over {passthrough, judge}).
- Existing tests pass unchanged — default behavior preserved.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
The `make build-abctl` target was a no-invalidation cache: if
/tmp/abctl-ibac-demo existed, it was reused, even if the abctl
source had changed since. Anyone iterating on abctl while testing
the IBAC demo would silently get a stale binary out of `make
show-result` and hit cryptic runtime errors when the binary's
behavior diverged from the current source (e.g. picker port-
forward features added after the binary was built).

Concrete symptom that surfaced this: after the abctl picker port-
forward was added to the source, an old cached binary still
connected to a hardcoded localhost:9094, while the picker was
allocating random local ports — abctl reported "connection
refused" against a port nothing was listening on.

Replace the if-exists check with a freshness check: rebuild the
binary when any .go file under cmd/abctl/ is newer than the
cached binary. Equivalent to how the demo's per-image .built
stamp files invalidate against source mtimes elsewhere in the
same Makefile.

Behavior:
- First run: builds.
- Subsequent runs with no abctl-source changes: skips with a
  message naming the cache file.
- Subsequent runs after editing any .go under cmd/abctl/:
  rebuilds, prints "source has been modified since ... rebuilding".

The launch script (scripts/launch-abctl.sh) already matches
abctl's picker model — it just runs the binary and the picker
handles port-forward setup. The bug was purely the stale-binary
cache; no script changes needed.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
A2A spec §6.6 says the server SHOULD assign a contextId on first
interaction when the client omits one. The IBAC demo's email-agent
treated `sessionID` as a passthrough trace identifier — read it
from the inbound message, fall back to X-Session-Id header, return
whatever was found (including the empty string) — and never minted.

The kagenti UI currently sends bare `message/send` calls with no
contextId / sessionId / taskId field, so the agent's sessionID was
empty on every turn. After commit 84c323d (drop ActiveSession
fallback) the listener stopped silently inheriting prior buckets
on empty contextId, so every conversation now collapses into the
"default" session bucket. Operators testing the demo can't
distinguish two test runs in abctl — both turns share one bucket
even after a UI refresh.

Fix: when both the message-level contextId and the X-Session-Id
header are empty, mint a fresh UUID and use that as sessionID.
The listener's rekey-on-response path then migrates Default →
<UUID> on the agent's reply, giving each conversation its own
bucket.

Backward-compatible:
- UI sends contextId → agent uses it (unchanged)
- UI sends X-Session-Id header → agent uses it (unchanged)
- UI sends nothing → agent mints, returns it; listener rekeys

If the kagenti UI ever starts round-tripping contextIds (which it
should, per spec), continuity across turns within a conversation
is preserved automatically — the same UUID stays in use, the same
bucket stays.

`newUUID()` is the existing UUID helper used elsewhere in this
file (writeRPCSuccess uses it for taskID).

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@huang195 huang195 force-pushed the fix/parser-classification branch from 7ccd1be to eec27ad Compare June 1, 2026 12:06
Copy link
Copy Markdown
Contributor

@mrsabath mrsabath left a comment

Choose a reason for hiding this comment

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

Follow-up commits comprehensively address all prior review feedback.

Prior finding Status
Must-fix #1: TestOnRequest_InferenceBypassByDefault passing for wrong reason Fixed in f001507IsAction:true on inference + assertion on inv.Reason == "inference_bypass"
Must-fix #2: ibac-plugin.md threat model contradicts new gate Fixed in f001507 — new unclassified_policy config knob + demo manifest opts in to "judge" + Default Security Posture doc section
Suggestion: RequiresAny cross-chain modeling Fixed in f001507a2a-parser dropped from RequiresAny; doc-comment explains the cross-chain runtime dependency governed by no_intent_policy
Suggestion: no_intent_policy posture flip undocumented Fixed in f001507 — Default Security Posture section in ibac-plugin.md covers both fail-open knobs

The new unclassified_policy config knob is the cleanest resolution to the "plain-HTTP exfil under defense-in-depth" tension — defaults to "passthrough" (clean defense in depth for production), opts into "judge" for the demo's threat-model walkthrough. New tests cover both branches plus default-validation (TestOnRequest_UnclassifiedPolicy_Judge, TestConfigure_UnclassifiedPolicy_*). Doc updates to ibac-plugin.md (Default Security Posture, Threat-coverage notes) and plugin-reference.md (Classifying requests contract) are thorough.

Unrelated demo improvements (f1e28b0 abctl rebuild-on-source-change; eec27ad mint contextId per A2A spec §6.6) are sensible and well-scoped.

Areas reviewed: Go pipeline framework (Classification, RequiresAny scope), IBAC classification gate, mcp-parser synthetic transport extensions + path config, threat-model docs, demo manifest, prior-review delta.
Commits: 9, all signed off.
CI status: All 19 checks passing.

@huang195 huang195 merged commit 0a8dfbb into kagenti:main Jun 1, 2026
19 checks passed
@github-project-automation github-project-automation Bot moved this from New /:ToDo to Done in Kagenti Issue Prioritization Jun 1, 2026
@huang195 huang195 deleted the fix/parser-classification branch June 1, 2026 16:39
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