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},

// ─── ADK-114: TS ADK FunctionTool HTTP call without a timeout ───────────
// adk-js uses the options-object form (new FunctionTool({ ..., execute })),
// not the Python FunctionTool(fn) wrapper shape.
{
name: "ADK-114 fires on TS fetch with no AbortSignal", ruleID: "ADK-114",
kind: models.KindADKFunctionTool, lang: models.LanguageTypeScript, wantFires: true,
src: "import { FunctionTool } from \"@google/adk\";\n" +
"const t = new FunctionTool({ name: \"fetch_report\", description: \"Fetch a report.\", parameters: {}, execute: async () => {\n" +
" const r = await fetch(\"https://reports.internal/x\");\n" +
" return await r.text();\n" +
"} });\n",
},
{
name: "ADK-114 silent when AbortSignal present", ruleID: "ADK-114",
kind: models.KindADKFunctionTool, lang: models.LanguageTypeScript, wantFires: false,
src: "import { FunctionTool } from \"@google/adk\";\n" +
"const t = new FunctionTool({ name: \"fetch_report\", description: \"Fetch a report.\", parameters: {}, execute: async () => {\n" +
" const r = await fetch(\"https://reports.internal/x\", { signal: AbortSignal.timeout(15000) });\n" +
" return await r.text();\n" +
"} });\n",
},
{
name: "ADK-114 fires when fetch options omit any timeout", ruleID: "ADK-114",
kind: models.KindADKFunctionTool, lang: models.LanguageTypeScript, wantFires: true,
src: "import { FunctionTool } from \"@google/adk\";\n" +
"const t = new FunctionTool({ name: \"fetch_report\", description: \"Fetch a report.\", parameters: {}, execute: async () => {\n" +
" const r = await fetch(\"https://reports.internal/x\", { method: \"POST\" });\n" +
" return 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
34 changes: 34 additions & 0 deletions testdata/rules-fixture/google_adk/network.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,37 @@ rules:
Pass `timeout=` (typically 5–30 seconds) to the request. Choose a
value that's tight enough to fail fast and loose enough to allow
legitimate slow responses for this endpoint.

- id: ADK-114
title: TypeScript FunctionTool HTTP call has no timeout
severity: high
confidence: 0.6
language: typescript
applies_to:
- adk_function_tool
scope: tool
match:
has_http_call_without_timeout: true
explanation: >
This TypeScript ADK FunctionTool 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. What that costs depends on where the tool sits in the agent tree,
and the composition ADK encourages makes it worse rather than better: a
stalled tool inside a SequentialAgent blocks every step after it, and
inside a ParallelAgent the whole fan-out waits on its slowest branch, so
one unresponsive host stalls work that has nothing to do with it.
ADK-108's max_iterations bounds how many times a LoopAgent goes round, not
how long one tool call may run, so no configured limit breaks the stall.
The exposure compounds with ADK-016: a tool that fetches a
caller-controlled URL and cannot time out can be steered at an internal
host that 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 result so
the agent can react to an unreachable upstream rather than the branch
hanging.