Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
bump: minor
---

### Added

- `/machine-health:setup apply` takes `revoke=<remediation>` to withdraw an approval and `unmute=<key>` to remove a finding mute under every check.
- One audit runs at a time per state folder: a second run emits a complete run snapshot with one UNKNOWN `run-lock` result naming `state/run.lock` and writes nothing; the lock is released on every exit path.
- A state folder that a OneDrive variable names, or that sits under one, is detected: the run still runs the checks and writes the markdown report, adds one UNKNOWN `state-location` result, and creates or writes nothing else under the state folder (when the report folder is inside it, the report is still written there); a report folder under a sync root only warns. A folder under a reparse point carrying a cloud attribute only warns.

### Changed

- `approvals.json` and `mutes.json` are written only through `Set-ApprovalState`, which holds `state/approvals.lock`, validates the schema before writing, and replaces the file atomically; the one-time TODO.md migration uses the same replace. Hand edits of either file are unsupported while an audit or setup session runs.
33 changes: 30 additions & 3 deletions plugins/machine-health/skills/audit/reference/shared/approvals.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Approvals and per-user state

User-specific approval state lives at `<StateBase>/state/approvals.json`, **not** in the plugin's shipped `TODO.md`. The skill ships with defaults (nothing approved); the user enables individual remediations via `/machine-health:setup` or by editing the JSON directly.
User-specific approval state lives at `<StateBase>/state/approvals.json`, **not** in the plugin's shipped `TODO.md`. The skill ships with defaults (nothing approved); the user enables individual remediations via `/machine-health:setup`.

Schema: [`catalog/schemas/approvals.schema.json`](../../catalog/schemas/approvals.schema.json).

Expand All @@ -23,9 +23,29 @@ The `TODO.md` shipped with the plugin is **policy documentation**, not state. It
remediations available for approval, their risks, and how to enable them. It contains no
checkbox that drives behavior. Runtime proposals accumulate in `<StateBase>/TODO.md`.

## Writing approvals and mutes

`approvals.json` and `mutes.json` have one writer, `Set-ApprovalState`, which
`/machine-health:setup apply` runs (`approve=`, `revoke=`, `unmute=`). The one-time TODO.md migration below is the only other path that writes `approvals.json`,
and it never overwrites an existing file. Both go through the same atomic replace.

What a write guarantees:

- A reader sees the old file or the new one, never a partial write, and a reader never blocks it.
- Concurrent writers are serialized by `<StateBase>/state/approvals.lock`: a second writer waits and
applies its change to the file the first one wrote.
- The result is validated against the file's schema before it replaces anything; an invalid result
leaves the file as it was.
- Nothing is written when a OneDrive variable names `<StateBase>` or a folder above it.

Hand edits of `approvals.json` or `mutes.json` are unsupported while an audit or setup session
runs: the lock covers the plugin's writers, not an editor, so a hand edit can be overwritten or can
overwrite a change made at the same moment.

## Enabling a remediation

Default state: both shipped remediations start as `approved: false`. Enable by editing `approvals.json`:
Default state: both shipped remediations start as `approved: false`. Enable one with
`/machine-health:setup apply approve=<remediation-id>`, which writes an entry like these:

```json
{
Expand Down Expand Up @@ -68,7 +88,8 @@ change recorded in `CHANGELOG.md`, not a condition this file predicts.

## Revoking an approval

Flip `approved: true` to `false`. Next run respects it immediately. No cache.
`/machine-health:setup apply revoke=<remediation-id>` sets `approved` to `false` and refreshes
`approved_at`. The next run respects it immediately. No cache.

## Per-host check overrides

Expand Down Expand Up @@ -121,6 +142,12 @@ Pester tests in `tests/windows/lib/Get-ApprovalState.Tests.ps1` cover:
- Migration path: TODO.md `[x]` present, no approvals.json - migrates, writes file, logs
- Revocation: edit file, re-read, respects new state
- Shared read: succeeds while another handle holds the file for write
- The migration writes through `Write-StateFile` and never overwrites a file that appeared meanwhile

`tests/windows/lib/Write-StateFile.Tests.ps1` covers the replace recipe and its sharing and
first-write cases; `tests/windows/lib/Set-ApprovalState.Tests.ps1` covers each action, the
refusals, the schema check before the replace, the `approvals.lock` wait and the synced
`<StateBase>` refusal.

`tests/windows/lib/Get-MutedFindingKey.Tests.ps1` covers `mutes.json`: the schema, the per-check
key lookup, the fail-safe fallbacks, the shared read, and that the read creates no file or folder.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Orchestrator consults this list after check execution and before any action.
- User-load heuristic tripped.
- Service name appears in `approvals.json` `check_overrides.services.service_exclusions` (per-host user curation).
- `Start-Service` requires elevation the run lacks. Log `UNKNOWN` for the remediation attempt, do not auto-elevate.
- **Default state:** **DISABLED** until user sets `remediations.restart-stopped-service.approved: true` in `<StateBase>/state/approvals.json`. First run dry-modes it regardless.
- **Default state:** **DISABLED** until the user runs `/machine-health:setup apply approve=restart-stopped-service` (see [`../shared/approvals.md`](../shared/approvals.md)). First run dry-modes it regardless.
- **On failure:** corresponding `services` finding upgrades to `CRIT` with `"notes": "Restart-StoppedService failed for <svc>: <message>"`.

---
Expand All @@ -41,7 +41,7 @@ Orchestrator consults this list after check execution and before any action.
- `DryRun -eq $true`.
- User-load heuristic tripped.
- Free space on `C:` >15%, no longer WARN/CRIT, so no action (idempotent re-run safety).
- **Default state:** **DISABLED** until the user sets `remediations.clear-temp-files.approved: true` in `<StateBase>/state/approvals.json`. First run dry-modes it regardless.
- **Default state:** **DISABLED** until the user runs `/machine-health:setup apply approve=clear-temp-files` (see [`../shared/approvals.md`](../shared/approvals.md)). First run dry-modes it regardless.
- **Output contract:**
- `before`: `{ "temp_usage_bytes": <int>, "count": <int> }` for each target path.
- `after`: same structure, recomputed post-delete.
Expand Down Expand Up @@ -71,9 +71,9 @@ These remediations are proposed periodically in online discussions but are **not
## Approval workflow

1. First scheduled run produces a report with remediations flagged `DISABLED (pending approval)`.
2. Human reviews the report, then approves via `/machine-health:setup` (or edits `<StateBase>/state/approvals.json` to set `remediations.<id>.approved: true`). See [`../shared/approvals.md`](../shared/approvals.md) for full schema and examples.
2. Human reviews the report, then approves with `/machine-health:setup apply approve=<id>`, per [`../shared/approvals.md`](../shared/approvals.md).
3. Next run sees the approval, flips authorization to enabled, and attempts the remediation.
4. If the remediation causes trouble, the human flips `approved: true` back to `false`; the skill respects the revocation immediately on next run.
4. If the remediation causes trouble, the human runs `/machine-health:setup apply revoke=<id>`; the skill respects the revocation immediately on next run.

`approvals.json` is the single source of truth. The `TODO.md` shipped with the plugin is policy
documentation only; no checkbox in it drives behavior. One-time carry-forward of checkbox-era
Expand Down
Loading
Loading