@agentx-core/scan
v0.5.0
Published
List the tools your AI agent can call, ranked by how dangerous they are, and which are unguarded.
Maintainers
Readme
@agentx-core/scan
List the tools your AI agent can call, ranked by how dangerous they are.
No install, no signup, no code changes:
npx @agentx-core/scan .🔍 AGENTX SCAN (TypeScript · 1 file · 4 tools)
===========================================================================
RISK TOOL SYSTEM WHY
---- ---- ------ ---
high purgeUploads - calls `fs.unlink`
src/tools.ts:6
med refundCharge Stripe calls `fs.writeFile`
src/tools.ts:7
-----------------------------------------------------------------------
1 of 4 tools can take destructive or batch actions.
1 writes or sends. 2 need a human look.
1 could not be traced to an agent.
It is exported, so something outside this scan may wire it up.
Your tools connect to: Stripe, read from each tool's own code.
1 tool has no body this scan could read.
What it reaches is unread.
Showing high and med only. Run with --all to list all 4 tools.
-----------------------------------------------------------------------
These can do things the AgentX gateway blocks on the live call:
Mass Destructive Intent purgeUploads
Nothing in this project is guarding them: no AgentX in your dependencies or
MCP config. A guard added at runtime would not show in a static scan.(Trimmed after this point; the run ends with the next steps below.)
What it does
When you give an LLM a tool, you give it a real function and let it choose the arguments. The tools it can call are your risk surface. Scan reads that surface out of your code and sorts it, so the destructive end is the first thing you see.
It reports:
- What the model can call — every declared tool it can read, with
file:line. What it cannot read is named rather than dropped; see What it supports below. - How dangerous each one is —
high/med/low, plusreviewwhen we genuinely could not tell. - Why — every ranking carries its evidence, so you can audit it rather than trust it.
- What each tool can reach — the external services its own body uses (
SYSTEM), named by company rather than by category: Stripe, OpenAI, Anthropic, SendGrid, Postgres. Which company is the question a security review actually asks. Read from the tool's own code, not from its file's imports, so one Stripe import does not tar every tool beside it. A?in the column means that tool's body went unread, and the column shows nothing else for it. What such a tool's file imports is reported separately in the prose below the table, labelled as the guess it is, rather than filled into the column as though we had read it. - Whether an agent can actually call it — a tool that was written but never wired into
an agent is not a live capability. Three answers, and the third is the point:
registered,not-registered, andunknownwhen the tools list is assembled somewhere a static scan cannot follow. We would rather say "cannot tell" than tell you a wired-up tool is dead code. - Where it was declared — tools in
test/andscripts/are marked, so an examples-heavy repo does not read as a 400-tool agent.
What it supports
TypeScript agents built on the Vercel AI SDK (ai) or LangChain.js (@langchain/*).
Both converge on the same tool() helper, so one detector covers both. If your agent uses
something else, we cannot see it yet — that is a gap in this scanner, not in what can be
guarded, and the gateway does not care which framework you use.
Two shapes inside those frameworks are not read yet, require() in place of import and
LangChain's new DynamicStructuredTool({...}) class form. Neither is silent: the scan names
the files and tells you the counts are short. Both are detailed under Limits below, which is
where they are described in full so the two lists cannot drift apart.
TypeScript source, literally. Files must be .ts, .tsx, .mts or .cts. Plain
JavaScript (.js, .jsx, .mjs, .cjs) is not read at all — a JavaScript agent scans
as zero files, and the run tells you nothing was read rather than reporting "no tools found".
How it avoids crying wolf
A scanner that flags everything is a toy. Two rules keep this one honest:
- AST, never regex, and every detection is import-gated.
toolonly counts when it was actually imported fromaior@langchain/*. A local function namedtoolcannot produce a hit. Scanning a current checkout of the Vercel AI SDK repo (5246 TypeScript files), a naive text search matches 916tool(strings; the real number is 408. Counts move as that repo does, so treat them as one measured run rather than a fixed property. - Evidence beats naming. A call to
fs.unlinkis a fact about your code; a name is only a claim. A tool calledtidyUpthat unlinks a path rankshigh. A tool namedrunReportwith a body we read and found clean does not get escalated on vibes — it goes toreviewfor a human to look at, not to the top of the table.
And when we cannot tell, we say so. This is the part most scanners get wrong, so it is
worth being concrete about what review means:
- An implementation we could not read is
review, neverlow— "unreadable" is not "safe". That includes a body that hands off to an imported helper:execute: handlerandexecute: (x) => handler(x)are the same fact and get the same answer. - A directory we could not open, a broken symlink, a file that would not parse: all reported. Never rounded to "clean".
- If we could not find your manifest, we say we could not tell whether anything is guarding you, rather than telling you nothing is.
Expect roughly a third of a real codebase to land in review. That is the honest cost of
not guessing.
None of those rules came from fixtures. Running this over real repositories is what forced
them, and twice it forced a correction. An early classifier demanded a read verb before it
would say low, which stranded 168 of 358 tools in review, because real code names
tools by noun: weather, cityAttractions. Later, a review found four paths that
returned low for a body the scanner had never read, which is the one answer this tool
must not give. Both are fixed. No fixture test would have surfaced either.
Guarding what it finds
Scan reports; it does not modify your code. What you do next depends on where you are.
Shipping agents to customers? Their security review will ask what your agent can do and what stops it. Your SOC 2 covers the company, not the agent. We write that up for you, free.
npx @agentx-core/scan . --shareThat writes agentx-scan.json locally, then you email it to
[email protected] (subject: "security review"). Nothing is uploaded — there is no
endpoint; it is a local file and an email address.
By default the file carries only the high and med risk tools, so it is small enough to read
before you send it, and it says so at the top. --share --all sends everything.
Just guarding your own project? Both guards are language-agnostic and sit at runtime, so there is no separate SDK to add:
- Front your agent with the gateway: https://agentx-core.com/gateway
- Wrap your MCP server in one line: https://github.com/vdalal/agentx-mcp
Limits (worth knowing before you trust a clean result)
.ts/.tsx/.mts/.ctsonly. A plain JavaScript agent is not read, so it scans as zero files. The run says "nothing was read" rather than "no tools found", because those are different answers and only one of them means we looked.- ESM only. A file that loads the SDK with
require()is not read, so tools declared in it are not listed — but, like the class form below, they are no longer missed in silence. Scan names those files and says the counts are short. It used to tell such a reader that their framework was the one it could not read, while they were on a framework it reads. - The
tool()helper only. LangChain'snew DynamicStructuredTool({...})class form is not classified yet — but it is no longer missed in silence. Scan counts those declarations and tells you the result is incomplete, because a scan that quietly skips half your tools reads as "you're covered" when you are not. - The OpenAI raw-array form (
tools: [{ type: 'function' }]) is not detected. SYSTEMspots SDKs, not traffic. We look for a known SDK inside each tool's own code. An API called directly over HTTP has no SDK to spot, so it will not show up. An empty column means "no SDK we could see", never "touches nothing".- Reachable, not executed. Everything here is what your agent can do. Only runtime instrumentation knows what it actually did.
- Risk classification is a coarse heuristic, and deliberately labelled as one. It sorts an inventory; it does not adjudicate whether a tool is exploitable.
Usage
npx @agentx-core/scan [path] # defaults to the current directory
npx @agentx-core/scan . --all # list every tool, not just high and med
npx @agentx-core/scan . --json # machine-readable, always complete
npx @agentx-core/scan . --fail-on-risk # exit 3 if any high or med risk tool was found
npx @agentx-core/scan . --share # write the risky tools to a file to send us
npx @agentx-core/scan . --share --all # same, but every tool
npx @agentx-core/scan --help--json and --share are different jobs on purpose. --json is for your own tooling and is
always the complete list. --share is a file leaving your building, so it defaults to the
risky tools only and states in the file how many it left out. Both payloads carry a schema
field (agentx.scan/1, view: "full" or view: "share") so a consumer can tell which one
it's reading; --json's also carries a cta object (the same next step the human report
ends on) so a program parsing it still has something to hand a person. Combine --json and
--share and stdout stays pure JSON — the human "wrote the file" message moves to stderr, so
npx @agentx-core/scan . --json --share > out.json is always safe to parse.
Use in CI
--fail-on-risk turns a scan into a gate. The command still prints its usual report (or the
--json payload, if you also pass --json), and exits 3 if any tool ranked high or
med, 0 otherwise. Exit code 3 is deliberate and separate from 1 (a --share write
failed) and 2 (bad path), so a bare exit code in a CI log tells you what happened without
reading the output.
- name: AgentX scan
run: npx @agentx-core/scan . --fail-on-riskWithout the flag the exit code is always 0 — this is opt-in, so adding it to your pipeline does not change any scan you already have wired in today.
MIT licensed.
