Skip to content

ai_tools: detect custom and framework-based AI agents #52040

Description

@karmine05
  • @karmine05: This is a follow-on to New fleetd table: ai_tools #47619. The ai_tools table was based on my extension, https://github.com/karmine05/agentic-detector. Fleet's vendored copy was imported at upstream v0.3.0 and has since received six Fleet-side fixes. Upstream has since gained four detection improvements that Fleet does not have. This story ports those four upstream commits into Fleet's copy, preserving every Fleet fix.

  • This story also closes the open licensing blocker. orbit/pkg/table/ai_tools/README.md currently states that at the vendored commit the upstream repository contained no LICENSE file, so redistribution has no explicit grant, and that this "must be resolved with the author before this code ships." I am the author. Upstream now carries an MIT license (Copyright (c) 2026 Karmine, upstream commit e494314), and this story vendors it into orbit/pkg/table/ai_tools/LICENSE and removes the warning from the README.

Goal

User story
As a Security Engineer,
I want ai_tools to detect AI agents that aren't on a known-tool list — homegrown agents and framework-based harnesses — and tell me how confident each detection is
so that I can find shadow AI on my fleet instead of only the tools I already knew to look for.

Context

Today the agents type only reports tools present in a hardcoded catalog (Claude Code, aider, goose, Codex, and so on). An internally-built agent, or anything assembled on CrewAI / AutoGen / LangChain, is invisible. This story adds a second detection tier that correlates independent signals — tool home directories, workspace shape, framework dependencies, running processes, MCP configs, instruction files — scores them, and emits a row when the combined evidence clears a threshold. Two new columns expose the reasoning so an analyst can judge a detection rather than trust it blindly.

Upstream commits being ported:

Upstream What it adds
f5c36d2 Multi-signal agent detection: new evidence-gathering package, confidence and evidence columns
ff5825f Strict home-directory boundaries; fixes false-positive process matching on short binary names
0653405 Uses true argv boundaries instead of re-splitting the command line, so MCP servers launched via inline eval scripts are named correctly
3cbcc10 Parses TOML and YAML MCP configs (previously JSON only); adds an authorization risk flag for MCP servers carrying auth headers

Changes

Product

  • UI changes: No changes
  • CLI (fleetctl) usage changes: No changes
  • YAML changes: No changes
  • REST API changes: No changes
  • Fleet's agent (fleetd) changes: Yes — orbit/pkg/table/ai_tools. Two new columns on the existing ai_tools table, plus new rows for previously undetected agents. Schema change to be submitted as a PR against the reference docs release branch updating schema/tables/ai_tools.yml:
    • confidence (integer) — 0–100 detection confidence. Catalog-matched tools are always 100. Multi-signal detections score by weighted signal family; a row is only emitted at score ≥ 40, or score ≥ 30 with two or more independent signal families.
      • @noahtalerman: In the column description let's link out to the go code where these numbers are calculated. Let's also link users to a list of the known agents (these are the ones that get a 100 confidence score)
    • evidence (text) — comma-separated tokens naming the signals behind the detection. Vocabulary: catalog, running, binary, tool_home, workspace_shape, workspace_shape_weak, mcp_config, instructions, and framework:<name> (for example framework:crewai).
      • @noahtalerman: In the column description let's say something like "This is the proof that Fleet is using to determine that this is an agent and not a folder called Claude"
    • risk_flags gains one new token, authorization, for MCP servers configured with an auth header.
    • As with the existing table, this should be included in the fleetd tables extension so plain-osquery deployments get it too.
  • Fleet server configuration changes: No changes
  • Exposed, public API endpoint changes: No changes
  • fleetdm.com changes: No changes
  • GitOps mode UI changes: No changes
  • GitOps generation changes: No changes
  • Activity changes: No changes
  • Permissions changes: No changes
  • Changes to paid features or tiers: Fleet Free and Fleet Premium — no change, table stays available on both
  • My device and fleetdm.com/better changes: No changes
  • Usage statistics: No changes
  • Compatibility: Not a breaking change, but two behavior changes are user-visible and worth calling out in release notes:
    1. Hosts will report more agents rows than before, because previously undetected agents now surface. Saved queries or policies that count rows in ai_tools may shift.
    2. SELECT * returns two additional columns. Queries naming columns explicitly are unaffected.
  • Other reference documentation changes: No changes
  • First draft of test plan added
  • Once shipped, requester has been notified
  • Once shipped, dogfooding issue has been filed

Engineering

  • Test plan is finalized
  • Contributor API changes: No changes
  • Feature guide changes: No changes
  • Licensing: vendor upstream's MIT LICENSE into orbit/pkg/table/ai_tools/LICENSE and remove the ⚠️ License warning from that directory's README. This resolves the blocker the README currently records. Should land as the first commit in the PR, before any Go changes.
  • File-read hardening parity: every new config reader added by this port must use fsutil.ReadFileBounded, matching the four readers Fleet already hardened in internal/mcp/mcp.go. See Testing notes — the upstream code as written does not, and that is a genuine denial-of-service hole, not a style preference.
  • Database schema migrations: No changes — osquery table only, no Fleet server tables involved
  • Load testing: No changes required. Detection runs on the host inside fleetd, not on the server, and the new work is bounded by the same home-directory walk the table already performs.

