# Coverage Coverage matrix for Trustabl's static analysis: which agent SDKs (and which languages) we currently scan, analyse, and detect against. This file is the at-a-glance reference; `ARCHITECTURE.md` has the implementation detail. _Last reviewed: 2026-10-26 (branch `feat/agcfg-preapproved-fetch-user-content`, merged with `main` 96a781a; first reviewed 2026-10-21) — **AGCFG-011: pre-approved fetch from a user-content host (`schema_version` 30 → 31, provisional; +1 rule, 355 total).** High / 0.7, repo scope, new pack `agent_config/fetch_permissions.yaml`, predicate `repo_settings_preapprove_user_content_fetch` over `ProjectConfigFacts.UserContentFetchAllow`. Not judged: user and managed settings, `WebFetch()` and non-`domain:` specifiers, WebSearch, Bash `curl` allows, MCP fetch tools, hosts outside the fixed list. Prior review: 2026-10-26 (branch `feat/mcpcfg-003-unpinned-mcp-launch`, merged with `main` 3147eba; first reviewed 2026-10-21) — **Unpinned MCP package launches (`schema_version` 29 → 30, provisional; +1 rule, 354 total):** new repo predicate `repo_mcp_config_launches_unpinned_package` and **MCPCFG-003** (high, 0.7) over `ProjectConfigFacts.UnpinnedMCPLaunch`, sharing the MCP client config parse with MCPCFG-002 and judging a disjoint set of entries. Prior review: 2026-10-26 (branch `feat/csdk-115-subagent-tools-inherit`, merged with `main` e3685b4; first reviewed 2026-10-21) — **Subagent inherits all tools (`schema_version` 28 → 29, provisional; +1 rule, 353 total):** **CSDK-115** (subagent scope, high, 0.75) on the new predicate `subagent_inherits_all_tools`: a `.claude/agents/` subagent with no `tools:` key and no `disallowedTools:` entries. In-memory `SubagentDef.ToolsOmitted` / `Canonical` carry key presence and pass-1 provenance; `tools:` with an empty or null value counts as declared, and shape-fallback files are skipped. Prior review: 2026-10-26 (branch `feat/ci-agent-workflows`, merged with `main` 26b6152; first reviewed 2026-10-19) — **CI workflows (`schema_version` 27 → 28, provisional; +5 rules, 352 total):** new unconditionally-loaded category `ci` with **CI-001** (coding agent on an untrusted trigger with no actor gate), **CI-002** (the same job holding a write token, a secret outside the model-key input or `secrets: inherit`), **CI-003** (`actions/checkout` without `persist-credentials: false` in an agent job), **CI-004** (unrestricted shell or approval / sandbox bypass), **CI-005** (shell or fetch with no earlier `harden-runner` `egress-policy: block`). The walk now reads the repo-root `.github/workflows/*.yml|yaml` into `ScanManifest.WorkflowFiles` and `github_workflow` components. `workflow_run` is deliberately outside the trigger set (follow-up). Prior review: 2026-10-26 (branch `feat/otel-labels-correctness`, merged with `main` c047646; first reviewed 2026-10-08) — **agent-reliability-otel-labels correctness, Batch 1 (`schema_version` 26 → 27; main took 22 for LC-116 and 23–26 for the `agent_config` pack).** `OtelLabelsSignal` gains the `run_construct` kind and per-call facts (`ModuleScope`, `RunIDConst`, `FailureSafe`, `HookUnknown`, `ErrorPath`, `AttemptConst`, `SideEffect`, `ExitReason`). Four repo-scope predicates back **OBS-009** (shared `Run`, medium), **OBS-010** (tool span labelled only on success, low), **OBS-011** (constant `attempt`, low) and **OBS-012** (run end only on success, low); test-file signals never count, one correct call credits the repo, and findings carry the offending call's file and line. The scan report gains `otel_labels`. Not built: the handoff rule (deferred, no recipe), the `side_effect` rules (Batch 2), the Go binding. Rule counts re-derived: **347** fixture / **347** production (343 + 4); `scripts/check-rules-sync.sh` passes. Prior review: 2026-10-13 (branch `feat/agcfg-preauth-config`, merged with `main` 32d845e) — **Pre-trust agent config (`schema_version` 22 → 23; +5 rules, 339 total):** new unconditionally-loaded category `agent_config` with **AGCFG-004** (`enableAllProjectMcpServers` / `enabledMcpjsonServers` in `.claude/settings.json`), **AGCFG-005** (project hooks running repo scripts / curl / `bash -c` / credential paths; Claude and Gemini settings), **AGCFG-006** (sandbox / proxy / endpoint / loader override keys in `.gemini/.env`, `.codex/config.toml`, narrowly root `.env`; key names only), **AGCFG-007** (`*autoApprove*` / `*yolo*` = true in `.vscode/` and `.cursor/` settings) and **MCPCFG-002** (MCP client config launching a local command). The walk now enters `.gemini/`, `.vscode/`, `.cursor/`, `.codex/`; `.mcp.json` is an `mcp_config`. The Gemini CLI hooks shape and Codex project-config trust behavior are not verified against vendor docs. Prior review: 2026-10-05 (HEAD d8a4f57 + uncommitted working tree, branch `feat/server-entrypoint-detection`) — **Server-entrypoint fact (Python; no schema change, stays 20; no rule, no predicate):** `RepoInventory.Entrypoints` (also `ScanResult.entrypoints`) records FastAPI / Flask route handlers (`http`) and Celery tasks / Dramatiq actors (`worker`), import-gated, skipping `examples/`, `demo/`, `docs/`, `samples/`, `cookbook/`, `tutorial(s)/`; `AgentRunCallDef.ServerReachable` is stamped in memory when a run call is in the entrypoint, a same-file helper, or an imported helper (one hop each; relative imports resolved within the scan set). Nothing consumes it yet; ranks 4 and 6 build on it in follow-up PRs. Deferred: Django, Starlette `Route()`, RQ, Temporal, serverless handlers, `add_api_route` / `add_url_rule`, class-based views, TypeScript. Corpus baseline: 2 entrypoints (Flask), 40 run calls, 0 stamped — all run calls are standalone scripts. Prior review: 2026-10-05 (HEAD 05c44c9 + uncommitted working tree, branch `fix/obs-global-instrumentor-credit`) — **Observability false-positive fix: process-wide instrumentor calls are credited (no schema change; `schema_version` stays 20, no new rules, no fixture change).** PYD-107's fix text recommends `logfire.instrument_pydantic_ai()`, but its match (`repo_has_observability` + `agent_kwarg_missing: [instrument]`) never read that call, so following the fix still produced one finding per agent; LC-112's "configure a global handler" advice had the same gap. Crediting the call as repo-level init evidence alone cannot fix this (the kwarg check does not read init evidence) and would make it worse for `Agent.instrument_all()`, so the credit is agent-scoped: new `AgentDef.InstrumentedBy` (`instrumented_by` in the JSON report, omitted when unset), stamped by `analysis.ApplyGlobalInstrumentation` from the init signals, and `PredAgentKwargMissing` treats the exact kwarg it names as set. Covered: `logfire.instrument_pydantic_ai` and `Agent.instrument_all()` (→ `instrument`, Pydantic AI, Python), `LangChainInstrumentor().instrument()` (→ `callbacks`, LangChain, Python). Also recognized as init evidence but crediting no agent: `logfire.instrument_openai_agents`, `logfire.instrument_mcp`, any other `Instrumentor(...).instrument(...)`. `.instrument_all()` counts only in a file importing `pydantic_ai` or `logfire`; the instrumentor shape only in a file importing `openinference`, `opentelemetry` or `traceloop` (the two-step `i = XInstrumentor(); i.instrument()` is not matched). Behavior changes beyond PYD-107/LC-112: a repo whose only signal is one of these calls now counts as observed (the repo absence rules go quiet, OBS-001 stays silent). VAI-101 is unchanged because the Vercel AI SDK has no process-wide telemetry switch — `experimental_telemetry` is enabled per call. **Opik** added as a vendor (Python `opik` import, `opik.configure` / `OpikTracer` / `track_openai`; TS `opik` import, `new Opik()` / `OpikExporter` / `OpikCallbackHandler`; recon dep needle `opik`). Known risk: a repo that uses Opik only through the `@track` decorator plus env configuration imports `opik` but makes no init call, so it reads as imported-never-initialized and OBS-001 fires; Langfuse `@observe` already behaves the same way, and decorator detection (`@observe`, `@track`, `@traceable`) is deliberately deferred until a rule consumes it. **Observability backlog items investigated and deliberately not shipped.** **Tool result not recorded on a span (P0 rank 3): no rule.** Frameworks span every tool call once tracing is enabled (OpenAI Agents by default, Google ADK, LangChain `BaseTool`, Pydantic AI `instrument`, Vercel `experimental_telemetry`, CrewAI instrumentors), and enablement is often env- or CLI-only (LangSmith, Claude Code, ADK), so a repo-level "tracing present" gate cannot say whether a given tool is spanned. Unlike the HTTPS-only rules it is not same-body evidence, and a rule would fire on correctly traced tools. **Required span attributes (P0 rank 2): no rule.** In every audited SDK the attributes are written by the SDK or by the OTel / OpenInference instrumentor, so a well-instrumented repo contains no attribute literals and a static rule cannot tell "the instrumentor sets them" from "nobody sets them". The OTel GenAI semantic conventions are still Development stability and have renamed keys (`gen_ai.usage.prompt_tokens` → `gen_ai.usage.input_tokens`, `gen_ai.system` → `gen_ai.provider.name`). The backlog's `thread.id` is the OS-thread attribute, not a conversation id; GenAI's conversation attribute is `gen_ai.conversation.id`, but the registry page now marks it deprecated / moved to the GenAI conventions repo — verify the current name before citing it. The session / `group_id` correlation idea (OpenAI `Runner.run` without `RunConfig(group_id=)`, Langfuse / LangChain without `session_id`) belongs to rank 4 (run-level correlation id), which needs a run-call kwarg predicate and is tracked with the HTTP-entrypoint design ticket. Prior review 2026-10-02 (HEAD 2acaf88 + uncommitted working tree, branch `feat/p0-gap-10-fetch-allowlist`) — **P0 rank #10, fetch tool HTTPS-only (`schema_version` 19 → 20): 14 new rules, one per SSRF rule.** The host allow-list half of the gap already shipped as the allow-list credit on the 14 SSRF rules; what was missing was the scheme. New tool-scope predicate **`has_unpinned_scheme_url_call`** (a recognized HTTP call whose URL is dynamic and does not begin with a literal `https://`; Python via `analysis.URLArgSchemePinned`, TS via the new `url_scheme_unpinned` handler fact next to `dynamic_url`; f-string / template / `+` / `%` / `.format` forms with a leading `https://` are pinned). New rules, medium / 0.6: **CSDK-024/025, OAI-032/033, MCP-031/032, ADK-022/023, LC-026/LC-027, CREW-015, PYD-015, AG2-022, VAI-021** — each fires only when its SSRF sibling has been silenced by an allow-list and no scheme check (`.scheme ==`, `.protocol ===`, `startswith("https://")`) is present, so SSRF and HTTPS never fire on the same tool. **PYD-103** fix text now notes `WebFetchTool(allowed_domains=)` is Anthropic-only and not a substitute for egress controls. Not built: LangGraph nodes (state is not a model-chosen argument, no SSRF base rule), `raw_llm_sdk` (repo scope), a Pydantic `WebFetchTool` `allowed_domains` rule (provider-dependent), scheme validation in another function/module, fully literal `http://` URLs (not dynamic). Rule count **330** (316 + 14; re-derive). Prior review: 2026-10-01 (HEAD 1d24b98 + uncommitted working tree, branch `feat/p0-partial-gaps`) — **P0 partial-gaps batch (no new schema field; `schema_version` stays 19): wall-clock timeout extended to Google ADK and AutoGen; cost cap and general tool-output trust investigated and deliberately not shipped.** New rules **ADK-113** and **AG2-021** (run call with no wall-clock timeout) ride on the existing `agent_run_call_wall_clock_timeout_missing` predicate, widened to ADK and AutoGen: `DiscoverAgentRunCalls` now also discovers ADK `Runner.run` / `run_async` (runner variable resolved to its agent through a same-file `Runner(agent=)` / `InMemoryRunner(agent)` pre-pass; `run_live` and `app=`-built runners are not run calls here) and AutoGen `initiate_chat` / `a_initiate_chat` (receiver **and** recipient) / `run` / `a_run` / `run_stream`; the predicate also credits ADK `abort_signal` and AutoGen `cancellation_token` (non-None). **VAI-020** now also requires `timeout` to be absent (the Vercel AI SDK's native `timeout` option, number or `{ totalMs, stepMs, ... }`, was a false positive); Vercel needed no new TS discovery. **Per-run token/cost cap (P0 rank 8): no further rule.** Checked against upstream: OpenAI Agents (`Runner.run*`/`RunConfig`), LangChain/LangGraph (`max_iterations`, `ModelCallLimitMiddleware` counts calls not tokens), Google ADK (`RunConfig.max_llm_calls` counts calls) and Vercel AI (`maxOutputTokens` is per step) have no run-level token/cost parameter; CrewAI's `Agent.max_tokens` is declared but never read by the agent or executor (usage reaches only `TokenCalcHandler`, which tracks and does not enforce), so an absence rule would point at a no-op. AutoGen v0.4's `TokenUsageTermination` is a real primitive but is a composable termination object on team constructors, which are not discovered — not built. **Untrusted tool output (P0 rank 11): no general rule.** Framework loops return tool output through the structured tool-result channel (no user-code concatenation site), user-code `f"...{result}"` needs inter-procedural taint to call `result` a tool result, and a "delimiter present" check would reward cosmetic tags; RAW-001/002 stay the only statically defensible slice. Rule count **316** (314 + 2; re-derive). Prior review: **P0 remaining-gaps, engine batch (`schema_version` 18 → 19): six rules that needed new predicates or discovery.** **LC-114** (raw `StateGraph` compiled with no `checkpointer`; new `agent_kwargs_observed` + dual-scope `repo_langgraph_platform_config_present`, so it stays silent on unlinked/chained/opaque/subgraph compiles and on repos with a `langgraph.json`), **LC-115** (a function registered via `add_node` that shells out / executes code / writes / fetches a dynamic URL with no `interrupt(` — new `langgraph_node` ToolKind via `DiscoverLangGraphNodes`; same-file undecorated top-level functions only, confidence 0.45), **OAI-120** / **PYD-108** (run call with no wall-clock timeout — new `AgentRunCallDef.WallClockTimeoutWrapped` from a structural ancestor walk of `asyncio.wait_for` / `asyncio.timeout` / `anyio.move_on_after` / `anyio.fail_after`, new predicate `agent_run_call_wall_clock_timeout_missing`), and **RAW-001** / **RAW-002** (new `raw_llm_sdk` category, loaded unconditionally: the bare `anthropic` / `openai` client in a manual tool loop that builds a dynamic system prompt/message from tool-call-derived values — a deliberately narrow same-function heuristic via `DetectRawLLMToolOutputInSystem`). Rule count **314** (308 + 6; re-derive). Prior review 2026-09-30 (branch `feat/p0-remaining-gaps-batch1`, merged with Batch 2 on `main`) — **P0 remaining-gaps batch, rules-layer only (no discovery, predicate or `schema_version` change; stays 18).** New rules **CSDK-114** (TS `query()` main agent with inline `options` and no `options.maxBudgetUsd`), **CREW-111** (`Agent` with no `max_execution_time`, exempting `config=`), **LC-113** (Python `AgentExecutor` with no `max_execution_time`) and **VAI-020** (`generateText`/`streamText` with a `stopWhen`/`maxSteps` loop and no `abortSignal`; `ToolLoopAgent` excluded). The 14 SSRF rules (CSDK-009/013, OAI-018/024, LC-005/013, CREW-005, PYD-005, ADK-012/016, MCP-008/013, AG2-011, VAI-003) now **credit a host allow-list** in the tool body (`not: has_body_text`) instead of firing on any non-literal URL; IDs/severity/confidence unchanged. Deferred because they need new predicates or discovery, which this batch was barred from adding: OAI/PYD whole-run wall-clock deadline (needs a run-call deadline-wrapper fact), Python `ClaudeAgentOptions(max_budget_usd)`, LangGraph no-checkpointer and mutating-node-without-HITL (need a Platform/`langgraph.json` exemption, opaque-`compile()` handling and an `interrupt()` fact), LangChain.js `maxExecutionTime` (option not verified to exist), and untrusted-tool-output concat (no raw-API-loop discovery). Rule count **308** (302 + Batch 2's 6); re-derive with `grep -rhoE '^\s*-\s*id:\s*\S+' testdata/rules-fixture/*/*.yaml | wc -l`. Prior review 2026-09-29 (branch `feat/p0-side-effect-bounds-batch2`, off `main` e5cef47) — Batch 2 of the side-effect-bounds rule: **CREW-014** (CrewAI), **ADK-020** (Python) / **ADK-021** (TypeScript), **AG2-020** (AutoGen / AG2), **VAI-019** (Vercel AI SDK), and **CSDK-023** (Claude Agent SDK TypeScript) — same detection as Batch 1, adapted to each SDK's tool shape (one new `side_effect_bounds.yaml` per SDK; severity high, confidence 0.5). Approval gates differ per SDK and are honest about it: Vercel keys on `needsApproval` (as VAI-018); ADK Python on `require_confirmation` or a `request_confirmation(` call; ADK TypeScript has **no** declarative confirmation option on `FunctionTool`, so ADK-021 treats a `requestConfirmation(` call in the handler as the gate; CrewAI, AutoGen and Claude TS have no tool-level gate (`Task(human_input=True)`, `human_input_mode`, and `canUseTool`/`permissionMode` live on the task/agent/`query()` options, which a tool-scope rule cannot see), so those rules can fire on a tool gated elsewhere. One small discovery change: `DiscoverADKTools` now copies `FunctionTool(...)` keyword arguments into `ToolDef.Config` (previously dropped), which is what lets ADK-020 see `require_confirmation`. Known discovery limits carried into the rules: Vercel tools are matched only through a `const x = tool(...)` binding (inline `tools: { x: tool(...) }` has no name); Claude TS reads parameter names only from an inline object-literal schema; CrewAI `BaseTool` subclasses, autogen_core `FunctionTool`, and ADK bare functions in `tools=[...]` are not discovered. No schema/predicate/`schema_version` change. Rule count **304** (298 + 6; re-derive rather than trust this figure). Prior review 2026-09-29 (pending merge commit; `origin/main` merged into `feat/p0-side-effect-bounds-batch1`) — Batch 1 of the _Last reviewed: 2026-10-12 (branch `feat/langgraph-inmemory-checkpointer-server`, merged with `main` 44b2062) — **LC-116: in-memory LangGraph checkpointer run from a server entrypoint (`schema_version` 21 → 22; 1 rule, 2 predicates).** High / 0.6, agent scope, Python, `langchain_state_graph` + `langchain_agent`: `checkpointer=` resolves to `MemorySaver` / `InMemorySaver` (call, `MemorySaver(serde=...)`, aliased, or a same-file variable bound to one), a run call of that graph is reachable from a FastAPI / Flask / Celery / Dramatiq entrypoint, and the repo has no `langgraph.json`. New predicates `agent_checkpointer_in_memory`, `agent_server_reachable`; new in-memory fields `AgentDef.CheckpointerClass`, `AgentRunCallDef.AgentFilePath`. Run-call discovery now anchors LangGraph `invoke`/`ainvoke`/`stream`/`astream`/`astream_events`/`batch`/`abatch` on compiled-graph variables, the direct `builder.compile(...).invoke(...)` chain and prebuilt-agent variables only (never on an arbitrary `.invoke`), cross-file for module-level graphs. Test paths (`tests/`, `test_*.py`, `conftest.py`) never satisfy `agent_server_reachable`. **Not covered:** a helper of a helper; a graph built/compiled in a factory function; a saver chosen at runtime; `import graph; graph.app.invoke()`; Django/Starlette/RQ/serverless entrypoints; TypeScript. **Recall on real server repos is unmeasured:** the corpus has 40 run calls, 0 LangGraph, 0 server-reachable, and its full findings output (185) is identical before and after. Rule count **334** (333 + 1; re-derive). Prior review: 2026-10-11 (branch `feat/otel-labels-rules`, rebased onto 9fc2c39) — **OBS-006..008 credit fixes (no schema change, stays 21; no rule change).** Each file's own imports of agent-reliability-otel-labels are followed, so a renamed import (`mark_tool_span as mts`, `markToolSpan as label`) still counts. `trustabl.*` label keys written to a span by hand (`set_attribute` / `setAttribute` first argument, or a key of the literal passed to `set_attributes` / `setAttributes`) are recorded as `attr_literal` signals and credit the ladder rung by rung, so a repo that writes the labels without the library no longer draws OBS-006..008. Every signal carries `TestOrigin`; test-file signals still credit OBS-006..008. Rule counts re-derived: **333** fixture. Prior review 2026-10-07 (HEAD 8d0fb76 + uncommitted working tree, branch `feat/forge-runtime-tracing`) — **agent-reliability-otel-labels rules renumbered to `schema_version` 21.** The OBS-006..008 batch below was built in parallel with the HTTPS-only batch, and both took 19 → 20; after merging main this batch takes 20 → 21 (engine constant and both manifests). Rule counts re-derived: **333** fixture / **333** production (330 + OBS-006..008); `scripts/check-rules-sync.sh` passes. No coverage change. Prior review 2026-10-05 (HEAD d8a4f57 + uncommitted working tree, branch `feat/server-entrypoint-detection`) — **Server-entrypoint fact (Python; no schema change, stays 20; no rule, no predicate):** `RepoInventory.Entrypoints` (also `ScanResult.entrypoints`) records FastAPI / Flask route handlers (`http`) and Celery tasks / Dramatiq actors (`worker`), import-gated, skipping `examples/`, `demo/`, `docs/`, `samples/`, `cookbook/`, `tutorial(s)/`; `AgentRunCallDef.ServerReachable` is stamped in memory when a run call is in the entrypoint, a same-file helper, or an imported helper (one hop each; relative imports resolved within the scan set). Nothing consumes it yet; ranks 4 and 6 build on it in follow-up PRs. Deferred: Django, Starlette `Route()`, RQ, Temporal, serverless handlers, `add_api_route` / `add_url_rule`, class-based views, TypeScript. Corpus baseline: 2 entrypoints (Flask), 40 run calls, 0 stamped — all run calls are standalone scripts. Prior review 2026-10-05 (HEAD 05c44c9 + uncommitted working tree, branch `fix/obs-global-instrumentor-credit`) — **Observability false-positive fix: process-wide instrumentor calls are credited (no schema change; `schema_version` stays 20, no new rules, no fixture change).** PYD-107's fix text recommends `logfire.instrument_pydantic_ai()`, but its match (`repo_has_observability` + `agent_kwarg_missing: [instrument]`) never read that call, so following the fix still produced one finding per agent; LC-112's "configure a global handler" advice had the same gap. Crediting the call as repo-level init evidence alone cannot fix this (the kwarg check does not read init evidence) and would make it worse for `Agent.instrument_all()`, so the credit is agent-scoped: new `AgentDef.InstrumentedBy` (`instrumented_by` in the JSON report, omitted when unset), stamped by `analysis.ApplyGlobalInstrumentation` from the init signals, and `PredAgentKwargMissing` treats the exact kwarg it names as set. Covered: `logfire.instrument_pydantic_ai` and `Agent.instrument_all()` (→ `instrument`, Pydantic AI, Python), `LangChainInstrumentor().instrument()` (→ `callbacks`, LangChain, Python). Also recognized as init evidence but crediting no agent: `logfire.instrument_openai_agents`, `logfire.instrument_mcp`, any other `Instrumentor(...).instrument(...)`. `.instrument_all()` counts only in a file importing `pydantic_ai` or `logfire`; the instrumentor shape only in a file importing `openinference`, `opentelemetry` or `traceloop` (the two-step `i = XInstrumentor(); i.instrument()` is not matched). Behavior changes beyond PYD-107/LC-112: a repo whose only signal is one of these calls now counts as observed (the repo absence rules go quiet, OBS-001 stays silent). VAI-101 is unchanged because the Vercel AI SDK has no process-wide telemetry switch — `experimental_telemetry` is enabled per call. **Opik** added as a vendor (Python `opik` import, `opik.configure` / `OpikTracer` / `track_openai`; TS `opik` import, `new Opik()` / `OpikExporter` / `OpikCallbackHandler`; recon dep needle `opik`). Known risk: a repo that uses Opik only through the `@track` decorator plus env configuration imports `opik` but makes no init call, so it reads as imported-never-initialized and OBS-001 fires; Langfuse `@observe` already behaves the same way, and decorator detection (`@observe`, `@track`, `@traceable`) is deliberately deferred until a rule consumes it. **Observability backlog items investigated and deliberately not shipped.** **Tool result not recorded on a span (P0 rank 3): no rule.** Frameworks span every tool call once tracing is enabled (OpenAI Agents by default, Google ADK, LangChain `BaseTool`, Pydantic AI `instrument`, Vercel `experimental_telemetry`, CrewAI instrumentors), and enablement is often env- or CLI-only (LangSmith, Claude Code, ADK), so a repo-level "tracing present" gate cannot say whether a given tool is spanned. Unlike the HTTPS-only rules it is not same-body evidence, and a rule would fire on correctly traced tools. **Required span attributes (P0 rank 2): no rule.** In every audited SDK the attributes are written by the SDK or by the OTel / OpenInference instrumentor, so a well-instrumented repo contains no attribute literals and a static rule cannot tell "the instrumentor sets them" from "nobody sets them". The OTel GenAI semantic conventions are still Development stability and have renamed keys (`gen_ai.usage.prompt_tokens` → `gen_ai.usage.input_tokens`, `gen_ai.system` → `gen_ai.provider.name`). The backlog's `thread.id` is the OS-thread attribute, not a conversation id; GenAI's conversation attribute is `gen_ai.conversation.id`, but the registry page now marks it deprecated / moved to the GenAI conventions repo — verify the current name before citing it. The session / `group_id` correlation idea (OpenAI `Runner.run` without `RunConfig(group_id=)`, Langfuse / LangChain without `session_id`) belongs to rank 4 (run-level correlation id), which needs a run-call kwarg predicate and is tracked with the HTTP-entrypoint design ticket. Prior review 2026-10-04 (HEAD 4f8f6bd + uncommitted working tree, branch `feat/forge-runtime-tracing`) — **agent-reliability-otel-labels process-label rules (`schema_version` 20 → 21; recorded as 19 → 20 before main's HTTPS-only batch was merged).** New inventory pass `analysis.DiscoverOtelLabels` emits typed `OtelLabelsSignal` records (Kind `import` / `run_start` / `tool_span` / `run_end`) for Python and TypeScript/JavaScript; the Go binding is not inspected. Recon gains `RepoProfile.OtelLabelsDeclaredIn` (`agent-reliability-otel-labels` / `agent_reliability_otel_labels` in the Python root manifests, `@trustabl/agent-reliability-otel-labels` in `package.json`). Both are kept out of `ObservabilitySignals` / `ObsDeps` because the library never creates a span and must not satisfy its own prerequisite. Three new repo-scope predicates (`repo_otel_labels_installed`, `repo_otel_labels_initialized`, `repo_otel_labels_implemented`) back three new rules in `observability/quality.yaml`, all gated on `repo_observability_initialized: true` and mutually exclusive: **OBS-006** (instrumented, library neither declared nor imported; low), **OBS-007** (installed, no `mark_start`; medium), **OBS-008** (run started, no `mark_run_end`, or tools exist and no `mark_tool_span`; medium). They check that the calls exist, not that labels land or are correct: a missing current span makes every call a silent no-op, and a shared `Run`, a constant `attempt`, labelling outside `finally`, a `side_effect` that contradicts the tool body, and a missing `mark_handoff` are deferred. Rule counts at the time: 319 fixture / 319 production, before main's 14 HTTPS-only rules were merged (333 after). Prior review 2026-10-04 (HEAD 5ba476d, branch `feat/forge-runtime-tracing`) — **Recon now detects Google ADK for Go (`go.mod`: `google.golang.org/adk` → recon-only dep name `google-adk-go`, not mapped to an SDK for `scan`; used by `trustabl forge` to select the Go runtime-tracing recipe).** Prior review 2026-10-02 (HEAD 2acaf88 + uncommitted working tree, branch `feat/p0-gap-10-fetch-allowlist`) — **P0 rank #10, fetch tool HTTPS-only (`schema_version` 19 → 20): 14 new rules, one per SSRF rule.** The host allow-list half of the gap already shipped as the allow-list credit on the 14 SSRF rules; what was missing was the scheme. New tool-scope predicate **`has_unpinned_scheme_url_call`** (a recognized HTTP call whose URL is dynamic and does not begin with a literal `https://`; Python via `analysis.URLArgSchemePinned`, TS via the new `url_scheme_unpinned` handler fact next to `dynamic_url`; f-string / template / `+` / `%` / `.format` forms with a leading `https://` are pinned). New rules, medium / 0.6: **CSDK-024/025, OAI-032/033, MCP-031/032, ADK-022/023, LC-026/LC-027, CREW-015, PYD-015, AG2-022, VAI-021** — each fires only when its SSRF sibling has been silenced by an allow-list and no scheme check (`.scheme ==`, `.protocol ===`, `startswith("https://")`) is present, so SSRF and HTTPS never fire on the same tool. **PYD-103** fix text now notes `WebFetchTool(allowed_domains=)` is Anthropic-only and not a substitute for egress controls. Not built: LangGraph nodes (state is not a model-chosen argument, no SSRF base rule), `raw_llm_sdk` (repo scope), a Pydantic `WebFetchTool` `allowed_domains` rule (provider-dependent), scheme validation in another function/module, fully literal `http://` URLs (not dynamic). Rule count **330** (316 + 14; re-derive). Prior review: 2026-10-01 (HEAD 1d24b98 + uncommitted working tree, branch `feat/p0-partial-gaps`) — **P0 partial-gaps batch (no new schema field; `schema_version` stays 19): wall-clock timeout extended to Google ADK and AutoGen; cost cap and general tool-output trust investigated and deliberately not shipped.** New rules **ADK-113** and **AG2-021** (run call with no wall-clock timeout) ride on the existing `agent_run_call_wall_clock_timeout_missing` predicate, widened to ADK and AutoGen: `DiscoverAgentRunCalls` now also discovers ADK `Runner.run` / `run_async` (runner variable resolved to its agent through a same-file `Runner(agent=)` / `InMemoryRunner(agent)` pre-pass; `run_live` and `app=`-built runners are not run calls here) and AutoGen `initiate_chat` / `a_initiate_chat` (receiver **and** recipient) / `run` / `a_run` / `run_stream`; the predicate also credits ADK `abort_signal` and AutoGen `cancellation_token` (non-None). **VAI-020** now also requires `timeout` to be absent (the Vercel AI SDK's native `timeout` option, number or `{ totalMs, stepMs, ... }`, was a false positive); Vercel needed no new TS discovery. **Per-run token/cost cap (P0 rank 8): no further rule.** Checked against upstream: OpenAI Agents (`Runner.run*`/`RunConfig`), LangChain/LangGraph (`max_iterations`, `ModelCallLimitMiddleware` counts calls not tokens), Google ADK (`RunConfig.max_llm_calls` counts calls) and Vercel AI (`maxOutputTokens` is per step) have no run-level token/cost parameter; CrewAI's `Agent.max_tokens` is declared but never read by the agent or executor (usage reaches only `TokenCalcHandler`, which tracks and does not enforce), so an absence rule would point at a no-op. AutoGen v0.4's `TokenUsageTermination` is a real primitive but is a composable termination object on team constructors, which are not discovered — not built. **Untrusted tool output (P0 rank 11): no general rule.** Framework loops return tool output through the structured tool-result channel (no user-code concatenation site), user-code `f"...{result}"` needs inter-procedural taint to call `result` a tool result, and a "delimiter present" check would reward cosmetic tags; RAW-001/002 stay the only statically defensible slice. Rule count **316** (314 + 2; re-derive). Prior review: **P0 remaining-gaps, engine batch (`schema_version` 18 → 19): six rules that needed new predicates or discovery.** **LC-114** (raw `StateGraph` compiled with no `checkpointer`; new `agent_kwargs_observed` + dual-scope `repo_langgraph_platform_config_present`, so it stays silent on unlinked/chained/opaque/subgraph compiles and on repos with a `langgraph.json`), **LC-115** (a function registered via `add_node` that shells out / executes code / writes / fetches a dynamic URL with no `interrupt(` — new `langgraph_node` ToolKind via `DiscoverLangGraphNodes`; same-file undecorated top-level functions only, confidence 0.45), **OAI-120** / **PYD-108** (run call with no wall-clock timeout — new `AgentRunCallDef.WallClockTimeoutWrapped` from a structural ancestor walk of `asyncio.wait_for` / `asyncio.timeout` / `anyio.move_on_after` / `anyio.fail_after`, new predicate `agent_run_call_wall_clock_timeout_missing`), and **RAW-001** / **RAW-002** (new `raw_llm_sdk` category, loaded unconditionally: the bare `anthropic` / `openai` client in a manual tool loop that builds a dynamic system prompt/message from tool-call-derived values — a deliberately narrow same-function heuristic via `DetectRawLLMToolOutputInSystem`). Rule count **314** (308 + 6; re-derive). Prior review 2026-09-30 (branch `feat/p0-remaining-gaps-batch1`, merged with Batch 2 on `main`) — **P0 remaining-gaps batch, rules-layer only (no discovery, predicate or `schema_version` change; stays 18).** New rules **CSDK-114** (TS `query()` main agent with inline `options` and no `options.maxBudgetUsd`), **CREW-111** (`Agent` with no `max_execution_time`, exempting `config=`), **LC-113** (Python `AgentExecutor` with no `max_execution_time`) and **VAI-020** (`generateText`/`streamText` with a `stopWhen`/`maxSteps` loop and no `abortSignal`; `ToolLoopAgent` excluded). The 14 SSRF rules (CSDK-009/013, OAI-018/024, LC-005/013, CREW-005, PYD-005, ADK-012/016, MCP-008/013, AG2-011, VAI-003) now **credit a host allow-list** in the tool body (`not: has_body_text`) instead of firing on any non-literal URL; IDs/severity/confidence unchanged. Deferred because they need new predicates or discovery, which this batch was barred from adding: OAI/PYD whole-run wall-clock deadline (needs a run-call deadline-wrapper fact), Python `ClaudeAgentOptions(max_budget_usd)`, LangGraph no-checkpointer and mutating-node-without-HITL (need a Platform/`langgraph.json` exemption, opaque-`compile()` handling and an `interrupt()` fact), LangChain.js `maxExecutionTime` (option not verified to exist), and untrusted-tool-output concat (no raw-API-loop discovery). Rule count **308** (302 + Batch 2's 6); re-derive with `grep -rhoE '^\s*-\s*id:\s*\S+' testdata/rules-fixture/*/*.yaml | wc -l`. Prior review 2026-09-29 (branch `feat/p0-side-effect-bounds-batch2`, off `main` e5cef47) — Batch 2 of the side-effect-bounds rule: **CREW-014** (CrewAI), **ADK-020** (Python) / **ADK-021** (TypeScript), **AG2-020** (AutoGen / AG2), **VAI-019** (Vercel AI SDK), and **CSDK-023** (Claude Agent SDK TypeScript) — same detection as Batch 1, adapted to each SDK's tool shape (one new `side_effect_bounds.yaml` per SDK; severity high, confidence 0.5). Approval gates differ per SDK and are honest about it: Vercel keys on `needsApproval` (as VAI-018); ADK Python on `require_confirmation` or a `request_confirmation(` call; ADK TypeScript has **no** declarative confirmation option on `FunctionTool`, so ADK-021 treats a `requestConfirmation(` call in the handler as the gate; CrewAI, AutoGen and Claude TS have no tool-level gate (`Task(human_input=True)`, `human_input_mode`, and `canUseTool`/`permissionMode` live on the task/agent/`query()` options, which a tool-scope rule cannot see), so those rules can fire on a tool gated elsewhere. One small discovery change: `DiscoverADKTools` now copies `FunctionTool(...)` keyword arguments into `ToolDef.Config` (previously dropped), which is what lets ADK-020 see `require_confirmation`. Known discovery limits carried into the rules: Vercel tools are matched only through a `const x = tool(...)` binding (inline `tools: { x: tool(...) }` has no name); Claude TS reads parameter names only from an inline object-literal schema; CrewAI `BaseTool` subclasses, autogen_core `FunctionTool`, and ADK bare functions in `tools=[...]` are not discovered. No schema/predicate/`schema_version` change. Rule count **304** (298 + 6; re-derive rather than trust this figure). Prior review 2026-09-29 (pending merge commit; `origin/main` merged into `feat/p0-side-effect-bounds-batch1`) — Batch 1 of the side-effect-bounds rule (competitive-analysis backlog rank 12, P0): new rules **OAI-030** (Python), **OAI-031** (TypeScript), **PYD-014**, **MCP-030**, and **LC-025** — each flags a send/notify or refund/charge/pay/payout/transfer/ issue tool whose recipient or amount parameter is free-form model input with no visible bound (no schema constraint, no `MAX_`/`ALLOWED`/allow-list marker in the body) and no SDK-native approval gate (`needs_approval`/`needsApproval` for OAI, `requires_approval` for PYD, `interrupt(` for LC, `ctx.elicit(` for MCP). Distinct from the idempotency family (OAI-009/019, PYD-007, MCP-007, LC-017): a duplicate call repeats an action within a bound the caller already accepted, this rule is about a single call moving further than the caller intended. Severity high, confidence 0.5 — the pack's floor, tied with OAI-019 — deliberately below the idempotency family's 0.55/0.5 (see the confidence-gap section in `openai_sdk/side_effect_bounds.md` in the rulebook for the full defense). Rules-only; no schema/predicate/`schema_version` change (existing predicates `name_has_prefix`, `param_name_matches`, `has_body_text`, `tool_decorator_kwarg_present`/`value` cover it). CSDK Python is a deliberate, documented gap for this rule: the idiomatic `@tool("name", "desc", {"to": str})` + `async def fn(args)` shape gives discovery `param_names=["args"]`, so a static parameter-name match can never see `to` or `amount` — CSDK-006 (idempotency) has the identical blind spot already. Batch 2 followed (see above). Rule count after merging main's observability packs: **298** (Batch 1 alone was 282; re-derive with `grep -rhoE '^\s*-\s*id:\s*\S+' testdata/rules-fixture/*/*.yaml | wc -l`, don't trust a cached figure). Prior review 2026-09-24 (branch `feat/observability-detection`, on top of HEAD 647f81e) — **agent-observability Phase 1 review fixups**: (1) added `analysis.DiscoverAgentObservabilitySignals`, wired via a new `analysis.MergeObservabilitySignals` alongside the existing file-level pass — it reads `AgentDef.Kwargs` for the three SDKs whose "traces are on" evidence lives on the agent constructor rather than a standalone init call (Pydantic AI `instrument=True`, Vercel `experimental_telemetry`, LangChain `callbacks=`) and emits the `instrument_kwarg` signal `repo_observability_initialized`'s own doc comment already claimed to honor but that nothing previously produced — closing a false positive where an instrumented-via-kwarg-only Pydantic AI project misfired OBS-001; (2) gated the Python bare (receiver-less) init-callee names `init_logger` / `autolog` on the corresponding vendor import, mirroring the TS side's existing import gate — closes a false-negative where a project's own unrelated `init_logger()` helper silently suppressed all nine absence rules; (3) `OBS-002` / `OBS-003` now set `language: python` — their exporter-construction and content-capture detection (`pyObservabilitySignals`) has no TS/JS counterpart yet, so the rules no longer claim TS/JS coverage (via `vercel_ai` etc. in `applies_to`) that does not exist; (4) removed the half-wired `helicone` vendor (the `detectObsDeps` needle and `models.VendorHelicone`) — recon could detect the dependency but no AST pass ever produced a code signal for it, so any real Helicone user was permanently and unfixably flagged by OBS-005 as "declared but never wired." Deferred to whichever phase implements the base-URL-literal detection the design doc sketches. No schema/predicate/`schema_version` change — all four are discovery-logic and rule-metadata fixes on top of the Phase 1 rules landed below. Prior review 2026-09-21 (HEAD fbc5de0) — **agent-observability detection (Phase 1, static wiring)**: new inventory pass `analysis.DiscoverObservability` emits typed `ObservabilitySignal` records (vendor × Kind, where Kind grades evidence as import / init / exporter / content_capture / instrument_kwarg) across Python and TypeScript/JavaScript only — Go, C#, PHP and Rust are NOT inspected, and the new `repo_observability_inspectable` predicate is what stops an absence rule firing there on an empty result. Recon gains a separate `RepoProfile.ObsDeps` needle scan (deliberately not folded into `SDKDeps`, which would make META-002 fire forever on any repo declaring langfuse). Seven new repo-scope predicates; `repo_has_observability` is the catalog's first **dual-scope** predicate (repo + agent), which required relaxing `TestPredicatesByScope_MirrorsStruct` to a named `dualScopePredicates` allow-list. 16 new rules (`schema_version` 17 → 18): nine repo-scope absence rules at severity low (OAI-203, CSDK-208, LC-202, PYD-202, VAI-201, CREW-202, ADK-202, AG2-202, MCP-202), three agent-scope outlier rules gated on the repo already being instrumented (PYD-107, LC-112, VAI-101), and a new `observability` category holding cross-SDK quality rules OBS-001 (imported but never initialized, medium), OBS-002 (console-only exporter, low) and OBS-003 (full prompt/response capture, medium) and OBS-005 (an observability package declared in a hand-edited manifest that no code imports, medium — the one predicate reading recon's `RepoProfile.ObsDeps` rather than code signals, with `poetry.lock` excluded because a transitive entry proves nobody's intent; the nine absence rules gained `repo_observability_declared: false` so "shipped a tracing dep and never wired it" is medium while "never tried" stays low). The `observability` category loads unconditionally in `LoadFor` like `openshell`/`claude_skill` — it is not an SDK, so SDK-gating would drop the pack entirely; that gap was caught by an end-to-end CLI run, not by the per-rule tests. Runtime trace ingestion is Phase 2 and is NOT implemented. Rule counts re-derived: 292 fixture / 292 production. Prior review 2026-09-21 (pending commit) — outreach-feedback investigation (Aug–Sep 2026 batch) fixed two confirmed rule bugs. **CSKILL-080/081** (claude_skill/skill_quality_text.yaml) moved from raw-substring `skill_*_has_text` matching onto a new sentence-scoped, word-boundary predicate, `skill_text_matches` (`schema_version` 17 → 18), closing confirmed false positives: a skill documenting "API keys live in GitHub Actions secrets" (named a data class, didn't handle it — closed via `require_context` requiring an operational verb in the same sentence) and a skill listing "hash a string" as a worked example (closed via `exclude_context`). CSKILL-082..087 still use the raw-substring predicates unmodified — no confirmed FP against them yet. Separately, **CSDK-201/202** (bypassPermissions) were reported as under-scoped for not checking `allowed_tools` at the same site; that premise was refuted against the Agent SDK's own docs (`allowed_tools` only auto-approves, it does not restrict — an empty/narrow allow-list next to `bypassPermissions` is the *most* dangerous shape, not a mitigated one — see `docs/decisions/tool-allowlist-scope.md`), and the fix was retargeted onto the two mechanisms that genuinely do restrict: **CSDK-202** now uses a new per-construction-site correlation predicate, `repo_claude_options_mode_without_kwarg`, firing only when a `bypassPermissions` `ClaudeAgentOptions(...)` lacks `disallowed_tools` at that same site (fixing a repo-wide-vs-per-site gap in the process); new rule **CSDK-206** (medium/0.6) fires the complementary case, `bypassPermissions` paired with a deny-list that may still leave a broad surface. **CSDK-103** (Python) / **CSDK-120** (TypeScript) `AgentDefinition` rules gained a co-check on `tools` — sound here because, unlike `allowed_tools`, `AgentDefinition.tools` genuinely restricts the agent — going silent on a genuinely read-only tools list, rules-only, no schema change. Rule count: **277** (was 210 as of the last figure recorded in `CLAUDE.md`, which itself predates a large unrelated rules-repo pull that added `autogen`, `network`, `observability`, and `tool_definition` packs across most SDK categories — re-derive, don't trust a cached figure, per `grep -rhoE '^\s*-\s*id:\s*\S+' testdata/rules-fixture/*/*.yaml | wc -l`). Prior review 2026-09-15 (HEAD 21a806f) — new rules **OAI-118** (Python) and **OAI-119** (TypeScript): an OpenAI Agents SDK agent that wires an MCP server (`MCPServerStdio`/`MCPServerSse`/`MCPServerStreamableHttp`) with no `tool_filter`/`toolFilter` — the self-hosted sibling of OAI-115/116's `HostedMCPTool`/`hostedMcpTool` allow-list checks, both severity high / confidence 0.75. Required a discovery fix first: Python MCP server construction (`classifyMCPServerCall`, the `async with X() as srv:` alias path, and the alias-to-def copy in `ResolveEdges`) now captures constructor kwargs onto `MCPServerDef.Kwargs` — previously nil by design ("not captured at v1"); TS already captured them. New predicate `agent_mcp_server_kwarg_missing` (`schema_version` 15 → 16) takes a `classes:` list rather than one `class:` like `agent_hosted_tool_kwarg_present`, because all three MCP server classes share `tool_filter` as the same allow-list mechanism — one rule per language, not one per class. Closes the last of the three follow-ups in `docs/decisions/tool-allowlist-scope.md`. Prior review 2026-09-10 (HEAD 05bcd9e) — new rule **OAI-117**, a second and independent TypeScript sibling alongside OAI-116 on the same `hostedMcpTool({...})` construct: fires when `requireApproval` is omitted from the options, severity medium / confidence 0.65 (below OAI-115/116 — the mechanism is certain, verified directly in `openai-agents-js`'s `tool.ts` (both the `serverUrl` and `connectorId` branches inject `require_approval: 'never'` when the option is `undefined`), but the predicate cannot distinguish negligent omission from a deliberately read-only MCP server where `'never'` is the correct setting). Does not check `allowedTools` and is not combined with OAI-116 — an agent can set one control and miss the other, and a `hostedMcpTool(...)` missing both draws both findings. Fires on omission only, not an explicit `requireApproval: 'never'` (a reviewable choice OAI-116's own fix text recommends). Re-verified against `openai-agents-python` that Python has no equivalent gap (no SDK-level default injection in `tool.py`; the Responses API's `Mcp` TypedDict has no default either) — no parallel Python rule. Rules-only; no schema/predicate/`schema_version` change. Closes the `require_approval` / `requireApproval` follow-up left open by the prior OAI-116 review in `docs/decisions/tool-allowlist-scope.md`. Prior review 2026-09-09 (HEAD 1fb7d38) — new rule **OAI-116**, the TypeScript sibling of OAI-115: a `hostedMcpTool({...})` factory call (`@openai/agents`) with no `allowedTools` allow-list, severity high / confidence 0.75 (one notch above OAI-115's 0.7 — the JS SDK injects `require_approval: 'never'` when `requireApproval` is omitted, foreclosing the platform-default approval fallback that justifies OAI-115's discount). Rules-only, built on the TS hosted-tool kwarg capture from the prior review; no schema/predicate/`schema_version` change. Closes the OpenAI Agents SDK half of Class 1 in `docs/decisions/tool-allowlist-scope.md`. Prior review 2026-09-07 (HEAD d9c8dc6) — TS OpenAI hosted-tool factory calls (`hostedMcpTool({...})`, `webSearchTool({...})`, etc.) now capture their options-object kwargs the same way the Python `HostedMCPTool(tool_config={...})` path already did, and `ResolveEdges` materializes the discovery-built def (precise factory-call `Location` + `Kwargs`) instead of synthesizing a bare one at the agent's line. Discovery-only, no rule/schema change — unblocks the OpenAI Agents SDK half of Class 1 in `docs/decisions/tool-allowlist-scope.md`, which also now records the verified flat `hostedMcpTool` options shape (no `toolConfig` wrapper, unlike Python). TS ADK and Vercel AI hosted-tool refs are unchanged (still agent-line Location, no kwargs) — a documented follow-up, not done here. Prior review 2026-09-01 — new rule CSDK-205, Claude SDK repo-scope: flags a `ClaudeAgentOptions(...)` session that sets `permission_mode="acceptEdits"` with no `disallowed_tools` deny-list — Claude SDK's `allowed_tools` only auto-approves, it never restricts, so the risk signal is a missing deny-list paired with a permissive mode, not an empty allow-list. Discovery-free: kwargs were already captured generically, so this is a rules-layer-only change (`schema_version` 15). Closes the Claude SDK half of Class 1 in `docs/decisions/tool-allowlist-scope.md`; the OpenAI Agents SDK half remains unresearched. Prior review 2026-08-24 — docs catch-up for execution-limit rules that already ship: OAI-112 `Runner.run`/`run_sync`/`run_streamed` with no `max_turns`, PYD-106 `agent.run`/`run_sync`/`run_stream` with no `usage_limits` — both via `DiscoverAgentRunCalls` in `internal/analysis/agent_run_calls.go`; CSDK-204 no `ClaudeAgentOptions.max_turns`; CREW-110 `Agent` with no `max_iter`. TypeScript Claude row also lists CSDK-121..124, which the 2026-07-07 note already claimed. PYD-104 also shipped in that window: new `agent_file_url_force_download` predicate with FileUrl-family / `WebFetchTool` `force_download` discovery. Prior review 2026-08-10 — NVIDIA NeMo Agent Toolkit recognized at recon only — new `nvidia-nat` dep needle in `detectSDKDeps`, catching bracketed extras like `nvidia-nat[langchain]`; no discovery pass, no rules, and deliberately not wired into `scanner.depNameToSDK` — that would emit a permanent false META-002 with no discovery to ever satisfy it. Prior review 2026-07-07 — rules audit corrections — ADK shell-tool class rename support (`ExecuteBashTool` alongside `BashTool` in discovery + ADK-008/102/103) and the `url_context`/`load_web_page` instance forms in ADK-110; four new TypeScript Claude agent rules CSDK-121..124 (AgentDefinition tool grants — Bash / WebSearch / write built-ins / WebFetch), closing the unaudited TS AgentDefinition gap; harmonized network-timeout callee lists across the six Python packs (Session aliases, urllib both forms, aliased aiohttp); repo-hygiene rules CSDK-203/OAI-202/ADK-201 no longer python-gated so TS-only repos get them; loader now skips the reserved `mappings/` pack subtree. Prior review 2026-06-14 — LangGraph: raw `StateGraph` graph discovery — the imperative `StateGraph(...)` → `add_node` / `add_edge` → `compile()` builder is now discovered as one `AgentDef` (Class `StateGraph`, SDK `langchain`), closing the "hand-wired graph finds nothing" gap; discovery-only — `langchain_state_graph` rules, `ToolNode`/`bind_tools` tool-edge resolution, and the TS equivalent are pending. Prior review 2026-06-08 — Agent Skills: enriched SKILL.md discovery — body-fact + bundled-file inventory, plus bundled-file content-scanning for egress/credential reads and committed secret literals, and hidden-Unicode detection (Tags-block U+E0000–E007F + bidi overrides) — and the `claude_skill` CSKILL-* rule pack (CSKILL-001/002/003/010/011/020/030/040/050/060 — 060 = description-vs-tools mismatch; `schema_version` 12; mapped to the OWASP Agentic Skills Top 10 / ASI in the rulebook) at the `skill` scope; plus a deterministic repo-wide **dependency BOM** (`ScanResult.Dependencies` parsed from declared manifests across all six languages — pip/npm/Go/Composer/NuGet/Cargo; optional CycloneDX `--bom-out`; pure inventory by default, TR-278, supersedes the skill-only TR-221) **plus an opt-in `--vuln-scan` layer that matches the repo's concretely-pinned deps against a pinned OSV snapshot and emits CVE/GHSA findings with CVSS-derived severity + fixed version (`internal/vulndb`; `trustabl vulndb pull` pre-warms the cache; snapshot version folded into `ScanID` only when enabled; TR-271)**; prior 2026-06-07 review covered JavaScript `.js`/`.jsx`/`.mjs`/`.cjs` via the shared TypeScript-family pipeline and Go/C#/PHP/Rust MCP discovery with field-based rules MCP-015..022)._ > **Note:** Detection rules are not shipped in the binary. They live in the > separate `agent-reliability-rules` git repository > (`https://github.com/trustabl/agent-reliability-rules`) and are resolved at scan > time (cached locally, with offline fallback). The rule IDs and packs listed > below describe the rules Trustabl currently ships in that repository; the SDK > and language *recognition* surface (scanning + AST discovery) is what the > engine binary provides. > **JavaScript:** every TypeScript row below applies to JavaScript too. > `.js` / `.jsx` / `.mjs` / `.cjs` are parsed through the same > TypeScript-family pipeline (the tsx grammar parses plain JS), discovered by > the same passes, tagged `javascript`, and audited by the `language: > typescript` rule packs via the TS/JS family gate (`models.IsTSOrJS`). > Both ES-module `import`s and CommonJS `require()` bindings are recognized. ## Coverage matrix Legend: ✅ full · ◐ partial · ❌ none · — N/A | SDK | Language | Scanning | Analysis (AST discovery) | Detection rules | |---|---|---|---|---| | **Claude Agent SDK** | Python | ✅ dep-scan + file inventory + `.claude/` & `.claude-plugin/` components | ✅ tools, agents, subagents (canonical + flat-collection shape fallback), skills (`SKILL.md`), slash commands, plugin manifests, settings, `ClaudeAgentOptions` session config | ✅ tool CSDK-001..009, 107, 108 (008 = `**kwargs` without input_schema, 009 = SSRF, 024 = allow-listed host without pinned https://); agent CSDK-101..105; subagent CSDK-110, 111 (fire on pure-markdown collections); repo CSDK-201..205 (`defaultMode` / `permission_mode` bypass; 203 = SDK code without an agent-guidance doc: AGENTS.md/CLAUDE.md; 204 = no explicit `max_turns` on `ClaudeAgentOptions`; 205 = `acceptEdits` with no `disallowed_tools` deny-list); skill CSKILL-001 (unrestricted shell), 002/003 (dynamic-context exec + egress/secret), 010/011 (bundled-script egress + credential-read), 030 (hardcoded secret in bundled file), 020 (external-URL fetch), 040 (injection markers incl. hidden-Unicode), 050 (model-invocable + side-effecting), 060 (description claims read-only but grants side-effecting tools) | | **Claude Agent SDK** | TypeScript | ✅ dep-scan (`@anthropic-ai/claude-agent-sdk`) + file inventory + `.claude/` components | ✅ tools (`tool()` factory), agents (main thread `QueryMainAgent` per `query()` call + sub-agents inline-in-query + typed-const `AgentDefinition`), MCP servers (createSdkMcpServer + 4 config literals) | ◐ tool CSDK-010 (shell), 011 (eval/new Function), 012 (fs-write), 013 (SSRF / dynamic URL), 025 (allow-listed host, https:// not pinned), 014 (no description), 016 (mutating tool no idempotency key), CSDK-023 (side-effecting tool with a free-form recipient/amount and no visible bound; no tool-level gate exists to check); agent CSDK-120 (permissionMode bypass), 121..124 (`AgentDefinition` grants Bash / WebSearch / write built-ins / WebFetch), 130 (`query()` main agent grants Bash), 131 (`query()` main agent grants write/fetch built-ins), 114 (`query()` main agent with no `options.maxBudgetUsd`); META-004 no longer fires | | **OpenAI Agents SDK** | Python | ✅ dep-scan + file inventory | ✅ tools, hosted tools (11 classes), agents, MCP servers (3 transports + alias, kwargs captured incl. `tool_filter`), guardrails, sessions, `Runner.run`/`run_sync`/`run_streamed` call sites (`max_turns`) | ✅ tool OAI-001..015, 018 (018 = SSRF / caller-controlled URL), 032 (allow-listed host, https:// not pinned), 030 (side-effecting tool with a free-form recipient/amount and no visible bound); agent OAI-101..104, 106, 109, 110, 111, 112 (`Runner.run` family with no `max_turns`), 120 (run call with no wall-clock timeout), 115 (`HostedMCPTool` with no `allowed_tools`), 118 (MCP server with no `tool_filter`); repo OAI-201, 202 (202 = SDK code without an agent-guidance doc: AGENTS.md/CLAUDE.md) | | **OpenAI Agents SDK** | TypeScript | ✅ dep-scan (`@openai/agents` substring catches `-core` / `-openai`) + file inventory | ✅ tools (`tool({...})` factory), agents (`new Agent({...})` + `Agent.create(...)`), hosted tools (9 factories across `@openai/agents-core` and `@openai/agents-openai`), MCP servers (3 transports + `MCPServers` wrapper, kwargs captured incl. `toolFilter`), guardrails (4 `defineX` factories), sessions (`MemorySession` / `OpenAIConversationsSession` / `OpenAIResponsesCompactionSession`) | ✅ tool OAI-016 (fetch without AbortSignal timeout), OAI-017 (eval / new Function), OAI-019 (mutating tool without idempotency), OAI-022 (no description), OAI-024 (dynamic URL / SSRF), OAI-033 (allow-listed host, https:// not pinned), OAI-031 (side-effecting tool with a free-form recipient/amount and no visible bound — the TS sibling of the Python row's OAI-030); agent OAI-105 (content hosted-tool without inputGuardrails), OAI-116 (`hostedMcpTool` with no `allowedTools` allow-list), OAI-117 (`hostedMcpTool` with no `requireApproval`), OAI-119 (MCP server with no `toolFilter`); all with fire/silent cases in the per-rule harness | | **MCP** | Python | ✅ tool registrations + config files | ◐ tool registrations only (no server-side resource/prompt discovery) | ✅ dedicated `mcp/` pack: tool MCP-001..010 (001 no description, 002 untyped params, 003 ambiguous name, 004 network timeout, 005 path safety, 006 error contract, 007 idempotency, 008 SSRF, 031 allow-listed host without pinned https://, 009 code-exec, 010 shell), MCP-030 (side-effecting tool with a free-form recipient/amount and no visible bound). `mcp_tool` coverage now lives ONLY in this pack — stripped from the CSDK rules' `applies_to` so a pure-MCP repo is covered and a mixed Claude+MCP repo does not double-fire | | **MCP** | TypeScript | ✅ dep-scan (`@modelcontextprotocol/sdk`) + file inventory | ✅ server authoring: `new McpServer(...)` receiver tracked, `registerTool` / legacy `tool` registrations → `KindMCPTool` (reuses `tsZodParamNames` / `tsHandlerFacts`). Distinct from the Claude client-config `createSdkMcpServer` discovery. Low-level `Server` + `setRequestHandler` not extracted (gap) | ✅ tool MCP-011..014 (011 no description, 012 shell, 013 SSRF, MCP-032 allow-listed host without pinned https://, 014 eval / new Function); shared `mcp/` pack, `language: typescript` | | **MCP** | Go | ✅ dep-scan (`go.mod`: mark3labs/mcp-go, official go-sdk, metoro-io/mcp-golang) + file inventory | ◐ tools (`mcp.NewTool("n", mcp.WithDescription(...), mcp.WithString(...))` — mark3labs, full name/desc/params; `mcp.AddTool(server, &mcp.Tool{Name, Description}, fn)` — official go-sdk, name/desc). metoro `RegisterTool`, the official handler-struct param schema, and the `s.AddTool` registration edge are v1 gaps | ◐ field-based language:go: MCP-015 (no description), MCP-016 (ambiguous name). Body-fact rules (shell/SSRF/timeout) need Go AST predicate branches (fast-follow); untyped-params is N/A (Go is statically typed) | | **MCP** | C#/.NET | ✅ dep-scan (`ModelContextProtocol` in `Directory.Packages.props` / `packages.config`; variable-named `.csproj` is a best-effort gap) + file inventory | ◐ tools (`[McpServerTool]`-attributed methods, official ModelContextProtocol SDK; name = method name, description from a co-located `[Description(...)]`, typed params). `[McpServerTool(Name=...)]` override, Semantic Kernel `[KernelFunction]`, and AutoGen `[Function]` are gaps | ◐ field-based language:csharp: MCP-017 (no description), MCP-018 (ambiguous name). Body-fact rules need C# AST predicate branches (fast-follow); untyped-params N/A (C# is statically typed) | | **MCP** | PHP | ✅ dep-scan (`mcp/sdk` / `php-mcp/server` in `composer.json`) + file inventory | ◐ tools (`#[McpTool]`-attributed methods, official mcp/sdk + community php-mcp/server; name from the `name:` arg or the method name, description from the `description:` arg, params + typed-params from the signature). The smacker tree-sitter-php grammar parses single-line `#[...]` as a comment, so the attribute is read from comment text; multi-line attributes, `#[McpResource]` / `#[McpPrompt]`, and the `#[McpTool(Name=...)]`-style class registration are gaps | ◐ field-based language:php: MCP-019 (no description), MCP-020 (ambiguous name). Body-fact rules need PHP AST predicate branches (fast-follow); untyped-params is a fast-follow too — discovery already captures `HasTypedParams` (PHP type hints are optional, so unlike Go/C# the check is meaningful), the rule is just not shipped yet | | **MCP** | Rust | ✅ dep-scan (`rmcp` in `Cargo.toml` / `Cargo.lock`) + file inventory | ◐ tools (`#[tool]`-attributed methods, official rmcp crate; name from the `name = "..."` arg or the method name, description from the `description = "..."` arg **or** the method's `///` doc comment, typed params). `#[tool]` is a real `attribute_item` node (no comment hack). Raw-string descriptions, `#[tool(name=...)]` on free functions outside an impl, and `#[prompt]` / resource shapes are gaps | ◐ field-based language:rust: MCP-021 (no description, doc-comment-aware), MCP-022 (ambiguous name). Body-fact rules need Rust AST predicate branches (fast-follow); untyped-params N/A (Rust is statically typed; the param schema lives in a separate `#[derive(JsonSchema)]` struct) | | **Google ADK** | Python | ✅ dep-scan (`google-adk`) + file inventory | ✅ LlmAgent (+ Agent alias), SequentialAgent, ParallelAgent, LoopAgent, LanggraphAgent; FunctionTool wrapping; 13 built-in hosted tools; sub_agents edges | ✅ tool ADK-001..007, 009..012 (009 = print to stdout, 010 = subprocess, 011 = eval/exec/compile, 012 = SSRF), ADK-022 (allow-listed host, https:// not pinned), ADK-020 (side-effecting tool with a free-form recipient/amount and no visible bound, no `require_confirmation` / `request_confirmation(`); agent ADK-008, 101..108, 110..111, ADK-113 (no `asyncio.timeout`/`wait_for`/`abort_signal` around the `Runner.run`/`run_async` that executes the agent; runner → agent resolved same-file via `Runner(agent=)` / `InMemoryRunner(agent)`); repo ADK-201 (SDK code without an agent-guidance doc: AGENTS.md/CLAUDE.md) | | **Google ADK** | TypeScript | ✅ dep-scan (`@google/adk`) + file inventory | ✅ tools (`new FunctionTool({...})`), agents (5 constructors: LlmAgent + SequentialAgent + ParallelAgent + LoopAgent + RoutedAgent), hosted tools (13 classes), subAgents edges | ◐ tool ADK-013 (no description), 015 (eval / new Function), 016 (SSRF / dynamic URL), 023 (allow-listed host, https:// not pinned), 021 (side-effecting tool with a free-form recipient/amount and no visible bound, no `requestConfirmation(` in the handler); agent ADK-109 (LlmAgent no description); first ADK TS pack; META-004 no longer fires | | **Google ADK** | Go | ◐ dep-scan only (`go.mod`: `google.golang.org/adk`). Recon-only name `google-adk-go`: not mapped to an SDK for `scan`, used by `trustabl forge` to select the Go runtime-tracing recipe; forge then lists only the language-independent `google_adk` rules, with a note that no Go rules exist | ❌ none | ❌ none | | **Google ADK** | Java / Kotlin | ❌ | ❌ | ❌ | | **LangChain / LangGraph** | Python | ✅ dep-scan (`langchain` / `langgraph` needles, all manifests) + file inventory | ✅ tools (`@tool` decorator — import-gated to disambiguate from the Claude SDK's `@tool`; `StructuredTool` / `Tool` factories + `.from_function`), agents (`create_react_agent`, `create_agent`, `AgentExecutor` + `AgentExecutor.from_agent_and_tools`; positional `tools` captured), raw `StateGraph` graphs (`StateGraph(...)` → `AgentDef` Class `StateGraph`, import-bound; `compile()` kwargs captured; `ToolNode` / `bind_tools` tools resolved), dangerous built-ins (`PythonREPLTool` / `PythonAstREPLTool` / `ShellTool` / `Requests*` → `HostedToolDef` edges) | ✅ tool LC-001 (no description), LC-002 (untyped params), LC-003 (shell), LC-004 (code-exec), LC-005 (SSRF), LC-006 (`return_direct`), LC-025 (side-effecting tool with a free-form recipient/amount and no visible bound), LC-026 (allow-listed host, https:// not pinned), LC-115 (`add_node` function — Kind `langgraph_node` — with a side effect and no `interrupt(` gate); agent LC-101 (code-exec/shell built-in — incl. raw `StateGraph`), LC-102 (AgentExecutor no `max_iterations`), LC-113 (AgentExecutor no `max_execution_time`), LC-114 (`StateGraph` compiled with no `checkpointer`, silent on `langgraph.json` repos); repo LC-201 (no agent-guidance doc) | | **LangChain / LangGraph** | TypeScript | ✅ dep-scan (`@langchain/*` / `langchain` / `langgraph`) + file inventory | ✅ tools (`tool(fn, {...})` factory — import-gated, config from arg 1; `DynamicStructuredTool` / `DynamicTool`), agents (`createReactAgent`, `createAgent`, `new AgentExecutor`) | ◐ tool LC-010 (no description), LC-011 (shell), LC-012 (code-exec), LC-013 (SSRF), LC-014 (`returnDirect`), LC-027 (allow-listed host, https:// not pinned); agent LC-111 (AgentExecutor no `maxIterations`). Provider hosted tools (`shell()` / `bash_*` / `applyPatch`) and the raw `StateGraph` graph agent are documented gaps | | **CrewAI** | Python | ✅ dep-scan (`crewai` / `crewai-tools`) + file inventory | ✅ agents (`Agent(...)`, import-gated to `crewai` so it doesn't collide with OpenAI/ADK `Agent`), tools (`@tool` decorator routed in `kindFromDecorators` by import binding; `Tool(fn)` factory), dangerous built-ins (`CodeInterpreterTool` / `FileReadTool` / scrape+RAG tools → `HostedToolDef`) | ✅ tool CREW-001..006 (no-desc, untyped, code-exec, shell, SSRF, idempotency), CREW-108 (`result_as_answer`), CREW-014 (side-effecting tool with a free-form recipient/amount and no visible bound; no tool-level gate exists), CREW-015 (allow-listed host, https:// not pinned); agent CREW-101..104 (`allow_code_execution`, `code_execution_mode=unsafe`, wired `CodeInterpreterTool`, `allow_delegation`), CREW-106 (unconstrained `FileReadTool`), CREW-107 (URL-fetching tools), CREW-110 (no explicit `max_iter`), CREW-111 (no `max_execution_time`); repo CREW-201. `class X(BaseTool)` and `Crew(...)` orchestration are v1 gaps | | **AutoGen / AG2** | Python | ✅ dep-scan (`pyautogen` / `ag2` / `autogen-agentchat`) + file inventory | ✅ two import gates (AG2/0.2 `autogen`; Microsoft v0.4 `autogen_agentchat`/`_core`/`_ext`): agents `ConversableAgent` / `UserProxyAgent` / `AssistantAgent` / `GroupChat` / `GroupChatManager` / `CodeExecutorAgent`; tools `register_function(fn,...)` + stacked `@x.register_for_llm` / `@x.register_for_execution` attribute decorators; nested `code_execution_config` dict captured | ✅ agent AG2-001 (`use_docker=False`), 002 (`human_input_mode=NEVER` + code exec), 004 (`GroupChat` no `max_round`), 005 (`AssistantAgent` code exec), 006 (no `max_consecutive_auto_reply`), AG2-021 (no wall-clock timeout around `initiate_chat` / `a_initiate_chat` / `run` / `a_run` / `run_stream`; credits `cancellation_token`); tool AG2-007..012 (no-desc, untyped, shell, code-exec, SSRF, timeout), AG2-020 (side-effecting tool with a free-form recipient/amount and no visible bound; no tool-level gate exists), AG2-022 (allow-listed host, https:// not pinned); repo AG2-201. AG2-003 (v0.4 executor-class), the `register_function` caller/executor edge, and AG2 `@tool` are v1 gaps | | **Raw `anthropic` / `openai` clients** (no agent framework) | Python | — no dep needle: the `raw_llm_sdk` category loads unconditionally and the rule predicates gate | ✅ per function: `.messages.create(…, tools=…)` / `.chat.completions.create(…, tools=…)` manual tool loops with a runtime-built system prompt/message, plus same-function name-level taint from the model's tool-call arguments (`DetectRawLLMToolOutputInSystem`) | ◐ repo RAW-001 (Anthropic `system=`), RAW-002 (OpenAI `role: system`/`developer`). Narrow heuristic: no cross-function/file flow, no Responses API | | **Vercel AI SDK** | TypeScript | ✅ dep-scan (quoted `"ai"` key + `@ai-sdk/`) + file inventory | ✅ tools (`tool({...})` / `dynamicTool({...})` single-object factory, import-gated to `ai`), agents (call-based `generateText` / `streamText` / `generateObject` / `streamObject` carrying a `tools` record + class `ToolLoopAgent` / `Experimental_Agent`; **`tools` is an object/record**, walked by property value, not an array), provider hosted tools (`.tools.*()` → `HostedToolDef`) | ◐ tool VAI-001..005 (shell, code-exec, SSRF, no-desc, untyped), VAI-011 (HTTP call without timeout), VAI-019 (side-effecting tool with a free-form recipient/amount and no visible bound, no `needsApproval`; matches `const x = tool(...)` bindings only), VAI-021 (allow-listed host, https:// not pinned); agent VAI-006 (provider shell/computer/code-exec tool), VAI-007 (no loop bound), VAI-008 (`toolChoice:'required'` + dangerous tool), VAI-020 (bounded tool loop with no `abortSignal`); repo VAI-012. VAI-009/010 (name rules — Vercel tools carry no `Name`) are gaps; `.js`/`.mjs`/`.cjs` apps are now AST-parsed via the shared TS-family pipeline (ES `import` and CommonJS `require()`) | | **Pydantic AI** | Python | ✅ dep-scan (`pydantic-ai` / `pydantic-ai-slim`) + file inventory | ✅ agents (`Agent(...)` → Class `PydanticAgent`, import-gated to `pydantic_ai`), tools (`@agent.tool` / `@agent.tool_plain` attribute decorators — routed in `kindFromDecorators`, disambiguated from the Claude SDK's `@agent.tool` by import; `Tool(fn)` factory), native tools (`capabilities=[NativeTool(CodeExecutionTool())]` / `builtin_tools=[...]` unwrapped → `HostedToolDef`, inner kwargs such as `force_download` captured), FileUrl-family `force_download` stamped on the agent, `agent.run`/`run_sync`/`run_stream` call sites (`usage_limits`) | ✅ tool PYD-001..007 (no-desc, untyped, shell, code-exec, SSRF, timeout, idempotency), PYD-014 (side-effecting tool with a free-form recipient/amount and no visible bound), PYD-015 (allow-listed host, https:// not pinned); agent PYD-101 (no `output_type` validation), 102 (`CodeExecutionTool`), 103 (`WebFetchTool` / `UrlContextTool` / `WebSearchTool`), 104 (`force_download=True` / `"allow-local"` on FileUrl-family or `WebFetchTool`), 105 (`end_strategy='exhaustive'`), 106 (no explicit `usage_limits`), 108 (run call with no wall-clock timeout); repo PYD-201. The bare-`tools=[fn]` ToolDef shape and `RunContext` param-strip for PYD-002 remain gaps | | **NVIDIA NeMo Agent Toolkit** | Python | ◐ dep-scan (`nvidia-nat`, catches bracketed extras) + file inventory | ❌ | ❌ | | **OpenShell** | Python | ✅ shell-invocation discovery + `openshell/*.yaml` policy files surfaced | ✅ `KindShellInvocation` tools → `RepoInventory.HasShellInvocations` (the "openshell" risk surface; not an SDK, never in `SDKsDetected`) | ❌ rules moved to closed-source companion project (no rule fires; no META finding — openshell is not treated as an unaudited SDK) | ## What we parse exactly (per SDK) ### Claude Agent SDK — Python Discovery sources: `internal/analysis/discovery.go`, `agents.go`, `subagents.go`, `claude_settings.go`. | Construct | Recognition | |---|---| | Tools | Decorators matched by resolved callee path: `@tool`, `@claude_tool`, `@agent.tool`, or a decorator whose callee contains `claude_agent_sdk`. Exact-callee matching (not substring), so unrelated decorators like `@toolbar` / `@tool_registry.register` no longer misclassify as tools. Captures: function name, params, type annotations, docstring, decorator kwargs | | Agents | `AgentDefinition(...)` constructor calls. Captures every kwarg into a typed `KwargTree`: `name`, `description`, `prompt`, `tools`, `disallowedTools`, `permissionMode`, `mcpServers`, `skills`, `memory`, `maxTurns`, `background`, `effort`, `initialPrompt`. Typed accessors expose `tools`/`disallowedTools`/`permissionMode`/`mcpServers` without reaching into the tree | | Subagents | **Hybrid** discovery: canonical `.claude/agents/*.md` (any path depth, monorepo-safe) PLUS a frontmatter-shape fallback over all markdown files (gate: `name` + `tools`/`model`, excluding `SKILL.md` and `.claude/commands/`) that catches flat collections like `VoltAgent/awesome-claude-code-subagents` (subagents under `categories//*.md`). Captures `name`, `description`, `tools` (verbatim) + `ToolGrants` (parsed grammar: bare / `Bash(...)` / `mcp__server__tool`), `disallowedTools`, `model`, `permissionMode`, `mcpServers`, `skills`, `isolation`, `HasHooks`. Files without frontmatter or without a `name:` are skipped. Audited by subagent-scope rules — CSDK-110 ("Subagent granted the built-in Bash tool"), CSDK-111 ("Subagent granted filesystem-write or web-fetch built-ins"), CSDK-112 (WebSearch) and CSDK-115 ("Subagent omits tools and inherits the full subagent tool pool") are the shipped rules; predicate `subagent_grants_tool: [Bash]` matches parsed grants (so `Bash(...)` matches `Bash`), and `subagent_inherits_all_tools: true` matches a `.claude/agents/` file with no `tools` key and no `disallowedTools` entries. Subagent presence alone marks the repo as `claude_agent_sdk` (no SDK code required), so the pack loads and CSDK-110/111 fire on pure-markdown collections. Subagent rules carry no `language:` field (markdown frontmatter, language-agnostic) | | Skills | `SKILL.md` (basename, any depth: `.claude/skills//SKILL.md`, plugin `skills/`, nested). Parses `name`, `description`, `allowed-tools` (space-separated or YAML-list → `ToolGrants`), `argument-hint`, `disable-model-invocation`, plus **body facts** (dynamic-context exec commands, external URLs, prompt-injection markers incl. hidden-Unicode — zero-width, the Tags block U+E0000–E007F, and bidi overrides), a **bundled-file inventory** (script/markdown/binary/resource by extension; non-binary files are content-scanned, size-capped — scripts for network egress and credential reads, any file for committed secret literals), and `disallowed-tools` / `context` / `agent` (fork) / `HasHooks` / `user-invocable`. Audited by skill-scope rules: CSKILL-001 (auto-approved unrestricted shell), CSKILL-002/003 (dynamic-context execution + network/secret egress), CSKILL-010/011 (bundled-script egress + credential-read — the payload-in-aux-file surface body-only scanners miss), CSKILL-030 (hardcoded secret committed in a bundled file), CSKILL-020 (external-URL fetch), CSKILL-040 (injection markers incl. hidden-Unicode), CSKILL-050 (model-invocable + side-effecting tools), CSKILL-060 (description claims read-only but grants side-effecting tools — OWASP AST04 metadata mismatch). Skill rules carry no `language:` field (markdown, language-agnostic); the `claude_skill` pack loads unconditionally (skills are not an SDK import). Dependency manifests are parsed **repo-wide** (not just skill-bundled) into `ScanResult.Dependencies` — a deterministic supply-chain BOM across all six supported languages (`requirements.txt`/`pyproject.toml`/`Pipfile` pip, `package.json` npm, `go.mod` Go, `composer.json` Composer, `*.csproj` NuGet, `Cargo.toml` Cargo; `DepRef{name, version, ecosystem, source}`, sorted + deduped, **not** folded into `ScanID`), with an optional CycloneDX 1.5 export via `--bom-out`. Pure inventory by default: no network, no CVE matching (hands off to OSV-Scanner / Dependabot / syft). The opt-in `--vuln-scan` layer (`internal/vulndb`, TR-271) adds the CVE verdict — it matches each concretely-pinned dep against a pinned OSV snapshot (per-ecosystem PEP 440 / semver comparison + the OSV range algorithm, CVSS 3.x → severity) and emits both `ScanResult.Vulnerabilities` and synthesized advisory-ID findings that hit the normal exit codes and SARIF; the snapshot is fetched from osv.dev on first use, cached with an offline fallback (`trustabl vulndb pull` pre-warms it), and its version is folded into `ScanID` only when enabled. Declared ranges (no lockfile) are skipped, not guessed. See TR-278, TR-271. | | Slash commands | **Two path shapes**: canonical `.claude/commands/*.md` AND `/commands/*.md` whenever `` has a sibling `.claude-plugin/plugin.json` (the layout used by plugin-distribution repos like `wshobson/agents` — `plugins//commands/*.md`). Command name = file basename. Parses frontmatter `description`, `allowed-tools`, `model`, `argument-hint`, `disable-model-invocation`; a command without frontmatter is still emitted (body is the prompt). No rules yet — surfaced in inventory/JSON | | Plugin manifests | `.claude-plugin/plugin.json` and `marketplace.json` JSON-parsed into `PluginManifest` (`kind` plugin/marketplace, `name`, catalog `plugins[]` with `name` + normalized `source`). The `source` field accepts both forms in the wild: a plain string (`"./local-foo"`) or an object (`{"source":"git-subdir","url":"…","path":"…"}` for external git refs); object forms are normalized to `:#` so the trust category survives the round-trip, unknown shapes fall back to raw JSON. A previous typed-string parser dropped the entire manifest on the object form. The recon walk descends into `.claude-plugin/`. No rules yet — surfaced in inventory/JSON | | Settings | `.claude/settings.json` and `settings.local.json` JSON-parsed: `permissions.allow`/`deny`/`ask` decomposed via the grammar `` \| `()` plus `mcp____`; `defaultMode`, `additionalDirectories`, presence flags for `env`/`hooks`/`sandbox` | | Codex and OpenCode harness files | The walk enters `.agents/` and `.opencode/`. Skills under `.agents/skills/` are discovered like any `SKILL.md`. `.codex/hooks.json` is tagged `codex_hooks`, `.opencode/plugin(s)/*.{ts,js,mjs}` `opencode_plugin`, `.opencode/agent(s)/*.md` `opencode_agent`; the scan summary counts them as `Harness files`. A bare `plugin/` or `sub_agents/` outside `.opencode/` is not labelled. **No rules yet** — inventory only, so zero findings over these files is not a pass. `AGENTS.md` is still only the guidance-present marker; its content is not analysed | | Staged OpenCode harness tree | A directory holding `systemprompt.md` and `guardrails.md` side by side (the signature; either alone is not enough). Under that root, `systemprompt.md`, `guardrails.md`, `LongTermMEMORY.md` and `tool_descriptions/*.md` are tagged `harness_guidance`, `plugin/*.{ts,js,mjs}` `opencode_plugin` and `sub_agents//AGENT.md` `opencode_agent`; the scan summary counts them as `Harness files`. Works at any depth. Only objective hidden text is judged, by **AGCFG-010** (medium: zero-width characters, Tags-block characters and bidirectional overrides in the guidance files); what the instructions say, the plugins and the subagents are not judged, so zero findings is not a pass. A repository that merely has both file names in one folder, such as a docs directory, is labelled too | | Session config | `ClaudeAgentOptions(...)` constructor calls. Captures kwargs into a `KwargTree`; `permission_mode` is read by the repo-scope rule CSDK-202, `max_turns` absence by CSDK-204, and `disallowed_tools` absence (combined with an `acceptEdits` `permission_mode`) by CSDK-205 — `max_turns`/`disallowed_tools` absence share one predicate helper, `repoClaudeOptionsMissingKwarg`. Presence alone marks the repo `claude_agent_sdk` so the pack loads for options-only repos | | Components surfaced (path-only) | `CLAUDE.md`, `AGENTS.md`, `hooks/*.{py,ts,js,jsx,mjs}`, MCP configs (`mcp.json`, `mcp_servers.json`, `claude_desktop_config.json`) | ### Claude Agent SDK — TypeScript Discovery sources: `internal/analysis/ts_discovery.go`, `ts_agents.go`, `ts_mcp_servers.go`. Import gate: only files importing from `@anthropic-ai/claude-agent-sdk` are processed (handles named, renamed, namespace `* as`, and default imports). | Construct | Recognition | |---|---| | Tools | `tool(name, description, zodSchema, handler, extras?)` factory calls. Captures: name (arg 0), description (arg 1), Zod schema top-level keys as ParamNames, handler body facts (`shells_out`, `http_call`), extras flattened into Config | | Agents (main thread) | Every `query({prompt, options?})` call emits one `AgentDef` with `Class="QueryMainAgent"`. The TS SDK has no `AgentDefinition` constructor for the main thread — the call site IS the declaration. `Name`: the `const X = query(...)` binding if present, else the enclosing function name (e.g. `"queryStream"`), else `"ClassName.methodName"` if inside a class method. `Opaque=true` when `options` is a computed identifier (the typical real-world shape, e.g. `query({prompt, options: mergedOptions})`); inline `options` populates `Kwargs` plus `ToolRefs` (from `options.allowedTools`) and `MCPServerRefs` (from `options.mcpServers`). When `options` is opaque, `ToolRefs` falls back to a file-scoped scan for any `allowedTools: [...]` array (heuristic — catches the common class-field / named-const shapes; may over-extract if a file has multiple unrelated allowedTools arrays) | | Agents (sub-agents inline) | Each property inside `query({options: {agents: {...}}})`. Property key becomes `Name`; `Class="AgentDefinition"`; value object becomes `Kwargs` | | Agents (sub-agents typed-const) | `const x: AgentDefinition = {...}` (and `export const ...`). `Name=VarName=constName`; `Class="AgentDefinition"`; value object becomes `Kwargs` | | MCP servers | `createSdkMcpServer({...})` → `Class="createSdkMcpServer"`, `Transport="sdk"`. Object literals in `options.mcpServers` discriminated by `type:` → one of `McpStdioServerConfig`/`McpSSEServerConfig`/`McpHttpServerConfig`/`McpSdkServerConfigWithInstance` | | Tool refs | `agent.tools=["Read","Bash",…]` strings populate `AgentDef.ToolRefs` | | MCP refs | Each property in `agent.options.mcpServers` populates `AgentDef.MCPServerRefs` (inline-object values → class from `type:`; identifier values → `Class="createSdkMcpServer"`) | ### OpenAI Agents SDK — Python Discovery sources: `internal/analysis/discovery.go`, `agents.go`, `hosted_tools.go`, `mcp_servers.go`. | Construct | Recognition | |---|---| | Tools | `@function_tool` decorator with kwargs (`strict_mode`, `failure_error_function`) captured into `ToolDef.Config` | | Agents | `Agent(...)` and `SandboxAgent(...)` constructor calls. Full KwargTree capture: `instructions`, `model`, `model_settings`, `tools`, `handoffs`, `input_guardrails`, `output_guardrails`, `tool_use_behavior`, `mcp_servers`, `output_type`, `tool_choice`, etc. | | Hosted tools | Closed set of 11 classes inside `tools=[...]`: `WebSearchTool`, `FileSearchTool`, `ComputerTool`, `HostedMCPTool`, `CodeInterpreterTool`, `ImageGenerationTool`, `LocalShellTool`, `ShellTool`, `ApplyPatchTool`, `CustomTool`, `ToolSearchTool` → emits `HostedToolDef` + `HostedToolRef` edge. Matched bare or module-qualified (`agents.WebSearchTool()`) | | MCP servers | Closed set of 3 classes inside `mcp_servers=[...]`: `MCPServerStdio` (stdio), `MCPServerSse` (sse), `MCPServerStreamableHttp` (streamable_http), matched bare or module-qualified (`mcp.MCPServerStdio()`). Both inline construction AND `async with X() as srv:` alias resolution (single-file scope) | | handoffs edges | `handoffs=[...]` kwargs resolved into `HandoffRefs` pointing to same-file `AgentDef`s (resolved by both the `name=` literal and the assignment-target variable). A list item wrapped in the `handoff(...)` helper resolves to `external` | | Guardrails | `@input_guardrail` / `@output_guardrail` decorated functions, resolved as edges from each agent. Class-based guardrails are a documented gap | | Sessions | Construction sites of `SQLiteSession`, `SQLAlchemySession`, `RedisSession`, `MongoDBSession`, `EncryptedSession`, `AdvancedSQLiteSession` | ### OpenAI Agents SDK — TypeScript Discovery sources: `internal/analysis/ts_openai_tools.go`, `ts_openai_agents.go`, `ts_openai_hosted_tools.go`, `ts_openai_mcp_servers.go`, `ts_openai_guardrails.go`, `ts_openai_sessions.go`, plus the shared `ts_handler_facts.go`. Import gate: only files importing from `@openai/agents`, `@openai/agents-core`, or `@openai/agents-openai` (handled by the `TSImportAliasesAny` union helper) are processed. | Construct | Recognition | |---|---| | Tools | `tool({name, description, parameters, execute, ...})` factory calls. Captures: `name` / `description`, `parameters` keys as `ParamNames` — both an inline object literal (`{ city: ... }`) and a Zod schema constructor (`z.object({...})`, with chained refinements like `.strict()` unwrapped); any schema constructor sets `HasTypedParams` even when keys cannot be enumerated. Plus handler body facts via shared `tsHandlerFacts` (`shells_out`, `http_call`), and option fields (`strict`, `needsApproval`, `timeoutMs`, etc.) flattened into `Config`, including nested objects with dot-joined keys (e.g. `annotations.readOnlyHint`). `VarName` from the enclosing `const x = tool({...})` binding | | Agents | `new Agent({...})` and `Agent.create({...})`. All option-object kwargs captured into a typed `KwargTree`; `Opaque=true` when the arg is not an object literal or contains a `...spread`. Pre-resolves hosted-tool factory calls inside `tools: [...]` during discovery; identifier-valued refs in `tools`/`handoffs`/`inputGuardrails`/`outputGuardrails`/`mcpServers` are wired by `ResolveEdges` via a Name+VarName double-indexed lookup. An inline `tool({...})` (or any unrecognized call/`new` in `tools`) marks the agent `Opaque` so "no tools" rules don't false-fire | | Hosted tools | Closed set of 9 factories across `@openai/agents-core` and `@openai/agents-openai`: emits `HostedToolDef` with `SDK=openai_agents`, the canonical factory name, the options-object argument captured into `Kwargs` via `TSObjectKwargs` (e.g. `hostedMcpTool({allowedTools, requireApproval, ...})`; nil for a no-arg call like `fileSearchTool()`), and the factory call's own precise `Location` — carried through `ResolveEdges` via `HostedToolRef.Pending` rather than approximated to the agent's line | | MCP servers | `new MCPServerStdio({...})` / `MCPServerSSE` / `MCPServerStreamableHttp` / `MCPServers` (the multi-transport wrapper). Emits `MCPServerDef` with `Transport` ∈ `stdio` / `sse` / `streamable_http` / `multi` and `VarName` from the enclosing `const` | | Guardrails | `defineInputGuardrail` / `defineOutputGuardrail` / `defineToolInputGuardrail` / `defineToolOutputGuardrail` factory calls. Emits `GuardrailDef` with `Kind` ∈ `input` / `output` / `tool_input` / `tool_output` and `VarName` from the enclosing `const` | | Sessions | `new MemorySession()`, `new OpenAIConversationsSession()`, `new OpenAIResponsesCompactionSession()`, and the `startOpenAIConversationsSession()` factory. Emits `SessionUse` with `Class` set to the canonical name | ### MCP — Python | Construct | Recognition | |---|---| | Server tool registrations | Decorators: `@server.tool`, `@mcp.tool`, and `.register_tool(...)` calls. Tagged as `KindMCPTool` in inventory | | Config files | `mcp.json`, `mcp_servers.json`, `claude_desktop_config.json` surfaced as `mcp_config` components (paths only — not deep-parsed) | ### Google ADK — Python Discovery sources: `internal/analysis/adk_agents.go` (agents and FunctionTool-wrapped tools), `internal/analysis/adk_hosted_tools.go` (built-in hosted tool classes). Import gate (AST-based): only files that actually import `google.adk` via an `import` / `from … import` statement are processed — a comment or string literal that merely mentions the module does not trip the gate — which prevents the bare `Agent` class name from colliding with OpenAI's identically-named class. | Construct | Recognition | |---|---| | Agents | Constructor calls for `LlmAgent`, `SequentialAgent`, `ParallelAgent`, `LoopAgent`, `LanggraphAgent`. The `Agent` alias is recognized and normalized to `LlmAgent` in the emitted `AgentDef.Class`. All constructor kwargs captured into a typed `KwargTree` | | FunctionTool-wrapped tools | `FunctionTool(symbol)` calls where the argument resolves to a same-file top-level function → emits a `ToolDef` with `Kind=adk_function_tool`. Cross-module resolution is out of scope | | Built-in hosted tools | Closed set of 14 classes recognized as `HostedToolDef` with `SDK=google_adk`: `BashTool`, `GoogleSearchTool`, `VertexAiSearchTool`, `LangchainTool`, `CrewaiTool`, `AgentTool`, `LongRunningTool`, `LoadWebPage`, `ExitLoopTool`, `GoogleMapsGroundingTool`, `UrlContextTool`, `DiscoveryEngineSearchTool`, `EnterpriseSearchTool`, `MCPToolset` | | sub_agents edges | `sub_agents=[...]` kwargs resolved into `HandoffRefs` pointing to same-file `AgentDef`s (resolved by both the `name=` literal and the assignment-target variable) | **Limitation:** `AgentTool` wraps another agent. The wrapped agent is recorded as a `HostedToolDef` edge but is not transitively analyzed — its tools, guardrails, and sub-agents are not walked further. ### Google ADK — TypeScript Discovery sources: `internal/analysis/ts_adk_tools.go`, `ts_adk_agents.go`, `ts_adk_hosted_tools.go`, plus the shared `ts_handler_facts.go`. Import gate: only files importing from `@google/adk` are processed (handled by the `TSImportAliasesAny` union helper). ADK JS is a single package — no `-core` / `-openai`-style sibling packages. | Construct | Recognition | |---|---| | Agents | `new LlmAgent({...})` / `new SequentialAgent({...})` / `new ParallelAgent({...})` / `new LoopAgent({...})` / `new RoutedAgent({...})` (5 classes; no `Agent` alias unlike Python ADK). All option-object kwargs captured into a typed `KwargTree`; `Opaque=true` when the arg is not an object literal or contains a `...spread`. Pre-resolves hosted-tool class instantiations inside `tools: [...]` during discovery against `TSADKHostedToolClasses`; identifier-valued refs in `tools` / `subAgents` are wired by `ResolveEdges`. An inline `new FunctionTool({...})` (or any unrecognized call/`new` in `tools`) marks the agent `Opaque` | | Tools (FunctionTool) | `new FunctionTool({name, description, parameters, execute, ...})` constructor calls. Class instantiation with an options object (NOT a function-wrapper like Python's `FunctionTool(my_fn)`). Captures: `name` / `description` from string literals, `parameters` keys as `ParamNames` — both an inline object literal and a Zod schema constructor (`z.object({...})`, chained refinements unwrapped; any schema constructor sets `HasTypedParams`), handler body facts via shared `tsHandlerFacts` (`shells_out`, `http_call`), and option fields (`isLongRunning`, etc.) flattened into `Config`, including nested objects with dot-joined keys. `VarName` from the enclosing `const x = new FunctionTool({...})` binding. Reuses `KindADKFunctionTool` — the `Language` field distinguishes the JS options-object shape from Python's function-wrapper shape | | Hosted tools | Closed set of 13 classes recognized as `HostedToolDef` with `SDK=google_adk`: `AgentTool`, `ExitLoopTool`, `GoogleMapsGroundingTool`, `GoogleSearchTool`, `LoadArtifactsTool`, `LoadMemoryTool`, `LongRunningTool`, `PreloadMemoryTool`, `UrlContextTool`, `VertexAiSearchTool`, `VertexRagRetrievalTool`, `RunSkillInlineScriptTool`, `RunSkillScriptTool`. Partial overlap with Python's 13 classes (7 shared, 6 JS-only, 6 Python-only with no JS factory) | | subAgents edges | `subAgents: [...]` (camelCase, unlike Python's `sub_agents=`) kwargs resolved into `HandoffRefs` pointing to same-file `AgentDef`s via the language-agnostic ResolveEdges pass | **Limitation:** `AgentTool` wraps another agent — same transitive-analysis caveat as Python ADK applies. **v1 limitation:** only bare-identifier constructors are recognized; namespace-import constructors like `new ns.LlmAgent({...})` (a member_expression) are not handled. ### LangChain / LangGraph — Python Discovery sources: `internal/analysis/discovery.go` (the `@tool` decorator, import-routed), `langchain_tools.go`, `langchain_agents.go`, `langchain_hosted_tools.go`. Import gate (AST-based): a file is in scope when it imports the langchain ecosystem (`langchain`, `langchain_core`, `langgraph`, `langchain_community`, `langchain_experimental`, `langchain_classic`, the `langchain-*` provider packages). | Construct | Recognition | |---|---| | Tools (`@tool`) | The `@tool` decorator (shared with the Claude SDK), classified in `kindFromDecorators` by the **import binding** of the `tool` symbol (`collectToolImports`): `tool` bound from a langchain module → `KindLangChainTool`, from `claude_agent_sdk` → Claude, last-binding-wins on shadowing — so it attributes correctly even when a file imports both SDKs. A file-level import-presence check is the fallback for a star-import / locally defined `tool`. Captures name, docstring → Description, typed-params, decorator kwargs (incl. `return_direct`) → Config | | Tools (factories) | `StructuredTool(...)` / `StructuredTool.from_function(fn)` / `Tool(...)` / `Tool.from_function(fn)`. Resolves the wrapped function (first positional arg, or `func=`) to a same-file def and points the ToolDef at its body so the shell/code/SSRF predicates scan the implementation; explicit `name=` / `description=` / `args_schema=` override. `class X(BaseTool)` is a documented gap | | Agents | `create_react_agent(...)` / `create_agent(...)` / `AgentExecutor(...)` → `AgentDef` with normalized Class `ReactAgent` / `CreateAgent` / `AgentExecutor`. All kwargs captured; the positional `tools` argument (index 1) of the two factories is captured as a synthetic `tools` kwarg so edge + hosted-tool resolution sees it | | Dangerous built-ins | `PythonREPLTool`, `PythonAstREPLTool`, `PythonREPL`, `ShellTool` (code/shell — **consumed by agent rule LC-101**), plus the `Requests*` family (raw outbound HTTP — discovered as `HostedToolDef` edges but with **no consuming rule yet**, an unaudited SSRF/egress surface) inside an agent's resolved `tools` list → `HostedToolDef` (SDK `langchain`) | **Raw `StateGraph` graphs are now discovered** (`langgraph_graph.go`): the imperative `StateGraph(...)` (or legacy `MessageGraph(...)`) → `.add_node` / `.add_edge` → `.compile()` builder, emergent across many call sites, is anchored on the constructor — bound to its langgraph import origin so a same-named class from an unrelated package (`networkx.Graph`, a local `StateGraph`) is not matched — and emitted as one `AgentDef` (Class `StateGraph`, SDK `langchain`), with the builder var name and the `.compile()` kwargs (`checkpointer`, `interrupt_before` / `interrupt_after`, `store`) linked back onto the agent. The graph's tools are resolved from its `ToolNode([...])` / `llm.bind_tools([...])` call sites (`ResolveEdges`) — both inline list literals and the common `tools = [...]; ToolNode(tools)` same-file variable form — and the dangerous built-ins among them (`PythonREPLTool` / `ShellTool` / …) attach as `HostedToolRefs`, so **agent rule LC-101 fires on a StateGraph that wires a code-execution / shell tool** (`applies_to: [langchain_state_graph]`). **Limitations:** a tools list passed cross-function / cross-module (not a same-file list literal or `tools = [...]` variable) is not resolved; file-level attribution gives every StateGraph in a file the file's graph tools (one graph per file is the common case); and there is no loop-bound rule (`recursion_limit` is an invoke-site config, not a constructor kwarg, so it is out of static reach). ### LangChain / LangGraph — TypeScript Discovery sources: `internal/analysis/ts_langchain_tools.go`, `ts_langchain_agents.go`, plus the shared `ts_handler_facts.go`. Import gate: prefix-matched (`astutil.TSImportAliasesMatch`) so the many subpaths (`@langchain/core/tools`, `@langchain/langgraph/prebuilt`, `langchain`, `langgraph`, any `@langchain/*`) are all in scope. | Construct | Recognition | |---|---| | Tools | `tool(fn, { name, description, schema })` (config is arg 1, unlike OpenAI's arg-0 — the import gate keeps the shared `tool()` name from cross-firing with the Claude/OpenAI passes), `new DynamicStructuredTool({...})`, `new DynamicTool({...})`. Name/description/`schema` (Zod or literal) → typed-params; handler body facts (`shells_out`, `code_exec`, `dynamic_url`) via `tsHandlerFacts`; `returnDirect` → Config | | Agents | `createReactAgent({...})` / `createAgent({...})` / `new AgentExecutor({...})` → `AgentDef` with normalized Class `ReactAgent` / `CreateAgent` / `AgentExecutor`. Kwargs captured; identifier `tools` refs wired as ToolRefs; an inline or `ToolNode` tool list marks the agent `Opaque` | **Limitation:** provider-package hosted tools (`@langchain/openai` `shell()` / `localShell()`, `@langchain/anthropic` `bash_*` — date-stamped names) and class-based tools (`extends StructuredTool`) are documented gaps; the raw `StateGraph` graph agent is not modeled in TS yet (Python now is — see the Python section above). **Known low-severity precision gaps (from the LangChain/LangGraph audit), not yet fixed:** in TS, a `tool()` schema passed by identifier/member reference sets `HasTypedParams=false` (no TS untyped-params rule ships, so no consumer today); a non-string-literal `description` (template literal / identifier const) makes LC-010 read it as absent; and `new ns.DynamicStructuredTool(...)` namespace-import constructors are missed — all three touch the shared TS schema/description/ namespace helpers (4 SDK passes) and were deferred to avoid a risky late shared-code change. In Python, LC-101 only sees hosted tools captured at the `tools=` kwarg / positional, so a tools list bound to a variable leaves `HostedToolRefs` empty (a known LC-101 false-negative); LC-102's `AgentExecutor`-only scope is deliberate — `create_react_agent`/`create_agent` loop bounds live in the invoke-site `recursion_limit`, which is not a constructor kwarg and so is out of static reach. ### CrewAI — Python Discovery sources: `internal/analysis/crewai_agents.go`, `crewai_hosted_tools.go`, plus the `@tool` decorator routed in `internal/analysis/discovery.go` (`kindFromDecorators`). Import gate (AST-based): a file is in scope when it imports the CrewAI ecosystem (`crewai`, `crewai.*`, `crewai_tools`, `crewai_tools.*`) — the dot/underscore boundary keeps an unrelated `crewaix` from matching. The gate disambiguates the bare `Agent` class name from the OpenAI Agents SDK and Google ADK classes of the same name. | Construct | Recognition | |---|---| | Agents | `Agent(...)` constructor calls. Class is `Agent`; `agentKindMatches("crewai_agent")` keys on BOTH `SDK==crewai` AND Class, so an OpenAI/ADK `Agent` never cross-matches. All kwargs captured into a typed `KwargTree` (`role`, `goal`, `backstory`, `tools`, `allow_code_execution`, `code_execution_mode`, `allow_delegation`, …). CrewAI agents carry no `name=`; the human-facing label falls back to `role=`, then the assignment-target variable. `VarName` from `researcher = Agent(...)` so `tools=[...]` references resolve | | Tools (`@tool`) | The `crewai.tools` `@tool` decorator (shared name with the Claude SDK and LangChain), classified in `kindFromDecorators` by the **import binding** of the `tool` symbol (`collectToolImports`): `tool` bound from a crewai module → `KindCrewAITool`. A file-level import-presence check (`crewaiImport && !claudeImport && !lcImport`) is the fallback for a star-import / locally defined `tool`. Captures name, docstring → Description, typed-params, decorator kwargs (incl. `result_as_answer`) → Config | | Tools (factory) | `Tool(fn)` factory call — the wrapped first-positional function is resolved to a same-file def so the body predicates scan the implementation | | Dangerous built-ins | Closed set of 13 high-risk `crewai_tools` classes inside an agent's resolved `tools=[...]` → `HostedToolDef` (SDK `crewai`): `CodeInterpreterTool` (code exec); `FileReadTool` / `FileWriterTool` / `FileWriteTool` (older spelling) / `DirectoryReadTool` / `DirectorySearchTool` (filesystem); `ScrapeWebsiteTool` / `SeleniumScrapingTool` / `WebsiteSearchTool` / `SerperDevTool` / `JSONSearchTool` / `PDFSearchTool` / `CSVSearchTool` (model-chosen URL fetch). Benign built-ins are intentionally omitted so a match is always a security signal; consumed by agent rules CREW-103 / 106 / 107 | **v1 gaps:** the `class X(BaseTool)` subclass tool shape (the analog of LangChain's class-tool gap) and `Crew(...)` orchestration discovery are not modeled. ### AutoGen / AG2 — Python Discovery sources: `internal/analysis/autogen_agents.go`, `autogen_tools.go`. Import gate (AST-based): a file is in scope when it imports **either** upstream line. The two lines share class names but live under different roots: - **AG2 / 0.2** (`autogen`, formerly distributed as `pyautogen` / `ag2`) — matched by `autogen` or `autogen.*`. The dot boundary deliberately excludes the v0.4 roots. - **Microsoft v0.4** (`autogen_agentchat`, `autogen_core`, `autogen_ext`) — matched by each root or its dotted submodules. The union of the two is the discovery gate; `agentKindMatches` keys each `applies_to` token on BOTH `SDK==autogen` AND the class name, so a colliding `AssistantAgent` / `GroupChat` never produces a cross-SDK match. | Construct | Recognition | |---|---| | Agents | Closed set of 6 constructors across both lines: `ConversableAgent`, `UserProxyAgent`, `AssistantAgent`, `GroupChat`, `GroupChatManager`, `CodeExecutorAgent`. `GroupChat` is a config object, not a runtime agent, but it carries the `max_round` speaker-loop bound AG2-004 audits, so it is discovered and `agentKindMatches("autogen_group_chat_manager")` accepts both it and `GroupChatManager`. All kwargs captured; the nested `code_execution_config={...}` dict literal is descended (via `exprFromNode`'s `dictChildren`) so dotted-path lookups reach `code_execution_config.use_docker`. `name=` is the label; `VarName` captured | | Tools (`register_function`) | `register_function(fn, caller=, executor=, name=, description=)` call — the first positional ident is resolved to a same-file top-level function so the body predicates scan it. `name=` / `description=` override its metadata; remaining kwargs (e.g. `api_style=`) land in Config. Neither flows through `kindFromDecorators` (it is a call, not a decorator) | | Tools (attribute decorators) | Stacked `@.register_for_execution()` / `@.register_for_llm(name=, description=)` decorators. The callee is an **attribute** (`.register_for_llm`), not a bare name, so `kindFromDecorators` does not classify it — this pass walks `decorated_definition` nodes and emits one `KindAutoGenTool` per function whose decorator suffix is `register_for_llm` / `register_for_execution`. `register_for_llm`'s `name=` / `description=` override the function's | **v1 gaps:** the v0.4 executor-class hosted surface (`CodeExecutorAgent` + `LocalCommandLineCodeExecutor`) that AG2-003 would target, the `register_function` caller/executor two-agent edge (`VarName` is captured for a future pass but not yet resolved), and an AG2 bare-name `@tool` decorator shape are not modeled. ### Vercel AI SDK — TypeScript Discovery sources: `internal/analysis/ts_vercel_tools.go`, `ts_vercel_agents.go`, `ts_vercel_hosted_tools.go`, plus the shared `ts_handler_facts.go`. Import gate: the bare `ai` core module (matched exactly — the `@ai-sdk/*` provider packages are not gated here; their hosted tools are recognized structurally in the agent walk). The gate disambiguates the identically-named `tool()` factory from the Claude / OpenAI / LangChain passes. | Construct | Recognition | |---|---| | Tools | `tool({ description, inputSchema \| parameters, execute })` (v5/v6 `inputSchema`, v4 `parameters`) and `dynamicTool({...})`. Unlike LangChain's `tool(fn, {...})` (handler arg 0, config arg 1), the Vercel factory takes a SINGLE options object (arg 0). A Vercel tool's NAME comes from the agent's tools-record KEY, not the definition, so `ToolDef.Name` is empty and the binding identifier is captured as `VarName`. `description` is the only model-visible signal (no docstring fallback). Params: a typed Zod object → real names + `HasTypedParams`; an OPEN schema (`z.any()` / `z.unknown()` / `z.object({})` / `{}`) or `dynamicTool` → synthetic `"input"` param, `HasTypedParams=false` (the VAI-005 untyped signal); no schema key → empty params. `execute` body facts via `tsHandlerFacts` | | Agents (call-based) | `generateText` / `streamText` / `generateObject` / `streamObject` ({ model, system, tools, stopWhen, maxSteps, toolChoice }) — emitted as an `AgentDef` ONLY when the options object carries a `tools` property (a bare completion is not an agent and emitting one would flood findings). Class is the normalized callee (`GenerateText` / `StreamText` / `GenerateObject` / `StreamObject`) | | Agents (class-based) | `new ToolLoopAgent({...})` and `new Experimental_Agent({...})` (often imported `as Agent`; alias-resolved via the `ai` import map). Both normalize to Class `ToolLoopAgent`. The system-prompt slot here is `instructions` (vs `system` in the call form); both keys are captured | | tools record walk | In BOTH agent forms, `tools` is an OBJECT / RECORD (`{ weather: weatherTool, search: tool({...}) }`), NOT an array — every other TS agent pass reads `tools: [...]` arrays; this walk iterates the object's property **values**. A bare identifier → `ToolRef{Name}` (wired by `ResolveEdges`); an inline `tool({...})` / `dynamicTool({...})` → agent marked `Opaque` (no symbol edge); a `.tools.()` call → `HostedToolRef`; a spread (`...mcpTools`) or any other value → `Opaque` | | Provider hosted tools | Member-call shape `.tools.()` read directly from the `member_expression` text (`TSCalleeText` cannot resolve it — the provider object is a runtime value, not an import alias). Closed set across `@ai-sdk/anthropic` (`anthropic.tools.bash` / `textEditor` / `computer` / `codeExecution` / `webSearch`), `@ai-sdk/openai` (`openai.tools.localShell` / `computerUsePreview` / `codeInterpreter` / `webSearch` / `webSearchPreview` / `fileSearch`), `@ai-sdk/google` (`google.tools.codeExecution` / `googleSearch` / `urlContext`). A trailing `_` version suffix (e.g. `bash_20250124`) is stripped to a canonical class. The shell / computer / code-exec subset is what VAI-006 flags; web-search / URL-context tools are excluded (SSRF-class, not RCE) | **v1 gaps:** VAI-009 / 010 (name-quality rules — Vercel tools carry no `Name`, so there is nothing to lint) are not shipped. `.js` / `.mjs` / `.cjs` apps are now AST-parsed via the shared TypeScript-family pipeline (ES `import` and CommonJS `require()`). VAI-011 (HTTP-call-without-timeout) now ships via the structural `has_http_call_without_timeout` predicate. ### Pydantic AI — Python Discovery sources: `internal/analysis/pydantic_ai_agents.go`, `pydantic_ai_tools.go`, `pydantic_ai_hosted_tools.go`, plus the `@agent.tool` / `@agent.tool_plain` decorators routed in `internal/analysis/discovery.go` (`kindFromDecorators`). Import gate (AST-based): a file is in scope when it imports `pydantic_ai` / `pydantic_ai.*` (the dot boundary excludes `pydantic_aix`). The gate disambiguates the bare `Agent` class name from the OpenAI, ADK, and CrewAI classes of the same name. | Construct | Recognition | |---|---| | Agents | `Agent(...)` constructor calls, normalized to Class `PydanticAgent` (the upstream token is `Agent`; the stamped name disambiguates from the other SDKs' `Agent`). `agentKindMatches("pydantic_ai_agent")` keys on BOTH SDK and Class. All kwargs captured (`model`, `output_type`, `system_prompt`, `instructions`, `tools`, `retries`, `end_strategy`, …); optional `name=` is the label; `VarName` captured so the `@agent.tool` decorator owner and `tools=[...]` references resolve | | Tools (`@agent.tool` / `@agent.tool_plain`) | Attribute decorators on the agent var (`@agent.tool` takes a leading `RunContext`; `@agent.tool_plain` does not). Routed in `kindFromDecorators` ONLY when the file imports `pydantic_ai` AND does NOT import the Claude SDK — the `&& !claudeImport` guard is load-bearing because the Claude SDK also exposes an `@agent.tool`, so a Claude-only file (and a file importing both) falls through to the Claude case (claude wins the collision). Emits `KindPydanticAITool` | | Tools (factory) | `Tool(fn, takes_ctx=, requires_approval=, name=, description=)` call — gated on `pydantic_ai` imported AND NOT LangChain imported (LangChain ships an identically-named `Tool(...)`; LangChain's gate wins, mirroring the Claude `@agent.tool` precedence). First positional ident resolved to a same-file def so the body predicates scan it; `name=` / `description=` override; remaining kwargs land in Config | | Native (built-in) tools | The dangerous subset `CodeExecutionTool` (code exec), `WebFetchTool` / `UrlContextTool` / `WebSearchTool` (model-chosen URL fetch). Wired in two shapes under the `capabilities=` / `builtin_tools=` kwargs (NOT the generic `tools=` list, so `ResolveEdges` scans those two kwargs specifically): the modern `capabilities=[NativeTool(CodeExecutionTool())]` wrapper (unwrapped one level to the inner class, including the inner constructor's kwargs so `WebFetchTool(force_download=True)` is visible) and the legacy `builtin_tools=[CodeExecutionTool()]` direct form → `HostedToolDef` (SDK `pydantic_ai`), consumed by agent rules PYD-102 / 103 / 104 | | FileUrl `force_download` | File-level walk of `FileUrl` / `ImageUrl` / `AudioUrl` / `VideoUrl` / `DocumentUrl` constructors; the most-dangerous `force_download` (`"allow-local"` outranks `True`) is stamped on every Pydantic agent in that file as `AgentDef.FileURLForceDownload`. Consumed by PYD-104 via `agent_file_url_force_download` | **v1 gaps:** the bare-function `tools=[fn]` ToolDef-synthesis shape (the agent edge still works via the `tools=` kwarg, but no standalone `ToolDef` is emitted), and stripping the leading `RunContext` param for PYD-002 (so a `@agent.tool` whose only "param" is the injected context is not mis-flagged as typed) are not modeled. ### CI workflows — any repo (category `ci`) | Construct | Recognition | |---|---| | Workflow files | repo-root `.github/workflows/*.yml` / `*.yaml` only; nested `.github` directories and other `.github` subdirectories are ignored | | Agent actions | `uses:` exactly `anthropics/claude-code-action`, `anthropics/claude-code-action/base-action`, `anthropics/claude-code-base-action`, `google-github-actions/run-gemini-cli`, `openai/codex-action` (the `@ref` is dropped) | | Agent CLIs | `run:` command word `claude`, `gemini`, `codex`, `cline`, `aider`, or `npx` / `bunx` / `pnpm dlx` of `@anthropic-ai/claude-code`, `@google/gemini-cli`, `@openai/codex`, `cline` | | Untrusted triggers | `pull_request_target`, `issue_comment`, `issues`, `discussion`, `discussion_comment`, `pull_request_review_comment` (CI-001..005) | | Actor gate | `author_association` allow-list of OWNER / MEMBER / COLLABORATOR, literal actor equality, same-repo check (`pull_request_target` only); built-in write-access check of `claude-code-action` and `codex-action` unless a `*` bypass input is set (CI-001, CI-002) | | Write token / extra secret | `permissions` write-all or `contents` / `pull-requests` / `actions` / `packages: write`; any secret outside the agent's model-key input; `secrets: inherit` (CI-002) | | Tool grants | `claude_args` `--allowedTools`, `settings` JSON `permissions.allow`, Gemini `settings` `tools.core` / `tools.allowed`, Codex `sandbox` / `safety-strategy` / `codex-args`, CLI flags (CI-004, CI-005) | **v1 gaps:** `${{ }}` expressions and `strategy.matrix` are not evaluated; `if:` boolean structure is not evaluated (an allow-list joined by `||` with an open branch counts as gated); an actor check made in a step, environment reviewers and label gates are not recognised; reusable workflows are followed one hop and only when local; composite actions are not resolved; a missing `permissions:` block is not flagged (the repository default is not visible); `workflow_run` and `pull_request_review` are not in the trigger set; cline and aider tool flags are not modelled; `anthropics/claude-code-security-review` is not recognised. ### Pre-trust agent config — any repo (category `agent_config`) | Construct | Recognition | |---|---| | Claude project settings | `.claude/settings.json` / `settings.local.json`: `enableAllProjectMcpServers`, `enabledMcpjsonServers`, `hooks.[].hooks[].command` (AGCFG-004/005), and `permissions.allow` WebFetch rules that cover a fixed list of user-content hosts, cancelled by a deny or ask rule in the same `.claude/` folder (AGCFG-011; user and managed settings are not read) | | Gemini settings | `.gemini/settings.json`: hook commands (same walk; flat or nested) and `mcpServers` (AGCFG-005, MCPCFG-002, MCPCFG-003) | | Env files | `.gemini/.env` (full key set) and root `.env` (narrow set), **key names only** (AGCFG-006) | | Codex config | `.codex/config.toml`: `sandbox_mode`, `approval_policy`, override keys, `[mcp_servers.*]` (AGCFG-006, MCPCFG-002, MCPCFG-003) | | Codex hooks | `.codex/hooks.json`: `hooks.[].hooks[]` `command` and `commandWindows`, including the documented `$(git rev-parse --show-toplevel)/...` repo-root form; `mcp_tool`, `prompt` and `agent` handlers are skipped. Reported by **AGCFG-008** at medium, not by AGCFG-005, because Codex skips a project hook until the user reviews and trusts it. Plugin `hooks/hooks.json` and inline `[hooks]` tables in `config.toml` are not read yet | | Agent guidance files | `AGENTS.md` at any depth, read for invisible or direction-overriding characters only: zero-width characters, Unicode Tags-block characters and bidirectional overrides (**AGCFG-009**, medium). A leading byte order mark, emoji or non-ASCII letters joined by a zero-width (non-)joiner, and subdivision-flag emoji are exempt; right-to-left prose that uses bidirectional controls is flagged and is a known false positive. Text is never echoed. What the instructions say is not judged, and `CLAUDE.md` is not read yet | | Editor settings | `.vscode/settings.json` (JSONC), `.cursor/*.json`: boolean-true `*autoApprove*` / `*yolo*` keys, flat or nested (AGCFG-007) | | MCP client configs | `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json` (`servers`), `claude_desktop_config.json`: interpreter + inline code / repo-relative script, or a repo-local command path (MCPCFG-002); `npx`, `bunx`, `pnpm dlx`, `uvx`, `uv tool run`, `pipx run` and `docker run` on a package or image with no exact version or `@sha256:` digest (**MCPCFG-003**, high). Git, URL, local-path and `$VAR` specs are not judged; lockfile pins, a runner started through a shell, a flag before the subcommand, and `deno`, `podman`, `yarn dlx` and `npm exec` launchers are not seen | **v1 gaps:** the Gemini CLI hooks schema and Codex project-config trust behavior are unverified against vendor docs (the Codex hooks file shape and trust model were checked against OpenAI's documentation on 2026-10-20); `*autoApprove*` object-valued maps (per-command terminal allow-lists) are not flagged; remote MCP URLs without auth are not flagged by MCPCFG-002; nested `.env` files outside `.gemini/` are ignored. ### OpenShell — Python | Construct | Recognition | |---|---| | Shell-invocation surfaces | Any bare function body calling `subprocess.*`, `os.system`, or `os.popen` → tagged `KindShellInvocation` in inventory; sets `RepoInventory.HasShellInvocations` (the "openshell" risk surface). Not an SDK — never appears in `SDKsDetected` | | Sandbox policy files | `openshell/*.yaml` / `*.yml` surfaced as `sandbox_policy` components | | Detection trigger | An `openshell/` directory, or any YAML declaring an OpenShell schema (`openshell.nvidia.com/v`). No dependency-manifest needle — OpenShell is recognized by artifact presence and shell-invocation surfaces, not by a declared dep | The OSH-001..005 detection rules previously shipped here; they moved to a closed-source companion project. With no OSH rules shipped, a repo with shell-invocation surfaces fires **no** rule and **no** META finding — but the human report still surfaces it usefully. The `Risk surfaces: openshell` block reports the count of shell-invoking functions, the first three file:line locations (deterministically sorted), a `why:` line stating the threat model (a prompt-injected agent that exposes one of these as a tool can run arbitrary commands), and a `fix:` line with concrete remediations (sandbox, allowlist, drop `shell=True`, keep shell logic out of agent-callable code). The renderer does NOT claim an audit happened, since no openshell rule pack ships. Repo-scope rules with `applies_to: [openshell]` (if any are loaded from a private pack) gate on `HasShellInvocations`. OpenShell is deliberately not treated as an unaudited SDK, so META-001 does not fire for it. ## Gaps and what it would take to close them | Gap | Effort sketch | |---|---| | **Claude SDK TypeScript rules** (`@anthropic-ai/claude-agent-sdk`) | Shipped: tool CSDK-010 (shell), 011 (eval/new Function), 012 (fs-write), 013 (SSRF/dynamic URL), 014 (no description), 016 (mutating tool no idempotency key), 023 (side-effecting tool with a free-form recipient/amount, no visible bound); agent CSDK-120 (permissionMode bypass), 130/131 (`query()` main-thread agent grants Bash / write-fetch built-ins). The TS predicate machinery is in place (structural `shells_out`/`writes_fs`/`dynamic_url`/`code_exec` facts read by `has_shell_call`/`has_write_call`/`has_dynamic_url_call`/`has_code_exec_call` language branches, plus a `has_body_text` line-span substring fallback kept only for inherently *textual-absence* checks) and covered by the per-rule fire/silent harness. CSDK-010 (shell) and CSDK-012 (fs-write) now match structurally, not by substring. Remaining breadth-parity gaps vs the Python CSDK set: typed-params (no viable TS predicate today, see note), idempotency (intentionally still `has_body_text` — it tests for the *absence* of a textual marker, which has no call-shape) and error-handling. Network timeout now HAS a structural predicate (`has_http_call_without_timeout`, shipped for OAI-016 / VAI-011) that a Claude TS rule could adopt (follow-up). The `query()` main-thread agent surface (`claude_query_main`) is now audited by 130/131, which nothing previously checked | | **MCP cross-language follow-ups (Go/C#/PHP/Rust)** | **Go + C# + PHP + Rust MCP have all landed** — each via the same recipe: a dep needle, a tree-sitter pass over the existing smacker grammars (no new dependency), `#[…]`/attribute-based tool discovery, and field-based `language: ` rules. **Go** (MCP-015/016): mark3labs `mcp.NewTool(...)` + official `mcp.AddTool(...)`; follow-ups: the official SDK's handler-struct param schema and metoro-io/mcp-golang's reflection-based `RegisterTool`. **C#** (MCP-017/018): `[McpServerTool]`; follow-ups: the `[McpServerTool(Name=...)]` override, variable-named `.csproj` needle, Semantic Kernel `[KernelFunction]` / AutoGen `[Function]`. **PHP** (MCP-019/020): `#[McpTool]` read from comment text (the grammar parses single-line attributes as comments); follow-ups: multi-line attributes, `#[McpResource]` / `#[McpPrompt]`, an untyped-params rule (type hints are optional). **Rust** (MCP-021/022): rmcp `#[tool]`, doc-comment-aware descriptions; follow-ups: raw-string descriptions and `#[prompt]`/resource shapes. **Shared follow-ups across all four:** body-fact rules (shell/SSRF/timeout) need per-language AST predicate branches (currently TS/JS-only), and MCP resource/prompt registrations are not yet discovered in any language. **Rust** is the last mainstream MCP language; **Java / Ruby / Kotlin** MCP servers exist but have no needle or parser pass yet | | **MCP server-side completeness** | We discover tools registered with `@server.tool` etc., but don't extract `Prompt`, `Resource`, `Sampling` registrations — those exist in the spec and would be a small additional pass | | **LangChain class tools + raw `StateGraph` + TS provider hosted tools** | Three additive discovery gaps in the LangChain pack: `class X(BaseTool)` / `extends StructuredTool` (a class shape, not a call); the raw `StateGraph` → `.compile()` graph agent — **Python is fully landed** (`langgraph_graph.go`: discovered + import-bound, its `ToolNode` / `bind_tools` tools resolved, and LC-101 fires on it via `langchain_state_graph`; remaining is the TS equivalent and cross-function tool-list resolution); and the date-stamped TS provider hosted tools (`shell()` / `bash_*` / `applyPatch`). Each is discovery-only, no schema change | | **CrewAI class tools + `Crew(...)` orchestration** | Two discovery-only gaps: the `class X(BaseTool)` subclass tool shape (a class, not a call or `@tool` decorator — the same shape missing for LangChain) and the `Crew(...)` orchestration unit (the agent constructor `Agent(...)` is covered; the crew that wires agents + tasks together is not). No schema change | | **AutoGen v0.4 executor + caller/executor edge + AG2 `@tool`** | Three additive gaps: the v0.4 executor-class hosted surface (`CodeExecutorAgent` + `LocalCommandLineCodeExecutor`) that an AG2-003 rule would target; resolving the `register_function(caller=, executor=)` two-agent edge (the agents' `VarName`s are already captured, so this is edge-resolution work, not new discovery); and an AG2 bare-name `@tool` decorator arm in `kindFromDecorators`. Discovery / edge-resolution only | | **Vercel name rules** | VAI-009 / 010 need a tool `Name` to lint, but Vercel tools take their name from the agent's tools-record key, not the definition — closing this means propagating the record key onto the `ToolDef` during edge resolution. (VAI-011 network-timeout shipped via the structural `has_http_call_without_timeout` predicate; `.js` / `.mjs` / `.cjs` AST parsing — both ES `import` and CommonJS `require()` gating — shipped via the shared TypeScript-family pipeline.) | | **Pydantic bare-`tools=[fn]` + `RunContext` strip** | The bare-function `tools=[fn]` shape resolves the agent edge but emits no standalone `ToolDef` (so tool-scope rules don't fire on it) — closing it means synthesizing a `ToolDef` from each resolved function in `tools=`. And PYD-002 should strip the leading `RunContext` param a `@agent.tool` injects before judging type-annotation coverage, so a ctx-only tool is not mis-read as typed | ## Recommended next moves This section is editorial — recorded here so future contributors see the rationale, not as a binding roadmap. 1. **TypeScript parser** now backs every SDK Trustabl recognizes — Claude SDK TS, OpenAI Agents JS, Google ADK JS, MCP-proper servers (`ts_mcp_proper.go`), and LangChain / LangGraph TS. The discovery patterns differ per SDK but the AST plumbing is shared; the LangChain pass added a reusable prefix-matched import gate (`astutil.TSImportAliasesMatch`) for ecosystems whose imports use many subpaths. The remaining TS work is per-SDK *rule* parity (points 2, 3, and 5), not parser infrastructure. 2. **OpenAI Agents TS rule pack** has **expanded**: OAI-016 (fetch without AbortSignal timeout), OAI-017 (eval / new Function), OAI-019 (mutating tool without idempotency), OAI-022 (no description), OAI-024 (dynamic URL / SSRF), and agent OAI-105 (content hosted-tool without inputGuardrails), all `language: typescript`. TS OpenAI repos no longer produce META-004. The TS fire/silent harness (`parseTSTool` / `parseTSAgentInline` / `parseTSOpenAIAgentInline`) covers them and `TestPolicyRules_AllRulesCovered` enforces a case for every rule including TS. Remaining: TS analogues of the Python OAI path-safety and decorator-config rules (some need new TS facts). 3. **Google ADK TS rule pack** has **landed** (first ADK TS rules): tool ADK-013 (no description), 015 (eval / new Function), 016 (SSRF / dynamic URL), and agent ADK-109 (LlmAgent no description), retargeted at `new FunctionTool({...})` / `new LlmAgent({...})` option-object shapes. TS ADK repos no longer produce META-004. `has_shell_call` and `has_write_call` now read the `shells_out` / `writes_fs` facts for TS (so a TS ADK shell-out or fs-write rule is now expressible structurally). Remaining ADK parity gaps (typed-params, callback-gated agent rules) still need new TS predicates: ADK JS agent callback kwarg names need verification before porting ADK-102/105..107. 4. **MCP rule pack** has **landed** — the dedicated `mcp/` pack now ships 14 rules: tool MCP-001..010 (Python: no description, untyped params, ambiguous name, network timeout, path safety, error contract, idempotency, SSRF, code-exec, shell) and MCP-011..014 (TypeScript: no description, shell, SSRF, eval / new Function). `mcp_tool` coverage now lives only in this pack. Still open: server-side completeness (Prompt/Resource/Sampling registrations) and MCP discovery for Rust/Go (see the gaps table above). 5. **LangChain / LangGraph pack** has **landed** (newest SDK row): 15 rules (LC-001..201) across Python and TypeScript — tool no-description / untyped-params / shell / code-exec / SSRF / `return_direct`, agent code-exec-or-shell built-in + `AgentExecutor` iteration-limit, and the repo missing-guidance-doc rule. The `@tool` decorator (shared with the Claude SDK) is disambiguated by the import binding of the `tool` symbol (`collectToolImports`), so it attributes correctly even in a mixed-import file. The raw `StateGraph` → `.compile()` graph agent is **fully landed** in Python (`langgraph_graph.go`: discovered + import-bound, its `ToolNode` / `bind_tools` tools resolved, and LC-101 firing via `langchain_state_graph`); the TS equivalent is pending. Remaining (tracked in the gaps table): class-based tools (`class X(BaseTool)` / `extends StructuredTool`), and the date-stamped TS provider hosted tools (`shell()` / `bash_*` / `applyPatch`). 6. **Four SDK rows landed 2026-06-05** — CrewAI, AutoGen / AG2, and Pydantic AI (Python) plus the Vercel AI SDK (TypeScript). Each ships discovery in the engine binary and a rule pack in `agent-reliability-rules`: CrewAI CREW-001..006 / 101..108 / 201, AutoGen AG2-001..012 / 201, Vercel VAI-001..008 / 012, Pydantic PYD-001..007 / 101..105 / 201. The bare `Agent` name is import-gated per SDK so CrewAI / Pydantic / OpenAI / ADK never cross-match, and `kindFromDecorators` now also routes CrewAI `@tool` (by import binding) and Pydantic `@agent.tool` / `@agent.tool_plain` (import-gated, with the Claude SDK winning the collision). Remaining per-SDK work is tracked in the gaps table above: CrewAI `class X(BaseTool)` + `Crew(...)`; AutoGen v0.4 executor-class (AG2-003) + the `register_function` caller/executor edge + an AG2 `@tool` arm; Vercel VAI-009/010 name rules + `.js` parsing (the VAI-011 TS-timeout predicate has shipped); Pydantic bare-`tools=[fn]` synthesis + the `RunContext` param-strip for PYD-002 (PYD-104 `force_download` shipped). ## Sources - [google/adk-python](https://github.com/google/adk-python) — Google's official Python ADK - [google/adk-js](https://github.com/google/adk-js) — Google's official TypeScript ADK - [Agent Development Kit docs](https://google.github.io/adk-docs/) — ADK documentation - [Introducing ADK for TypeScript](https://developers.googleblog.com/introducing-agent-development-kit-for-typescript-build-ai-agents-with-the-power-of-a-code-first-approach/) — Google Developers blog announcement - [openai/openai-agents-js](https://github.com/openai/openai-agents-js) — OpenAI Agents TypeScript SDK - [OpenAI Agents SDK TypeScript docs](https://openai.github.io/openai-agents-js/) - [anthropics/claude-agent-sdk-typescript](https://github.com/anthropics/claude-agent-sdk-typescript) — Claude Agent SDK TypeScript repo - [@anthropic-ai/claude-agent-sdk on npm](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk)