Skip to content

fix(sparc-service): strip agent-injected keys before SPARC evaluates tool calls - #738

Merged
OsherElhadad merged 3 commits into
rossoctl:mainfrom
vz-ibm:fix/sparc-session-id-strip
Aug 9, 2026
Merged

fix(sparc-service): strip agent-injected keys before SPARC evaluates tool calls#738
OsherElhadad merged 3 commits into
rossoctl:mainfrom
vz-ibm:fix/sparc-session-id-strip

Conversation

@vz-ibm

@vz-ibm vz-ibm commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Problem

Exgentic wraps every agent response as a fake message tool call and
injects a session_id key into its JSON arguments (e.g.
{"content": "...", "session_id": "912ebc98-..."}). This key is not
declared in any tool spec, so SPARC's static layer correctly rejects the
call — but the rejection is spurious: it's a structural technicality, not
a real policy violation, and it short-circuits SPARC's semantic evaluation
before it ever runs.

Verified live: a real Tau2 exchange_delivered_order_items WRITE call was
static-layer-rejected purely because of the injected session_id
(decision=reject score=- ms=4.1).

Fix

  • SPARC_STRIP_TOOL_ARG_KEYS=<comma-separated keys> — removes the named
    keys from every tool_calls[].function.arguments before SPARC evaluates
    the call. No-op when unset.
  • SPARC_LOG_REQUESTS=true — logs the full incoming ReflectRequest JSON
    at DEBUG level, for diagnosing unexpected argument keys without
    rebuilding. No-op when unset.
  • Root logger configuration so log.info()/log.debug() output from
    sparc_service.* loggers actually reaches stdout (uvicorn's
    log_level="info" only configures uvicorn's own logger, not the Python
    root logger, so application-level logs were silently dropped).

Verification

Set SPARC_STRIP_TOOL_ARG_KEYS=session_id, re-ran the same tool call:
decision=approve score=1.00 ms=6990.7 — no session_id in the
evaluated args, and the ~7s latency confirms a genuine LLM-backed semantic
evaluation ran this time (vs. the ~4ms static-layer short-circuit before).

Summary by CodeRabbit

  • New Features

    • Added configurable request logging to support troubleshooting and monitoring.
    • Added configurable sanitization of selected tool arguments before requests are processed.
    • Sanitized request details can be recorded for improved visibility while helping protect sensitive values.
    • Added clearer startup logging for improved service visibility.
  • Bug Fixes

    • Preserved malformed or non-dictionary tool arguments unchanged instead of altering or rejecting them.

vz-ibm added 2 commits August 5, 2026 08:23
…KEYS env vars

SPARC_LOG_REQUESTS=true logs the full incoming ReflectRequest JSON at INFO
level so unexpected tool argument keys can be diagnosed without rebuilding.

SPARC_STRIP_TOOL_ARG_KEYS=<comma-separated keys> removes the named keys from
every tool_calls[].function.arguments before the request reaches SPARC. Needed
as a configurable hotfix for Exgentic sending session_id in tool arguments —
a key not declared in the tool spec that causes SPARC to reject the call.

Both vars are no-ops when unset. No image rebuild required to toggle them;
set via kubectl set env or the sparc-service ConfigMap.

Signed-off-by: Vitaly Zabershinsky <VITALYZ@il.ibm.com>
…utput is visible

uvicorn.run() with log_level="info" only configures the uvicorn logger, not the
Python root logger — application loggers (sparc_service.api) had no handler and
were silently dropped. Adding basicConfig before uvicorn.run() ensures all INFO+
log lines reach stdout.

Signed-off-by: Vitaly Zabershinsky <VITALYZ@il.ibm.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The service configures INFO-level logging at startup. The /reflect endpoint supports environment-controlled request logging and removes configured keys from valid tool-call arguments before reflection processing.

Changes

Request processing

