Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion internal/rules/policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -2246,7 +2277,6 @@ var policyRepoRuleCases = []policyRepoCase{
},
models.RepoInventory{SDKsDetected: []models.SDK{models.SDKOpenAIAgents}},
false},

}

// optionsWithPermissionMode builds a ClaudeAgentOptionsDef whose captured
Expand Down
31 changes: 31 additions & 0 deletions testdata/rules-fixture/claude_sdk/network.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.