You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@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:
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.
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.
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.
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.
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.
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
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
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).
QA: Added comment to user story confirming successful completion of test plan.
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":
@karmine05: This is a follow-on to New fleetd table:
ai_tools#47619. Theai_toolstable 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.mdcurrently 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 commite494314), and this story vendors it intoorbit/pkg/table/ai_tools/LICENSEand removes the warning from the README.Goal
ai_toolsto detect AI agents that aren't on a known-tool list — homegrown agents and framework-based harnesses — and tell me how confident each detection isContext
Today the
agentstype 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:
f5c36d2confidenceandevidencecolumnsff5825f06534053cbcc10authorizationrisk flag for MCP servers carrying auth headersChanges
Product
orbit/pkg/table/ai_tools. Two new columns on the existingai_toolstable, plus new rows for previously undetected agents. Schema change to be submitted as a PR against the reference docs release branch updatingschema/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.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, andframework:<name>(for exampleframework:crewai).risk_flagsgains one new token,authorization, for MCP servers configured with an auth header.agentsrows than before, because previously undetected agents now surface. Saved queries or policies that count rows inai_toolsmay shift.SELECT *returns two additional columns. Queries naming columns explicitly are unaffected.Engineering
orbit/pkg/table/ai_tools/LICENSEand remove thefsutil.ReadFileBounded, matching the four readers Fleet already hardened ininternal/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.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.
os.ReadFile, carrying// #nosec G304comments 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.jsonand siblings — andScanConfigsreads every catalog path unconditionally, with no existence or file-type gate. fleetd runs as root/SYSTEM across every user's home. An unprivileged user runningmkfifo ~/.grok/config.tomltherefore hangs the table's worker permanently:os.ReadFileon a FIFO with no writer blocks forever, and therecover()at theGenerateboundary catches panics, not hangs. Fleet'sfsutil.ReadFileBoundedalready prevents this — it refuses non-regular files, opens withO_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.agentsrows appear.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
Pre-QA schema check (do first)
SELECT * FROM ai_tools LIMIT 1;— confirmconfidenceandevidenceare present, and thatconfidenceis typed integer (not text).schema/tables/ai_tools.ymlmatches the shipped table exactly, including the newauthorizationvalue in therisk_flagsdescription.Core flow
1. Catalog detections keep working and are marked as such
SELECT name, confidence, evidence, running, sha256 FROM ai_tools WHERE type='agents';confidence=100,evidencestartingcatalogevidenceincludesrunning, andrunning=1with a validpidconfidence=100confidence=100/opt/homebrew/bin) →sha256is 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
confidencebetween 40 and 99 andevidencecontainingframework:<name>confidencestays 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';risk_flagsincludesauthorization3a. Hardened reads on the new config paths — security regression check for Risk item 1
mkfifo ~/.grok/config.toml, then run a query againstai_toolsas root. The query must return normally. If it hangs, the readers were not routed throughfsutil.ReadFileBounded. Repeat for~/.codex/config.tomland~/.hermes/config.yaml.os.ReadFileremains underorbit/pkg/table/ai_tools/, and that no#nosec G304suppression was added by this PR4. MCP servers launched via inline eval scripts are named correctly (upstream
0653405)node -e "<inline script>"where the script references a.cjs/.mjslauncher →nameresolves to the launcher filename, not a fragment of the script body5. Short binary names no longer false-positive (upstream
ff5825f)agentsrowRegression checks — Fleet fixes that must survive the port
risk_flagsincludesworld_writable(Read real Windows ACLs in ai_tools fsutil.Stat #50772)appsrows (Find per-user and MSIX-packaged AI apps in the ai_tools Windows collector #50771)ide_pluginsrow (Detect VS Code bundled built-in extensions in ai_tools #50768)username/uidfrom ProfileList, not the directory owner (Attribute ai_tools Windows rows from ProfileList, not the directory owner #51083)appsrow (Fix ai_tools apps collector matching AI apps by raw substring #51132)sideloaded_unverified(Check trusted location before from_webstore in chromiumSideloaded #50770)Edge cases
confidence/evidenceempty rather than malformed, no query erroragentsrows 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 rowsSELECT name, confidence, evidence FROM ai_tools WHERE type='agents' ORDER BY confidence;— confirm no row hasconfidence=0, and no row has emptyevidenceWHERE type='apps'does not run the agents evidence gatherer (spot-check latency)uid/usernamedetailremains valid JSON on tier-two rowsSupplemental testing
Testing notes
Engineering notes carried over from the port analysis, for whoever picks this up:
parseYAMLMapServers,parseTOMLMapServersandparseOpenClawuse bareos.ReadFile. Swap all three tofsutil.ReadFileBoundedand delete the accompanying#nosec G304comments, matching the four readers Fleet already hardened in the same file. Confirmed by direct test:os.ReadFileon a FIFO never returns, while Fleet'sOpenRegularrejects it at theIsRegularpre-check. The swap needs no test changes.internal/agents/agents.go: taking the upstream side wholesale silently reverts Fleet'sresolveSystemBinarysymlink-trust guard. Both sides must be kept.fsutil.SHA256call sites on the tier-two paths that do not route throughresolveSystemBinary. This one fails safe — hashing refuses symlinks, so the result is an emptysha256rather 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 populatedsha256" check in Core flow section 1 covers this.evidence.Gathertakesmap[string]boolupstream; Fleet's copy usesmap[string]struct{}with ahas()helper. The parameter type needs adapting, along with five call sites in the package's tests.mcp_serverrows and the sockets collector attributes egress by owning process. Upstream still shares MCP hostnames with sockets. Fleet's behavior is the one to keep.gofmtandgo vetare clean,go test -racepasses on all packages, and Windows and Linux cross-builds succeed.f5c36d2ships two defects that upstreamff5825fthen fixes, so replaying in order is easier than squashing.Confirmation