ggshield machine setup should also install the eligible platform plugins
Context
ggshield machine setup configures three protections in one idempotent run: AI hooks, global/system git hooks, and a honeytoken (ggshield/cmd/machine/setup.py). It does not touch plugins.
That leaves the machine half-set-up for accounts that are entitled to them: ggshield machine doctor already checks the machine_scan plugin (native scanner loads, endpoints:send scope) and, when it is missing, tells the user to go and run ggshield plugin install machine_scan by hand. ggshield machine scan likewise only appears once that plugin is installed (it merges into the group via PluginAwareLazyGroup(plugin_scope="machine")). So the one command that is supposed to set the machine up is the one command that does not install the thing doctor then asks for.
We don't have to guess the plan
The concern that this is "Business-only, so setup can't know" is already solved server-side. GET /v1/endpoints/plugins (PluginAPIClient.get_available_plugins) returns per-plugin entitlement:
available: true + latest_version → installable for this account;
available: false + reason → not entitled, with a human-readable reason;
404 → plugin system not enabled on the workspace (PluginsNotEnabledError).
So machine setup can simply ask, and install what comes back as available. No plan hardcoding, no new endpoint.
Proposal
Add a fourth step to machine setup, after the existing three:
- Fetch the catalog. On
PluginsNotEnabledError, a non-entitled plugin, or any network/API error: print one informational line and skip — this must not make machine setup fail. (Contrast with the honeytoken step, which returns non-zero on a free plan — out of scope here, but worth a separate look.)
- Install what is available, reusing the existing path:
PluginAPIClient.download_plugin → PluginDownloader.download_and_install → enable_installed_plugin + EnterpriseConfig.save(), i.e. what _install_from_gitguardian in ggshield/cmd/plugin/install.py already does. That code needs a small extract into a reusable helper, since the command version calls ctx.exit() on every error path.
- Idempotency, as everywhere else in
setup: already installed and up to date → report and do nothing; installed but older → update (or leave to plugin update, see open questions).
- Signature verification stays STRICT. No
--allow-unsigned equivalent on setup.
- New opt-out flag
--no-plugins, consistent with --no-ai-hooks / --no-git-hooks / --no-honeytokens.
Root / MDM already works: PluginDownloader lands the wheel in get_system_plugins_dir() when is_root(), which is exactly the fleet case machine setup --system targets.
Why it's worth doing
- A single root-run
machine setup fully provisions a fleet machine, including endpoint scanning.
machine doctor stops asking for a manual step that setup could have done.
- Accounts without entitlement are unaffected — they see one skipped line.
Open questions
- Which plugins? Install every
available: true entry in the catalog (future-proof, no hardcoded name), or only machine_scan (the one machine doctor and machine scan depend on)? Catalog-driven is the same amount of code; the risk is silently installing something new the day it ships in the catalog.
- Update-on-rerun? Should a re-run of
setup upgrade an outdated plugin, or is that strictly ggshield plugin update's job?
- Prompt in interactive runs? Silent install is right for MDM; interactive users might expect to be asked before a wheel is downloaded.
ggshield machine setupshould also install the eligible platform pluginsContext
ggshield machine setupconfigures three protections in one idempotent run: AI hooks, global/system git hooks, and a honeytoken (ggshield/cmd/machine/setup.py). It does not touch plugins.That leaves the machine half-set-up for accounts that are entitled to them:
ggshield machine doctoralready checks themachine_scanplugin (native scanner loads,endpoints:sendscope) and, when it is missing, tells the user to go and runggshield plugin install machine_scanby hand.ggshield machine scanlikewise only appears once that plugin is installed (it merges into the group viaPluginAwareLazyGroup(plugin_scope="machine")). So the one command that is supposed to set the machine up is the one command that does not install the thingdoctorthen asks for.We don't have to guess the plan
The concern that this is "Business-only, so setup can't know" is already solved server-side.
GET /v1/endpoints/plugins(PluginAPIClient.get_available_plugins) returns per-plugin entitlement:available: true+latest_version→ installable for this account;available: false+reason→ not entitled, with a human-readable reason;404→ plugin system not enabled on the workspace (PluginsNotEnabledError).So
machine setupcan simply ask, and install what comes back as available. No plan hardcoding, no new endpoint.Proposal
Add a fourth step to
machine setup, after the existing three:PluginsNotEnabledError, a non-entitled plugin, or any network/API error: print one informational line and skip — this must not makemachine setupfail. (Contrast with the honeytoken step, which returns non-zero on a free plan — out of scope here, but worth a separate look.)PluginAPIClient.download_plugin→PluginDownloader.download_and_install→enable_installed_plugin+EnterpriseConfig.save(), i.e. what_install_from_gitguardianinggshield/cmd/plugin/install.pyalready does. That code needs a small extract into a reusable helper, since the command version callsctx.exit()on every error path.setup: already installed and up to date → report and do nothing; installed but older → update (or leave toplugin update, see open questions).--allow-unsignedequivalent onsetup.--no-plugins, consistent with--no-ai-hooks/--no-git-hooks/--no-honeytokens.Root / MDM already works:
PluginDownloaderlands the wheel inget_system_plugins_dir()whenis_root(), which is exactly the fleet casemachine setup --systemtargets.Why it's worth doing
machine setupfully provisions a fleet machine, including endpoint scanning.machine doctorstops asking for a manual step thatsetupcould have done.Open questions
available: trueentry in the catalog (future-proof, no hardcoded name), or onlymachine_scan(the onemachine doctorandmachine scandepend on)? Catalog-driven is the same amount of code; the risk is silently installing something new the day it ships in the catalog.setupupgrade an outdated plugin, or is that strictlyggshield plugin update's job?