-
Notifications
You must be signed in to change notification settings - Fork 40
fix(sparc-service): strip agent-injected keys before SPARC evaluates tool calls #738
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import logging | ||
|
|
||
| from fastapi import FastAPI, HTTPException | ||
|
|
@@ -19,6 +20,29 @@ | |
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
| # _LOG_REQUESTS and _STRIP_KEYS are now read from Settings (via Settings.from_env) | ||
| # so all config comes from a single place. See settings.py. | ||
|
|
||
|
|
||
| def _strip_tool_arg_keys(tool_calls: list[dict], keys: frozenset[str]) -> list[dict]: | ||
| """Return a copy of tool_calls with the named argument keys removed.""" | ||
| result = [] | ||
| for tc in tool_calls: | ||
| fn = tc.get("function") or {} | ||
| if not isinstance(fn, dict): | ||
| result.append(tc) | ||
| continue | ||
| raw_args = fn.get("arguments", "") | ||
| try: | ||
| args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args | ||
| if isinstance(args, dict): | ||
| args = {k: v for k, v in args.items() if k not in keys} | ||
| new_args = json.dumps(args) if isinstance(args, dict) else raw_args | ||
| except (json.JSONDecodeError, TypeError): | ||
| new_args = raw_args | ||
| result.append({**tc, "function": {**fn, "arguments": new_args}}) | ||
| return result | ||
|
|
||
|
|
||
| def create_app(engine: ReflectionEngine | None = None) -> FastAPI: | ||
| """Build the FastAPI app. Inject ``engine`` in tests; defaults to env config.""" | ||
|
|
@@ -51,6 +75,16 @@ def readyz() -> dict[str, object]: | |
|
|
||
| @app.post("/reflect", response_model=ReflectResponse) | ||
| async def reflect(request: ReflectRequest) -> ReflectResponse: | ||
| 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) | ||
|
|
||
|
Comment on lines
+78
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
As stated in the PR objectives: 🤖 Prompt for AI Agents |
||
| # SPARCReflectionComponent.process is synchronous (and CPU/IO bound on the | ||
| # LLM call); run it off the event loop so the service stays responsive. | ||
| try: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,6 +98,10 @@ class Settings: | |
| host: str = "0.0.0.0" | ||
| port: int = 8090 | ||
|
|
||
| # Request logging / arg-stripping (see api.py). | ||
| log_requests: bool = False | ||
| strip_tool_arg_keys: frozenset = field(default_factory=frozenset) | ||
|
|
||
| # Validation errors collected at load time (provider creds missing, etc.). | ||
| errors: tuple[str, ...] = field(default_factory=tuple) | ||
|
|
||
|
|
@@ -152,6 +156,12 @@ def from_env(cls) -> "Settings": | |
| f"provider={provider} requires SPARC_MODEL (e.g. azure/<deployment> or anthropic/claude-3-5-sonnet)" | ||
| ) | ||
|
|
||
| strip_tool_arg_keys: frozenset[str] = frozenset( | ||
| k.strip() | ||
| for k in os.getenv("SPARC_STRIP_TOOL_ARG_KEYS", "").split(",") | ||
| if k.strip() | ||
| ) | ||
|
|
||
| return cls( | ||
| provider=provider, | ||
| model=model, | ||
|
|
@@ -171,5 +181,7 @@ def from_env(cls) -> "Settings": | |
| llm_registry_id=os.getenv("SPARC_LLM_REGISTRY_ID", "").strip(), | ||
| host=os.getenv("HOST", "0.0.0.0"), | ||
| port=_int_env("PORT", 8090), | ||
| log_requests=_truthy(os.getenv("SPARC_LOG_REQUESTS", "")), | ||
| strip_tool_arg_keys=strip_tool_arg_keys, | ||
|
Comment on lines
+184
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject invalid
🤖 Prompt for AI Agents |
||
| errors=tuple(errors), | ||
| ) | ||
There was a problem hiding this comment.
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:
Repository: rossoctl/cortex
Length of output: 16840
🏁 Script executed:
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/reflectrequest 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
Source: Coding guidelines