From e8b774b2b3dfa5836b7cfdef3dd9fc238f02e1e7 Mon Sep 17 00:00:00 2001 From: Swarit Pandey Date: Tue, 23 Jun 2026 16:52:23 +0530 Subject: [PATCH] chore(telemetry): gate scan-state delta protocol off by default Default UseLegacyPackageScan to true so npm/Python scans ship the legacy full-snapshot path until the agent-api side lands. Field becomes *bool so config.json can re-enable (use_legacy_package_scan=false); add STEPSEC_ENABLE_SCAN_STATE=1 test opt-in. STEPSEC_DISABLE_SCAN_STATE stays the kill switch. --- internal/config/config.go | 16 ++++++----- internal/config/config_test.go | 47 +++++++++++++++++++++++++++++---- internal/telemetry/telemetry.go | 19 +++++++++---- 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index ee88714..14ed38b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,9 +27,13 @@ var ( InstallDir string // "" means default (~/.stepsecurity); non-empty makes the agent put all its files (logs, hook errors, future state) under this directory. Bootstrap config.json itself stays at the legacy location. Per-run opt-out is the CLI flag --install-dir=. Resolution: --install-dir flag > STEPSECURITY_HOME env > this field > default — see internal/paths. // UseLegacyPackageScan, when true, disables the scan-state delta-upload // optimization for npm and Python project scans — every run re-uploads - // the full snapshot as in pre-1.13 agents. Default false = optimized. - // Environment override: STEPSEC_DISABLE_SCAN_STATE=1 takes precedence. - UseLegacyPackageScan bool + // the full snapshot as in pre-1.13 agents. + // + // Defaults to true: the delta protocol is gated OFF until the agent-api + // side ships. Set use_legacy_package_scan=false in config.json (or + // STEPSEC_ENABLE_SCAN_STATE=1) to opt back in. STEPSEC_DISABLE_SCAN_STATE=1 + // always forces legacy. + UseLegacyPackageScan = true ) // MaxExecutionDuration is the whole-process execution-watchdog limit @@ -59,7 +63,7 @@ type ConfigFile struct { LogLevel string `json:"log_level,omitempty"` InstallDir string `json:"install_dir,omitempty"` MaxExecutionDuration string `json:"max_execution_duration,omitempty"` - UseLegacyPackageScan bool `json:"use_legacy_package_scan,omitempty"` + UseLegacyPackageScan *bool `json:"use_legacy_package_scan,omitempty"` } // userConfigDir returns ~/.stepsecurity — the per-user config location. @@ -192,8 +196,8 @@ func Load() { if cfg.MaxExecutionDuration != "" && MaxExecutionDuration == "" { MaxExecutionDuration = cfg.MaxExecutionDuration } - if cfg.UseLegacyPackageScan { - UseLegacyPackageScan = true + if cfg.UseLegacyPackageScan != nil { + UseLegacyPackageScan = *cfg.UseLegacyPackageScan } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 32c0473..ef34351 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -184,7 +184,8 @@ func TestConfigFile_InstallDir_JSONRoundTrip(t *testing.T) { } func TestConfigFile_UseLegacyPackageScan_JSONRoundTrip(t *testing.T) { - in := ConfigFile{UseLegacyPackageScan: true} + legacy := true + in := ConfigFile{UseLegacyPackageScan: &legacy} data, err := json.Marshal(in) if err != nil { t.Fatal(err) @@ -197,18 +198,29 @@ func TestConfigFile_UseLegacyPackageScan_JSONRoundTrip(t *testing.T) { if err := json.Unmarshal(data, &out); err != nil { t.Fatal(err) } - if !out.UseLegacyPackageScan { - t.Errorf("UseLegacyPackageScan round-trip = false, want true") + if out.UseLegacyPackageScan == nil || !*out.UseLegacyPackageScan { + t.Errorf("UseLegacyPackageScan round-trip = %v, want true", out.UseLegacyPackageScan) } - // False/absent is omitted (default optimized path is the unmarked state). + // An explicit false must survive the round trip — it's how a config opts + // the (default-off) delta protocol back on. + enabled := false + data, err = json.Marshal(ConfigFile{UseLegacyPackageScan: &enabled}) + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(data, []byte(`"use_legacy_package_scan":false`)) { + t.Errorf("explicit false should serialize, not be omitted: %s", data) + } + + // Absent (nil) is omitted — the field falls back to the package default. empty := ConfigFile{} data, err = json.Marshal(empty) if err != nil { t.Fatal(err) } if bytes.Contains(data, []byte("use_legacy_package_scan")) { - t.Errorf("default-false should be omitted: %s", data) + t.Errorf("nil should be omitted: %s", data) } } @@ -236,3 +248,28 @@ func TestLoad_UseLegacyPackageScan_AppliedFromFile(t *testing.T) { t.Errorf("Load did not propagate use_legacy_package_scan from config.json") } } + +func TestLoad_UseLegacyPackageScan_FalseReEnablesFromFile(t *testing.T) { + // Default is legacy-on (true). An explicit false in config.json must flip + // the package var back to the delta protocol. + prev := UseLegacyPackageScan + t.Cleanup(func() { UseLegacyPackageScan = prev }) + UseLegacyPackageScan = true + + dir := t.TempDir() + t.Setenv("HOME", dir) + t.Setenv("STEPSECURITY_HOME", dir) + cfgPath := filepath.Join(dir, ".stepsecurity", "config.json") + if err := os.MkdirAll(filepath.Dir(cfgPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(cfgPath, []byte(`{"use_legacy_package_scan":false}`), 0o600); err != nil { + t.Fatal(err) + } + + Load() + + if UseLegacyPackageScan { + t.Errorf("explicit use_legacy_package_scan=false did not re-enable the delta protocol") + } +} diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index 573e67a..55ac778 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -387,15 +387,24 @@ func Run(exec executor.Executor, log *progress.Logger, cfg *cli.Config) (err err } endPhase(phaseCtx, phaseCancel, tracker, log, "device_info") - // Per-device scan state for the delta-upload protocol. Three opt-outs: - // - config.UseLegacyPackageScan = true (persistent, set in config.json) - // - STEPSEC_DISABLE_SCAN_STATE=1 (env, incident response) + // Per-device scan state for the delta-upload protocol. Gated OFF by + // default (config.UseLegacyPackageScan defaults true) until the agent-api + // side ships. Resolution, in order: + // - STEPSEC_DISABLE_SCAN_STATE=1 (env kill switch, always wins) + // - STEPSEC_ENABLE_SCAN_STATE=1 (env test opt-in) + // - config.UseLegacyPackageScan (persistent, set in config.json) // - paths.Home() unresolvable (no place to write the file) - // Any of the three leaves scanState nil and the run behaves as pre-1.13. + // A disabled gate leaves scanState nil and the run behaves as pre-1.13. var scanState *state.State var scanStatePath string var scanStateFullSync bool - scanStateDisabled := config.UseLegacyPackageScan || os.Getenv("STEPSEC_DISABLE_SCAN_STATE") == "1" + scanStateDisabled := config.UseLegacyPackageScan + if os.Getenv("STEPSEC_ENABLE_SCAN_STATE") == "1" { + scanStateDisabled = false + } + if os.Getenv("STEPSEC_DISABLE_SCAN_STATE") == "1" { + scanStateDisabled = true + } if !scanStateDisabled { scanStatePath = paths.ScanStateFile() if scanStatePath != "" {