Skip to content

feat(devicepolicy): enforce VS Code extension allowlist#138

Draft
raysubham wants to merge 5 commits into
step-security:mainfrom
raysubham:feat/dmg-policy-profiles
Draft

feat(devicepolicy): enforce VS Code extension allowlist#138
raysubham wants to merge 5 commits into
step-security:mainfrom
raysubham:feat/dmg-policy-profiles

Conversation

@raysubham

Copy link
Copy Markdown
Contributor

Summary

Adds the developer-MDM policy enforcement loop for the ide_extension category (Windows + Linux; macOS stays on the MDM-export path). The agent stays thin: the backend compiles the effective policy, the agent writes it to the OS-native VS Code managed-policy location, reads it back, and reports compliance — VS Code itself does the extension disabling.

What's in here

Fetch (internal/devmdm/fetch.go)

  • GET /v1/:customer/developer-mdm-agent/devices/:device_id/effective-policy?category=ide_extension on the existing agent auth channel
  • Backend hash is persisted and echoed verbatim in compliance reports — never recomputed — so the backend's byte-exact applied == desired check gates compliant
  • Malformed responses (including a non-object policy) are rejected; enforcement on disk is never wiped on a transient/malformed fetch

Writers (writer_windows.go / writer_linux.go / writer_file.go)

  • Windows: AllowedExtensions REG_SZ under the VS Code policies key; wrong-typed values surface as foreign (never overwritten)
  • Linux: AllowedExtensions key in /etc/vscode/policy.json
  • Both manage ONLY the AllowedExtensions value — coexisting policies at the same location are preserved
  • Value is the compiled extensions.allowed object as a stringified-JSON string on every platform (the shape VS Code reads)

Reconciler (reconcile.go)

  • Value-based ownership: the agent clears/overwrites only when the on-disk value equals the recorded written value; any other present value (no record, different bytes, wrong type) → mdm_managed, agent yields
  • Ownership-store writability preflight before any policy write, with rollback to the pre-cycle disk state if the post-write persist fails — no orphaned writes
  • Ownership recorded on every successful write ("what the agent wrote", not "what it verified") so a transient readback mismatch self-corrects next cycle
  • Idempotent: unchanged hash + converged disk → report only, no write
  • Clear path removes only agent-owned values and reports nothing

Wiring + gating

  • runIDEExtensionEnforce runs on the existing scheduled cycle in main.go
  • Gated behind FeatureDevMDMPolicies — off until GA (backend MinEnforcementAgentVersion is still a placeholder); enable via gate override for dogfooding

CI

  • New test-windows-devmdm job (windows-latest) in tests.yml runs the windows-tagged registry-writer tests natively — the macOS test job can only cross-compile them, so without this the production writer would ship with zero executed coverage

Test plan

  • go test -race -count=1 ./... — full suite green (33 packages)
  • go vet clean on darwin, linux, windows; gofmt clean; go mod tidy no drift
  • Reconciler covered by fake-injected tests: ownership/foreign-value matrix, preflight + rollback failure paths, readback-mismatch recovery, idempotent re-apply, clear semantics
  • Windows registry writer tests run natively in the new CI job
  • Dogfood on a Windows + Linux device via gate override before GA

Pre-GA follow-ups (not in this PR)

  • Cut a real enforcement-agent release version and update the backend's placeholder MinEnforcementAgentVersion
  • Flip FeatureDevMDMPolicies on

Add the developer-MDM policy enforcement loop for the ide_extension
category. The agent stays thin: the backend compiles the effective
policy; the agent writes it to the OS-native VS Code policy location,
verifies it, and reports compliance — VS Code itself does the disabling.

- fetch: GET /v1/:customer/developer-mdm-agent/devices/:device_id/
  effective-policy?category=ide_extension; backend hash persisted and
  echoed verbatim (never recomputed); non-object policy rejected
- writers: Windows registry (REG_SZ AllowedExtensions under the VS Code
  policies key) and Linux /etc/vscode/policy.json; only the
  AllowedExtensions value is managed, coexisting policies preserved;
  macOS/other get no writer (MDM-export path)