Layer / File(s) Summary
Request-processing settings
authbridge/sparc-service/sparc_service/settings.py
Settings reads request logging and tool-argument stripping options from environment variables.
Reflection request logging and sanitization
authbridge/sparc-service/sparc_service/api.py
The /reflect handler logs requests when enabled and removes configured keys from valid tool-call arguments. Malformed or non-dictionary arguments remain unchanged.
Startup logging
authbridge/sparc-service/sparc_service/__main__.py
main() initializes INFO-level logging before server startup.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • rossoctl/cortex#739: Both PRs modify SPARC request logging, tool-argument sanitization, and related logging settings.

Suggested reviewers: abigailgold

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.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 and concisely describes the primary change: stripping agent-injected keys before SPARC evaluates tool calls.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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

🤖 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/sparc-service/sparc_service/__main__.py`:
- Around line 11-12: Update the logging setup in the module-level initialization
around logging.basicConfig so application logs are explicitly routed to
sys.stdout by configuring its stream, while preserving the existing INFO level.
Ensure the deployed entry point’s direct sparc_service.logger usage follows this
stdout configuration rather than defaulting to stderr.

In `@authbridge/sparc-service/sparc_service/api.py`:
- Around line 43-44: Update _strip_tool_arg_keys to validate that each tool call
and its function value are mappings before invoking .get on them. Handle
malformed entries such as null or scalar function values consistently with the
existing stripping behavior, and keep argument processing inside the endpoint’s
established error-handling path.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 79b39f85-6f17-4735-b5f0-926a0b71b7eb

📥 Commits

Reviewing files that changed from the base of the PR and between fe449ae and 91262d0.

📒 Files selected for processing (2)
  • authbridge/sparc-service/sparc_service/__main__.py
  • authbridge/sparc-service/sparc_service/api.py

Comment on lines +11 to +12
import logging
logging.basicConfig(level=logging.INFO)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 5 \
  'basicConfig|sys\.stdout|sys\.stderr|stream=|log_config' \
  authbridge/sparc-service

Repository: rossoctl/cortex

Length of output: 1023


🌐 Web query:

Python logging.basicConfig default stream stderr documentation

💡 Result:

Yes, the Python logging.basicConfig function defaults to using sys.stderr [1]. When basicConfig is called without a filename argument, it configures the logging system by creating a StreamHandler [2][1]. If the optional stream keyword argument is not provided to basicConfig, this StreamHandler defaults to sys.stderr [1]. This behavior is inherited from the StreamHandler class itself, which defaults to sys.stderr when no stream is explicitly specified [3][1]. If you wish to send logs to standard output instead, you can explicitly pass sys.stdout to the stream argument [4]: import logging import sys logging.basicConfig(stream=sys.stdout)

Citations:


Route application logs to stdout.

logging.basicConfig() defaults to sys.stderr, so the sparc_service.logger call in this module does not meet the stdout contract. Add stream=sys.stdout, or route this logger through Uvicorn’s logging configuration. Verify the deployed entry point uses loguru or sparc_service.logger directly.

🤖 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/sparc-service/sparc_service/__main__.py` around lines 11 - 12,
Update the logging setup in the module-level initialization around
logging.basicConfig so application logs are explicitly routed to sys.stdout by
configuring its stream, while preserving the existing INFO level. Ensure the
deployed entry point’s direct sparc_service.logger usage follows this stdout
configuration rather than defaulting to stderr.

Comment thread authbridge/sparc-service/sparc_service/api.py Outdated
@OsherElhadad

Copy link
Copy Markdown
Contributor

Good catch on the root cause — a spurious static-layer reject from an undeclared session_id is exactly the kind of thing that silently guts semantic evaluation, and the before/after timings (4.1ms → 6990.7ms) are convincing evidence the LLM path now actually runs. The mechanism is right. Three things before this lands.

1. Overlap with #739 — pick one home for this code

The api.py half of this PR is byte-identical to part of #739 (same _strip_tool_arg_keys, same _LOG_REQUESTS flag), and #739 also touches __main__.py logging. Whichever merges second will conflict or silently duplicate. Suggest either rebasing #739 on this once merged, or dropping the api.py/__main__.py changes from #739 and letting this PR own them.

2. Config bypasses Settings (blocking, for me)

_STRIP_KEYS and _LOG_REQUESTS are read via os.getenv at module import time, not through Settings.from_env(). Three consequences:

  • inconsistent with every other knob in the service, which lives in settings.py with validation;
  • untestable — monkeypatch.setenv after import is a no-op, which is why there is no test for the stripping logic;
  • not reachable via the supported install path: deploy/Makefile builds the ConfigMap from a fixed list of keys, so SPARC_STRIP_TOOL_ARG_KEYS can't be set with make install today.

Please move both onto Settings (e.g. strip_tool_arg_keys: frozenset[str], log_requests: bool), add them to the README.md env table, and thread them through deploy/Makefile's ConfigMap.

3. {"function": null} raises 500 instead of 400

tc.get("function", {}) returns None when the key is present with a null value, so fn.get("arguments", "") on the next line raises AttributeError outside the try — and outside the endpoint's except ValueError → 400 path, so a malformed request becomes a 502/500. One-liner:

fn = tc.get("function") or {}
if not isinstance(fn, dict):
    result.append(tc)
    continue

(This is CodeRabbit's inline comment on line 44 — it's valid.)

Non-blocking

  • Add a unit test. _strip_tool_arg_keys is a pure function; once the keys come from Settings this is three asserts (key stripped, key absent, args not valid JSON → passed through unchanged). No network, fits tests/test_api.py.
  • import logging inside main() — move to module top-level with the existing imports.
  • Consider whether this belongs in sparc_service at all. The tool spec in fix(sparc-service): WatsonX reasoning-model support + Dockerfile fix + SPARC_SKIP_TOOLS #739's fixture is titled cancel_reservationArgumentsWithoutSessionId, which suggests the MCP layer already knows to exclude session_id from specs but not from calls. A generic strip list is a fine stopgap for Exgentic, but worth an issue on the agent/MCP side so we're not carrying this forever.
  • Dismiss CodeRabbit's stderr comment on __main__.py. basicConfig defaulting to stderr is fine — the Deployment runs with PYTHONUNBUFFERED=1 and kubelet captures both streams into the pod log. No stdout contract exists here to violate.

Happy to approve once (2) and (3) are addressed.

…s integration, null function guard

- settings.py: add log_requests (bool) and strip_tool_arg_keys (frozenset)
  fields to Settings dataclass; parse them from SPARC_LOG_REQUESTS and
  SPARC_STRIP_TOOL_ARG_KEYS in Settings.from_env() using _truthy()
- api.py: remove module-level os.getenv for _LOG_REQUESTS/_STRIP_KEYS;
  thread settings into the reflect endpoint so it reads settings.log_requests
  and settings.strip_tool_arg_keys instead of module globals; drop unused
  os import
- api.py: guard _strip_tool_arg_keys against {"function": null} — use
  `fn = tc.get("function") or {}` and skip the entry (not crash) when fn
  is not a dict, so a null function key returns 400 not 500
- __main__.py: hoist `import logging` to module top (was inside main())

Signed-off-by: Vitaly Zabershinsky <VITALYZ@il.ibm.com>

@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

🧹 Nitpick comments (2)
authbridge/sparc-service/sparc_service/settings.py (1)

159-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for SPARC_STRIP_TOOL_ARG_KEYS parsing.

Cover whitespace trimming, duplicate keys, and an unset variable. Assert the resulting frozenset before the API consumes this setting.

