From 4f53f93aa5677785e41ef180ab955af5b46c36f4 Mon Sep 17 00:00:00 2001 From: bradAGI <46579244+bradAGI@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:06:00 -0400 Subject: [PATCH] test(rules): mirror and cover CSDK-020 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#84, 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/network.yaml into testdata/rules-fixture and adds cases to policyRuleCases, as TestPolicyRules_AllRulesCovered requires. CSDK-003 covers Python; the TypeScript half was missing even though this pack ships TS rules throughout. Three cases, following OAI-016's own table: the bare fetch, the AbortSignal remediation, and a fetch whose options object is present but carries no timeout — the last pinning that the predicate checks for a deadline rather than merely for an options argument. --- internal/rules/policies_test.go | 32 ++++++++++++++++++- .../rules-fixture/claude_sdk/network.yaml | 31 ++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/internal/rules/policies_test.go b/internal/rules/policies_test.go index 65af6c0a..cde60e3b 100644 --- a/internal/rules/policies_test.go +++ b/internal/rules/policies_test.go @@ -1986,6 +1986,37 @@ def fetch_data(x: str) -> dict: return {} `, toolConfig: nil, wantFires: false}, + + // ─── CSDK-020: TS Claude SDK tool HTTP call without a timeout ─────────── + // Structural has_http_call_without_timeout, as OAI-016 uses. CSDK-003 is + // the Python half; this pack shipped TS rules but no TS timeout rule. + { + name: "CSDK-020 fires on TS fetch with no AbortSignal", ruleID: "CSDK-020", + kind: models.KindClaudeSDKTool, lang: models.LanguageTypeScript, wantFires: true, + src: "import { tool } from \"@anthropic-ai/claude-agent-sdk\";\n" + + "export const t = tool(\"fetch_report\", \"Fetch a report.\", {}, async () => {\n" + + " const r = await fetch(\"https://reports.internal/x\");\n" + + " return { content: [{ type: \"text\", text: await r.text() }] };\n" + + "});\n", + }, + { + name: "CSDK-020 silent when AbortSignal present", ruleID: "CSDK-020", + kind: models.KindClaudeSDKTool, lang: models.LanguageTypeScript, wantFires: false, + src: "import { tool } from \"@anthropic-ai/claude-agent-sdk\";\n" + + "export const t = tool(\"fetch_report\", \"Fetch a report.\", {}, async () => {\n" + + " const r = await fetch(\"https://reports.internal/x\", { signal: AbortSignal.timeout(15000) });\n" + + " return { content: [{ type: \"text\", text: await r.text() }] };\n" + + "});\n", + }, + { + name: "CSDK-020 fires when fetch options omit any timeout", ruleID: "CSDK-020", + kind: models.KindClaudeSDKTool, lang: models.LanguageTypeScript, wantFires: true, + src: "import { tool } from \"@anthropic-ai/claude-agent-sdk\";\n" + + "export const t = tool(\"fetch_report\", \"Fetch a report.\", {}, async () => {\n" + + " const r = await fetch(\"https://reports.internal/x\", { method: \"POST\" });\n" + + " return { content: [{ type: \"text\", text: await r.text() }] };\n" + + "});\n", + }, } // policyRepoRuleCases covers repo-scoped rules. @@ -2246,7 +2277,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/network.yaml b/testdata/rules-fixture/claude_sdk/network.yaml index 012b5137..d85e18e4 100644 --- a/testdata/rules-fixture/claude_sdk/network.yaml +++ b/testdata/rules-fixture/claude_sdk/network.yaml @@ -53,3 +53,34 @@ rules: fix: > Pass `timeout=` (typically 5–30s) to the request. Surface failures as a structured error the model can react to. + + - id: CSDK-020 + title: TypeScript Claude SDK tool HTTP call has no timeout + severity: high + confidence: 0.6 + language: typescript + applies_to: + - claude_sdk_tool + scope: tool + match: + has_http_call_without_timeout: true + explanation: > + This TypeScript Claude SDK tool makes an outbound HTTP call (fetch / + axios / got / undici) with no deadline — no signal, timeout, or + abortSignal option. Node's fetch has no implicit deadline, so a slow or + unresponsive host blocks the tool callback until the socket eventually + dies. Because the call sits inside the agent's turn, that stalls the + conversation rather than failing it: the model gets no result and no + error, the turn cannot advance, and a max_turns cap does not help because + the run is stuck inside a single turn rather than taking too many. In a + server embedding the SDK it also holds the request worker for the + duration. The exposure compounds with CSDK-013: a tool that fetches a + model-controlled URL and cannot time out can be pointed at an internal + host that simply never answers. + fix: > + Attach a deadline. On modern runtimes, + await fetch(url, { signal: AbortSignal.timeout(15_000) }); on older ones, + create an AbortController, abort it from a setTimeout, pass + controller.signal, and clear the timer in a finally. axios and got take a + timeout option directly. Return the abort as a structured tool error so + the model can retry or route around it instead of the turn hanging.