- reconcile: value-based ownership — clear/overwrite only when the
  on-disk value equals the recorded written value, any other present
  value yields mdm_managed; ownership-store writability preflight before
  any policy write, rollback to pre-cycle disk state if the post-write
  persist fails; ownership recorded on every successful write so a
  transient readback mismatch self-corrects next cycle; idempotent
  re-apply keyed on backend hash
- verify/report: compliant | policy_not_applied | vscode_unsupported |
  mdm_managed | write_failed | verification_failed; applied_hash only
  when readback-confirmed; clear path reports nothing
- main: runIDEExtensionEnforce on the existing scheduled cycle, gated
  behind FeatureDevMDMPolicies (off until GA; override for dogfooding)
- CI: windows-latest job runs the windows-tagged writer tests natively
  (the macOS test job can only cross-compile them)
@raysubham raysubham changed the title feat(devmdm): enforce VS Code extension allowlist via OS-native policy feat(devmdm): enforce VS Code extension allowlist Jun 11, 2026
…policy locations

Rework the enforcement surface (plan rev 2026-06-11): the agent now
converges the `extensions.allowed` key in the user's VS Code
settings.json — one writer for Windows, macOS, and Linux — instead of
writing OS managed-policy locations. macOS moves from export-only to
agent-enforced; real MDM-managed devices are detected and yielded to.

- settings writer: format-preserving single-key JSONC merge via
  tailscale/hujson (RFC 6902 patch) — comments, trailing commas, and
  every other key survive byte-for-byte; atomic temp+fsync+rename with
  capped sibling backups (internal/aiagents/atomicfile); an
  unparseable settings.json is never rewritten; the file is never
  deleted, Clear removes only the key; per-OS path resolution
  (%APPDATA% / ~/Library/Application Support / $XDG_CONFIG_HOME)
- managed-policy probe: read-only AllowedExtensions presence check at
  the OS policy location (HKLM registry / /etc/vscode/policy.json /
  /Library/Managed Preferences plists, machine-wide or per-user) →
  mdm_managed, skip write — a real policy outranks user settings
- ladder rework: fetch → clear-if-owned → probe → idempotency →
  ownership-store preflight → drift detection (on-disk diverged from
  recorded written value → re-apply + drift_detected) → merge-write +
  readback → persist-every-write with rollback → verify → report;
  values compared in compacted canonical form
- drop the VS Code version floor end-to-end: min_vscode_version gone
  from the fetch contract (backend removed it), version inputs gone
  from Verify, vscode_unsupported replaced by drift_detected in the
  reportable states, IDE version detection unwired from main
- delete the HKLM/policy.json writers; retarget the windows CI job at
  the settings writer + registry probe
…files

# Conflicts:
#	internal/featuregate/featuregate.go
"devicepolicy" matches the backend's vocabulary (device_id, registered
devices, per-device compliance) and names what the package manages — the
centrally assigned policy for this device — rather than borrowing the
product label. The product/feature name stays "Developer MDM" where it
refers to the product: the feature gate (FeatureDevMDMPolicies,
"developer-mdm-policies"), the developer-mdm-agent auth channel, wire
paths, and the backend sync references are all unchanged.

Mechanical sweep, no behavior change:
- internal/devmdm -> internal/devicepolicy (package clause + doc refs)
- ownership state file: developer-mdm-policy-state.json ->
  device-policy-state.json (pre-ship, nothing has written it yet)
- cmd wiring: devMDMEnforceTimeout -> devicePolicyEnforceTimeout,
  AppendError source tag "devmdm" -> "devicepolicy"
- CI: test-windows-devmdm job -> test-windows-devicepolicy
- windows probe test key: ...\DevMDMProbe -> ...\DevicePolicyProbe
@raysubham raysubham changed the title feat(devmdm): enforce VS Code extension allowlist feat(devicepolicy): enforce VS Code extension allowlist Jun 12, 2026
…nto settings_writer.go

Pure file consolidation, no behavior or API change:
- fetch.go + report.go -> api.go (and their tests -> api_test.go): the
  Fetcher and Reporter are the two halves of the same developer-mdm-agent
  HTTP channel and share config gating, auth, timeout, and error style.
- writer.go -> settings_writer.go: the Writer interface now lives beside
  its only production implementation.

go doc -all diff before/after confirms the exported surface is identical
apart from the reworded file references in comments.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant