## Summary
Adds two strictly opt-in env vars that let toolkit developers see
`developer_message` / `stacktrace` content *in* the agent-facing error
message while debugging. Off by default; activation requires a specific
acknowledgement string, not a boolean — `true`/`1` is explicitly
rejected with a warning log.
- `ARCADE_UNSAFE_DEBUG_LEAK_DEVELOPER_MESSAGE_TO_AGENT`
- `ARCADE_UNSAFE_DEBUG_LEAK_STACKTRACE_TO_AGENT`
- Magic ack: `yes-i-accept-leaking-internals-to-the-agent`
Everything goes through a single funnel — `ToolOutputFactory.fail` /
`fail_retry` in `arcade_core/output.py` — so the behavior covers both
the MCP server path and the Arcade Worker path with no call-site
changes. A loud `logger.warning` fires once per process on activation,
and a big header comment in `output.py` tells future maintainers not to
add more flags of this shape (debug info belongs in `logger.debug`, not
in a field that gets shipped to the model and often to end users).
Bumps `arcade-core` 4.6.2 → 4.7.0. Non-breaking, additive.
## Why
Today the project does a lot of work to keep `developer_message` and
`stacktrace` off the agent's context. That's the right default, but it
makes iterating on a new toolkit painful — you end up adding temporary
logging or rebuilds just to see what blew up. This gives toolkit authors
a safe, ugly, loud-on-activation escape hatch.
## Safety design
- Two separate flags so you only leak what you need.
- Magic string (not a boolean) activates the flag. Boolean-style values
are rejected and log a pointer to `output.py`.
- First activation logs a `WARNING` identifying the flag and the risk.
- Flags documented only in `CLAUDE.md`, not in the public README.
- Top-of-file banner in `output.py` explicitly tells maintainers not to
add more flags of this shape.
## Test plan
- [x] Existing test suite passes (1154 tests —
`libs/tests/{core,tool,arcade_mcp_server}`).
- [x] End-to-end smoke test against the built `arcade_core-4.7.0` wheel,
driven through `ToolExecutor.run` (same path toolkits hit). Covered
cases:
- flags off → message unchanged
- `ARCADE_UNSAFE_..._DEVELOPER_MESSAGE_TO_AGENT=true` → flag rejected,
warning logged, message unchanged
- `ARCADE_UNSAFE_..._DEVELOPER_MESSAGE_TO_AGENT=<magic>` → `[DEBUG]
developer_message: ...` appended
- both flags with magic, `ToolRuntimeError` path → developer_message
appended (stacktrace absent because `ToolRuntimeError.stacktrace()`
returned `None`, which is existing behavior)
- stacktrace flag with magic, generic `Exception` path → full
`traceback.format_exc()` appended, activation `WARNING` visible
Made with [Cursor](https://cursor.com)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Adds an opt-in path to include `developer_message` and stacktraces in
agent-facing MCP error messages, which could leak sensitive data if
misconfigured; safeguards (magic ack string + CI/pre-commit guard)
reduce but don’t eliminate risk.
>
> **Overview**
> Adds `arcade_mcp_server/_debug_exposure.py` with two env-gated debug
flags that, only when set to a specific acknowledgement string, append
`developer_message` and/or `stacktrace` into the agent-visible MCP tool
error `message` (and logs one-shot warnings on rejection/activation).
>
> Wires this into the MCP error path in `MCPServer._handle_call_tool`,
documents the flags in `CLAUDE.md`, bumps `arcade-mcp-server` to
`1.21.0`, and adds unit + integration tests plus a pre-commit hook and
GitHub Actions workflow (`scripts/check_debug_leak_flags_off.py`) to
ensure the magic ack string can’t be committed outside a small
allowlist.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
30e242c454128ec7cc62e169c2afd116be735cb5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
84 lines
3.3 KiB
Python
84 lines
3.3 KiB
Python
"""
|
|
Debug-only escape hatch for MCP tool error responses.
|
|
|
|
MCP clients typically render only the ``message`` field of a tool error
|
|
response, dropping ``developer_message`` and ``stacktrace``. That makes
|
|
server-side iteration painful when a tool is failing. The flags in this
|
|
module let a toolkit author opt in to appending those internals to the
|
|
``message`` field while debugging.
|
|
|
|
DEBUG-ONLY. Activating these flags can leak paths, tokens, or PII to
|
|
callers. Don't add more flags of this shape — put debug info in logs
|
|
instead.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
# Acknowledgement string a developer must set as the env value. Picked to be
|
|
# impossible to set by mistake — no sane config management or CI will ever
|
|
# emit this string.
|
|
_DEBUG_LEAK_MAGIC = "yes-i-accept-leaking-internals-to-the-agent"
|
|
|
|
_ENV_EXPOSE_DEVELOPER_MESSAGE = "ARCADE_DEBUG_EXPOSE_DEVELOPER_MESSAGE_IN_TOOL_ERROR_RESPONSES"
|
|
_ENV_EXPOSE_STACKTRACE = "ARCADE_DEBUG_EXPOSE_STACKTRACE_IN_TOOL_ERROR_RESPONSES"
|
|
|
|
# One-shot warning state per flag. The rejection warning (truthy but not the
|
|
# magic string) and the activation warning (magic string set) are tracked in
|
|
# *separate* sets so that fixing a misconfigured flag within the same process
|
|
# still fires the critical activation warning.
|
|
_warned_rejected: set[str] = set()
|
|
_warned_activated: set[str] = set()
|
|
|
|
|
|
def _leak_enabled(env_var: str) -> bool:
|
|
raw = os.environ.get(env_var)
|
|
if raw is None:
|
|
return False
|
|
if raw.strip() != _DEBUG_LEAK_MAGIC:
|
|
# A value is set but it isn't the magic ack. Treat as off and, if it
|
|
# looks like someone tried a boolean, nudge them via a log so the
|
|
# silence isn't confusing.
|
|
if raw.strip().lower() in {"1", "true", "yes", "on"} and env_var not in _warned_rejected:
|
|
_warned_rejected.add(env_var)
|
|
_logger.warning(
|
|
"%s is set to a truthy value but not to the required "
|
|
"acknowledgement string. Flag remains OFF. "
|
|
"See arcade_mcp_server/_debug_exposure.py.",
|
|
env_var,
|
|
)
|
|
return False
|
|
if env_var not in _warned_activated:
|
|
_warned_activated.add(env_var)
|
|
_logger.warning(
|
|
"%s is ENABLED. Tool error internals will be appended to the "
|
|
"`message` field of MCP tool error responses. This can leak paths, "
|
|
"tokens, or PII to callers. DO NOT USE IN PRODUCTION.",
|
|
env_var,
|
|
)
|
|
return True
|
|
|
|
|
|
def augment_error_message_for_debug(
|
|
message: str,
|
|
developer_message: str | None,
|
|
stacktrace: str | None,
|
|
) -> str:
|
|
"""Append debug internals to ``message`` when the corresponding env flags are set.
|
|
|
|
This is a no-op in the default case (both flags off), and also a no-op when
|
|
the flags are set to anything other than the activation ack string. See
|
|
module docstring for the full rationale.
|
|
"""
|
|
extras: list[str] = []
|
|
if developer_message and _leak_enabled(_ENV_EXPOSE_DEVELOPER_MESSAGE):
|
|
extras.append(f"developer_message: {developer_message}")
|
|
if stacktrace and _leak_enabled(_ENV_EXPOSE_STACKTRACE):
|
|
extras.append(f"stacktrace:\n{stacktrace}")
|
|
if not extras:
|
|
return message
|
|
return f"{message}\n\n[DEBUG] " + "\n\n[DEBUG] ".join(extras)
|