🤖 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/sparc-service/sparc_service/settings.py` around lines 159 - 163,
Add focused tests for the settings parsing that initializes strip_tool_arg_keys,
covering whitespace trimming, duplicate-key deduplication, and an unset
SPARC_STRIP_TOOL_ARG_KEYS variable. Assert the resulting frozenset directly
before any API consumption.
authbridge/sparc-service/sparc_service/api.py (1)

27-45: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add regression tests for the sanitizer.

Add or verify tests for JSON strings, dictionary arguments, invalid JSON preservation, configured-key removal, missing function, function: null, and scalar function values. The malformed-call test must assert the endpoint returns a 4xx response, not only that the helper avoids an exception.

As stated in the PR objectives: add unit tests for argument stripping.

🤖 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/sparc-service/sparc_service/api.py` around lines 27 - 45, Add
regression tests for _strip_tool_arg_keys covering JSON-string and dictionary
arguments, configured-key removal, invalid JSON preservation, missing or null
function values, and scalar function values. Also add a malformed tool-call
endpoint test that asserts a 4xx response, ensuring endpoint validation is
verified rather than only helper exception avoidance.
🤖 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/sparc-service/sparc_service/api.py`:
- Around line 78-87: Update the request logging in the visible reflect-request
handling block so both payload logs use log.debug rather than log.info, while
preserving the existing SPARC_LOG_REQUESTS setting as the gate for DEBUG output.
Redact configured sensitive tool-argument keys before logging the incoming
request, and ensure neither the pre-strip request nor post-strip tool_calls
exposes raw sensitive values.
- Around line 81-83: Restrict the stripping performed by the request handling
flow around _strip_tool_arg_keys to a protected allowlist of harmless internal
logging metadata, rather than arbitrary operator-provided keys. Preserve
/reflect request headers and all authorization, session, IBAC, token-exchange,
and policy-related arguments; add a regression test proving those keys remain
intact while approved metadata is removed.

In `@authbridge/sparc-service/sparc_service/settings.py`:
- Around line 184-185: Update the SPARC_LOG_REQUESTS handling in the settings
construction flow to validate unrecognized non-empty values instead of silently
treating them as false. When the value is not an accepted boolean
representation, append a descriptive error to the existing errors collection
while preserving valid true, false, and unset behavior.

---

Nitpick comments:
In `@authbridge/sparc-service/sparc_service/api.py`:
- Around line 27-45: Add regression tests for _strip_tool_arg_keys covering
JSON-string and dictionary arguments, configured-key removal, invalid JSON
preservation, missing or null function values, and scalar function values. Also
add a malformed tool-call endpoint test that asserts a 4xx response, ensuring
endpoint validation is verified rather than only helper exception avoidance.

In `@authbridge/sparc-service/sparc_service/settings.py`:
- Around line 159-163: Add focused tests for the settings parsing that
initializes strip_tool_arg_keys, covering whitespace trimming, duplicate-key
deduplication, and an unset SPARC_STRIP_TOOL_ARG_KEYS variable. Assert the
resulting frozenset directly before any API consumption.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c0486ac-075a-4a62-a90d-fc81c77dda25

📥 Commits

Reviewing files that changed from the base of the PR and between 91262d0 and ec90760.

📒 Files selected for processing (3)
  • authbridge/sparc-service/sparc_service/__main__.py
  • authbridge/sparc-service/sparc_service/api.py
  • authbridge/sparc-service/sparc_service/settings.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • authbridge/sparc-service/sparc_service/main.py

Comment on lines +78 to +87
if settings.log_requests:
log.info("incoming reflect request: %s", request.model_dump_json())

if settings.strip_tool_arg_keys and request.tool_calls:
request = request.model_copy(
update={"tool_calls": _strip_tool_arg_keys(request.tool_calls, settings.strip_tool_arg_keys)}
)
if settings.log_requests:
log.info("after strip (%s): tool_calls=%s", sorted(settings.strip_tool_arg_keys), request.tool_calls)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not log raw request payloads at INFO level.

log.info logs the complete request before stripping and logs tool calls again afterward. With SPARC_LOG_REQUESTS enabled, session identifiers and other tool arguments can enter normal production logs. Use log.debug, redact sensitive configured keys, and ensure the setting enables DEBUG output without promoting payloads to INFO.

As stated in the PR objectives: SPARC_LOG_REQUESTS must log incoming requests at DEBUG level.

🤖 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/sparc-service/sparc_service/api.py` around lines 78 - 87, Update
the request logging in the visible reflect-request handling block so both
payload logs use log.debug rather than log.info, while preserving the existing
SPARC_LOG_REQUESTS setting as the gate for DEBUG output. Redact configured
sensitive tool-argument keys before logging the incoming request, and ensure
neither the pre-strip request nor post-strip tool_calls exposes raw sensitive
values.