ℹ️ Please read this issue carefully and understand it. Pay special attention to UI wireframes, especially "dev notes".

Risk assessment

  • Requires testing in a hosted environment: No

  • Requires load testing: No

  • Risk level: Low, conditional on the file-read hardening below being part of the PR. Without it, High.

  • Risk description: four things for QA and review to watch.

    1. Unhardened config readers (must be fixed in this PR). The upstream TOML/YAML/OpenClaw parsers read config files with bare os.ReadFile, carrying // #nosec G304 comments that also suppress the static-analysis check that would flag them. Those paths are under user home directories — ~/.grok/config.toml, ~/.codex/config.toml, ~/.hermes/config.yaml, ~/.openclaw/openclaw.json and siblings — and ScanConfigs reads every catalog path unconditionally, with no existence or file-type gate. fleetd runs as root/SYSTEM across every user's home. An unprivileged user running mkfifo ~/.grok/config.toml therefore hangs the table's worker permanently: os.ReadFile on a FIFO with no writer blocks forever, and the recover() at the Generate boundary catches panics, not hangs. Fleet's fsutil.ReadFileBounded already prevents this — it refuses non-regular files, opens with O_NOFOLLOW|O_NONBLOCK, re-checks identity after opening to close the TOCTOU window, and caps the read. Fix is to route all three readers through it. Verified: the swap compiles, needs no test edits, and leaves vet and cross-platform builds clean.
    2. False positives. The whole point of tier two is emitting rows without a catalog match. The scoring thresholds are the only thing standing between "found shadow AI" and "flagged a developer's ordinary Python project." Verify on a host with normal, non-AI development work that no agents rows appear.
    3. Row-count growth. More rows per host is the intended outcome, but confirm the increase is explainable — every new row should carry evidence tokens that justify it.
    4. Merge regressions. This ports upstream code into a Fleet copy that has diverged. Six Fleet fixes must survive: Windows ACL reading (Read real Windows ACLs in ai_tools fsutil.Stat #50772), per-user and MSIX app discovery (Find per-user and MSIX-packaged AI apps in the ai_tools Windows collector #50771), VS Code bundled extensions (Detect VS Code bundled built-in extensions in ai_tools #50768), Windows ProfileList attribution (Attribute ai_tools Windows rows from ProfileList, not the directory owner #51083), the app substring-matching fix (Fix ai_tools apps collector matching AI apps by raw substring #51132), and the browser-extension trusted-location check (Check trusted location before from_webstore in chromiumSideloaded #50770). Regression coverage for each is in the test plan below.

    With item 1 addressed, the table's safety posture is unchanged from today: reads refuse symlinks and non-regular files, sizes are capped, and directory walks stay bounded to home directories.

Test plan

Make sure to go through the list and consider all events that might be related to this story, so we catch edge cases earlier.

Pre-QA schema check (do first)

  • SELECT * FROM ai_tools LIMIT 1; — confirm confidence and evidence are present, and that confidence is typed integer (not text).
  • Confirm schema/tables/ai_tools.yml matches the shipped table exactly, including the new authorization value in the risk_flags description.
  • Confirm queryable on Fleet Free and Premium — not premium-gated.

Core flow

1. Catalog detections keep working and are marked as such
SELECT name, confidence, evidence, running, sha256 FROM ai_tools WHERE type='agents';

  • macOS: install Claude Code → row with confidence=100, evidence starting catalog
  • macOS: run it → evidence includes running, and running=1 with a valid pid
  • Windows: Claude Code or Gemini CLI → confidence=100
  • Linux: aider or goose → confidence=100
  • Install a catalog agent via Homebrew (symlinked into /opt/homebrew/bin) → sha256 is populated, not empty (regression check on the symlink-resolution behavior; see engineering note below)

2. Multi-signal detection finds agents that are not in the catalog — the new capability

  • macOS: create a project with a CrewAI or AutoGen dependency plus an agent-shaped entry point → row appears with confidence between 40 and 99 and evidence containing framework:<name>
  • macOS: confirm the same project with the framework dependency removed produces no row
  • Windows: same framework-based project → row appears
  • Linux: same framework-based project → row appears
  • Confirm every tier-two row carries at least two independent evidence tokens, or a single token with score ≥ 40 — no row should appear on name resemblance alone
  • Confirm a tier-two detection that later matches a catalog tool merges into one row rather than duplicating (confidence stays 100, evidence tokens union)

3. MCP config parsing covers TOML and YAML (upstream 3cbcc10)
SELECT name, source AS client, location, endpoint, risk_flags FROM ai_tools WHERE type='mcp_server';

  • macOS: MCP server defined in a TOML config → row appears with correct name and transport
  • macOS: MCP server defined in a YAML config → row appears
  • Any OS: MCP server configured with an authorization header → risk_flags includes authorization
  • Confirm existing JSON configs still parse — no regression
  • Malformed TOML and malformed YAML → skipped gracefully, no crash, no partial row

3a. Hardened reads on the new config paths — security regression check for Risk item 1

  • macOS or Linux: as an unprivileged user, mkfifo ~/.grok/config.toml, then run a query against ai_tools as root. The query must return normally. If it hangs, the readers were not routed through fsutil.ReadFileBounded. Repeat for ~/.codex/config.toml and ~/.hermes/config.yaml.
  • Any OS: replace one of those config paths with a symlink pointing at a root-only file → the query returns and no content from the target file appears in any row
  • Any OS: place an oversized (multi-GB) file at one of those config paths → the read is capped, the query returns, memory stays bounded
  • Confirm no os.ReadFile remains under orbit/pkg/table/ai_tools/, and that no #nosec G304 suppression was added by this PR

4. MCP servers launched via inline eval scripts are named correctly (upstream 0653405)

  • Any OS: launch an MCP server via node -e "<inline script>" where the script references a .cjs/.mjs launcher → name resolves to the launcher filename, not a fragment of the script body
  • Confirm a server launched by ordinary file path is still named correctly — no regression

5. Short binary names no longer false-positive (upstream ff5825f)

  • Any OS: run a non-AI process whose name is a short substring of a catalog agent binary → confirm it does not produce an agents row
  • Confirm artifacts outside any home directory are not attributed to a user

Regression checks — Fleet fixes that must survive the port

Edge cases

  • Empty state per OS: host with no AI tools → every type returns 0 rows, confidence/evidence empty rather than malformed, no query error
  • False-positive sweep: a developer host with ordinary non-AI projects (web app, data science notebook, plain Python or Node project) → confirm no agents rows are emitted. This is the most important edge case in the story.
  • SELECT type, count(*) FROM ai_tools GROUP BY type; before and after upgrade on the same host — confirm any increase is attributable to explainable tier-two rows
  • SELECT name, confidence, evidence FROM ai_tools WHERE type='agents' ORDER BY confidence; — confirm no row has confidence=0, and no row has empty evidence
  • Type constraint pushdown still works: WHERE type='apps' does not run the agents evidence gatherer (spot-check latency)
  • Multi-user: framework project under a second user's home is surfaced when run as root, with correct uid/username
  • Performance: on a host with many large repositories, the query completes in reasonable time — the workspace scan must stay bounded
  • Non-ASCII usernames, spaces in paths, symlinked home directories
  • detail remains valid JSON on tier-two rows

Supplemental testing

Testing notes

Engineering notes carried over from the port analysis, for whoever picks this up:

  • Three unhardened readers must be fixed as part of this port (Risk item 1). Upstream's parseYAMLMapServers, parseTOMLMapServers and parseOpenClaw use bare os.ReadFile. Swap all three to fsutil.ReadFileBounded and delete the accompanying #nosec G304 comments, matching the four readers Fleet already hardened in the same file. Confirmed by direct test: os.ReadFile on a FIFO never returns, while Fleet's OpenRegular rejects it at the IsRegular pre-check. The swap needs no test changes.
  • The merge is small but has three conflicts, all known. The dangerous one is in internal/agents/agents.go: taking the upstream side wholesale silently reverts Fleet's resolveSystemBinary symlink-trust guard. Both sides must be kept.
  • The upstream code adds two new fsutil.SHA256 call sites on the tier-two paths that do not route through resolveSystemBinary. This one fails safe — hashing refuses symlinks, so the result is an empty sha256 rather than an unsafe read — but it means Homebrew-symlinked binaries discovered via tier two would report no hash. Both call sites should be wrapped for consistency; the "Homebrew-installed catalog agent has a populated sha256" check in Core flow section 1 covers this.
  • evidence.Gather takes map[string]bool upstream; Fleet's copy uses map[string]struct{} with a has() helper. The parameter type needs adapting, along with five call sites in the package's tests.
  • Fleet deliberately diverged at import so that the MCP config scan feeds only mcp_server rows and the sockets collector attributes egress by owning process. Upstream still shares MCP hostnames with sockets. Fleet's behavior is the one to keep.
  • The full port has been dry-run end to end, including both hardening fixes above: it compiles, gofmt and go vet are clean, go test -race passes on all packages, and Windows and Linux cross-builds succeed.
  • Suggested commit order: (1) vendor the LICENSE and clear the README warning, (2–5) the four upstream commits in upstream order with hardening applied in the commit that introduces the code, (6) schema and changes-file updates. Upstream f5c36d2 ships two defects that upstream ff5825f then fixes, so replaying in order is easier than squashing.

Confirmation

  1. Engineer: Added comment to user story confirming successful completion of test plan (include any special setup, test data, or configuration used during development/testing if applicable).
  2. QA: Added comment to user story confirming successful completion of test plan.
  3. QA: Determined whether this story needs Playwright automation.
    • Needs automation: No — this is an osquery table with no UI surface
    • If yes, filed a follow-up issue in the :help-qa project with status "Needs automation":

Metadata

Metadata

Assignees

Labels

#g-supply-chainSupply Chain product groupstoryA user story defining an entire feature

Type

No type

Projects

Status
🪿 Drafting

Relationships

None yet

Development

No branches or pull requests

Issue actions