From 38e0df6ac3396a718b2db9499a6d94e0eb3e4c48 Mon Sep 17 00:00:00 2001 From: bradAGI <46579244+bradAGI@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:05:09 -0400 Subject: [PATCH] test(rules): mirror and cover CSDK-019 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine half of a coordinated pair with trustabl/trustabl-rules#83, on a branch of the same name so the rules-sync job resolves the matching pack rather than main. Neither half should merge alone — check-rules-sync.sh fails if they do. Mirrors claude_sdk/observability.yaml into testdata/rules-fixture and adds cases to policyRuleCases, as TestPolicyRules_AllRulesCovered requires. Three cases: the print, the module-logger remediation, and a pprint call that must stay silent. The third pins has_print_call's bare-callee behavior, so the rule cannot regress into substring matching that sweeps in pprint and every other callee whose name contains "print". --- internal/rules/policies_test.go | 22 +++++++++- .../claude_sdk/observability.yaml | 41 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 testdata/rules-fixture/claude_sdk/observability.yaml diff --git a/internal/rules/policies_test.go b/internal/rules/policies_test.go index 65af6c0a..987ea228 100644 --- a/internal/rules/policies_test.go +++ b/internal/rules/policies_test.go @@ -1986,6 +1986,27 @@ def fetch_data(x: str) -> dict: return {} `, toolConfig: nil, wantFires: false}, + {name: "CSDK-019 fires on print() in the tool body", ruleID: "CSDK-019", kind: models.KindClaudeSDKTool, src: ` +def lookup_order(order_id: str) -> str: + """Look up an order.""" + print("looking up " + order_id) + return order_id +`, wantFires: true}, + {name: "CSDK-019 silent when logging to stderr instead", ruleID: "CSDK-019", kind: models.KindClaudeSDKTool, src: ` +import logging +logger = logging.getLogger(__name__) +def lookup_order(order_id: str) -> str: + """Look up an order.""" + logger.info("looking up %s", order_id) + return order_id +`, wantFires: false}, + {name: "CSDK-019 silent on pprint (bare-callee guard)", ruleID: "CSDK-019", kind: models.KindClaudeSDKTool, src: ` +from pprint import pprint +def lookup_order(order_id: str) -> str: + """Look up an order.""" + pprint({"order_id": order_id}) + return order_id +`, wantFires: false}, } // policyRepoRuleCases covers repo-scoped rules. @@ -2246,7 +2267,6 @@ var policyRepoRuleCases = []policyRepoCase{ }, models.RepoInventory{SDKsDetected: []models.SDK{models.SDKOpenAIAgents}}, false}, - } // optionsWithPermissionMode builds a ClaudeAgentOptionsDef whose captured diff --git a/testdata/rules-fixture/claude_sdk/observability.yaml b/testdata/rules-fixture/claude_sdk/observability.yaml new file mode 100644 index 00000000..42e5d36e --- /dev/null +++ b/testdata/rules-fixture/claude_sdk/observability.yaml @@ -0,0 +1,41 @@ +policy: + id: claude_sdk_observability + name: Claude Agent SDK tool observability hygiene + category: claude_sdk + description: > + Rules covering how a Claude Agent SDK tool emits diagnostics. A tool body + that prints to stdout writes somewhere neither the model nor the + application's log sink reads, and can corrupt a transport that uses stdout + as a protocol channel. + +rules: + - id: CSDK-019 + title: Tool prints to stdout for diagnostics + severity: low + confidence: 0.65 + language: python + applies_to: + - claude_sdk_tool + scope: tool + match: + has_print_call: true + explanation: > + The tool body calls print(), which writes diagnostics to the process's + stdout. The model never sees that output — only the tool's return value + flows back into the agent loop — so the print silently disappears in any + deployment that captures structured records rather than raw stdout. Two + Claude-SDK-specific consequences make it worse than a lost log line. A + tool defined in a Python SDK server is commonly served to the agent over + an MCP stdio transport, where stdout carries the JSON-RPC frames: a loose + print interleaves with them and the client hits a parse error on a line + that is not JSON. And when the SDK is driven programmatically, the host + application is reading the SDK's own message stream, so tool prints land + interleaved with it rather than in the application's logs, attributable to + no particular turn or tool call. + fix: > + Remove the print(). For operator diagnostics, emit through a module logger + (logging.getLogger(__name__).info(...)) so the record carries its module + and level and lands in the application's log sink; where the tool may be + served over a stdio transport, make sure that handler writes to stderr, + which the transport leaves alone. If the information needs to reach the + model, include it in the tool's return value instead.