Comment on lines +81 to +83
if settings.strip_tool_arg_keys and request.tool_calls:
request = request.model_copy(
update={"tool_calls": _strip_tool_arg_keys(request.tool_calls, settings.strip_tool_arg_keys)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'SPARC_STRIP_TOOL_ARG_KEYS|strip_tool_arg_keys|session_id|IBAC|token|authorization' \
  authbridge/sparc-service \
  --glob '*.py' \
  --glob '*.yaml'

Repository: rossoctl/cortex

Length of output: 16840


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant implementations and nearby tests without running repository code.
sed -n '1,130p' authbridge/sparc-service/sparc_service/api.py
printf '\n--- settings.py relevant section ---\n'
sed -n '130,195p' authbridge/sparc-service/sparc_service/settings.py
printf '\n--- tests around strip/logging ---\n'
sed -n '1,180p' authbridge/sparc-service/tests/test_api.py
printf '\n--- config/docs references ---\n'
rg -n -C 3 'SPARC_STRIP_TOOL_ARG_KEYS|strip_tool_arg_keys|SECURE|SESSION|IBAC|Authorization|Authorization|session_id' . \
  --glob '*.md' --glob '*.txt' --glob '*.yaml' --glob '*.yml' --glob '*.py' | sed -n '1,240p'

Repository: rossoctl/cortex

Length of output: 40077


Restrict stripping to harmless request metadata.

The strip setting accepts operator-provided key names, and _strip_tool_arg_keys() removes them from reflection arguments before SPARC evaluates the tool call. If this list includes authorization/session keys used by IBAC, token exchange, or policy decisions, it can bypass controls. Keep /reflect request headers intact, and only strip request-specific logging metadata through a protected allowlist or fixed internal keys, plus a regression test.

🤖 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/sparc-service/sparc_service/api.py` around lines 81 - 83, Restrict
the stripping performed by the request handling flow around _strip_tool_arg_keys
to a protected allowlist of harmless internal logging metadata, rather than
arbitrary operator-provided keys. Preserve /reflect request headers and all
authorization, session, IBAC, token-exchange, and policy-related arguments; add
a regression test proving those keys remain intact while approved metadata is
removed.

Source: Coding guidelines

Comment on lines +184 to +185
log_requests=_truthy(os.getenv("SPARC_LOG_REQUESTS", "")),
strip_tool_arg_keys=strip_tool_arg_keys,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject invalid SPARC_LOG_REQUESTS values.

_truthy() returns False for every unrecognized value. A typo such as SPARC_LOG_REQUESTS=treu therefore disables request logging without reporting a configuration error. Validate the value and append an error to errors, consistent with the existing settings validation flow.

🤖 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/sparc-service/sparc_service/settings.py` around lines 184 - 185,
Update the SPARC_LOG_REQUESTS handling in the settings construction flow to
validate unrecognized non-empty values instead of silently treating them as
false. When the value is not an accepted boolean representation, append a
descriptive error to the existing errors collection while preserving valid true,
false, and unset behavior.

@OsherElhadad
OsherElhadad merged commit efc9e94 into rossoctl:main Aug 9, 2026
20 checks passed
@github-project-automation github-project-automation Bot moved this from New/ToDo to Done in Rossoctl Issue Prioritization Aug 9, 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