feat: patching REST API, patching scope, run IDs from apply-updates and MCP

This commit is contained in:
2026-09-15 09:31:51 +00:00
parent 17c9f813fc
commit b5bbf28c63
11 changed files with 1697 additions and 28 deletions
+44 -6
View File
@@ -173,6 +173,37 @@ Skips are recorded and surfaced, not just logged: past the 1h grace window is
`missed`, an active run is `already_running`, and a schedule that no longer
parses is disabled rather than left spinning the loop every 30 seconds forever.
### Scheduled patching
Three collections: `maintenance_windows` (cron start, IANA zone, duration),
`patch_policies` (selector, window, `all|security`, `never|if_required`,
concurrency cap, channels) and `patch_runs` (one per firing or manual Apply
updates, one `servers[]` entry per target). All three are in
`ScopedCollections`.
**Runs are driven by database state, not goroutines.** A run can last hours; a
goroutine-driven run is stranded at `running` when its pod dies. `patchsched`
ticks every 30s inside the housekeeping leader: it claims due policies with the
workflowsched `next_run_at` pattern, then advances every running run. Every
decision is a pure function in `internal/patchrun` (`Advance`, `ApplyResult`,
`VerifyReboot`, `Finalize`) and every write is guarded by the server run's
current status, so a result landing mid-tick is never overwritten.
**Results do not cross the bus.** The pod holding the agent's stream writes
`PatchResult` straight into the run, found by `servers.command_id` and the
agent's own server ID. A reboot is settled by the first static inventory report
whose `boot_time_unix` is later than `rebooted_at`; a report during the
one-minute grace does not count.
**Old agents must never receive a scope.** An agent before
`patchrun.MinAgentVersion` ignores `scope` and installs everything, so policy
runs mark it `agent_too_old` and do not dispatch. A manual Apply updates still
sends such an agent the empty command and records "no result reported".
The next window starts after `max(now, windowEnd)`, so windows never overlap,
including across a daylight-saving fall-back. `patchsched` must not import
`services`; its dependencies are injected from `main.go`.
### Server tags and workflow targeting
A server carries `tags map[string]string` - lowercase `[a-z0-9_-]`, key ≤32,
@@ -337,10 +368,12 @@ need a PowerShell Gallery install on every host and fails on an air-gapped
fleet. `CurrentVersion` is empty on Windows and `NewVersion` carries the KB
article ID: a Windows update is not a version bump of a named package.
**The agent never reboots a host.** `ApplyUpdatesCmd` installs and stops there;
`inventory.reboot_required` reports that one is owed, set on the static snapshot
every 15 minutes. Linux fills it too, from `/var/run/reboot-required` or
`dnf needs-restarting -r`.
**The agent reboots a host only when a patch command asks and a reboot is
owed.** `ApplyUpdatesCmd` carries `scope`, `reboot_if_required` and
`deadline_unix`; an empty command still means "everything, no reboot". The
agent answers with `PatchResult` and, when rebooting, sends it first and then
restarts after a one-minute grace. `inventory.reboot_required` is still set on
the static snapshot every 15 minutes and at agent start.
### Package inventory and CVE findings
@@ -887,7 +920,7 @@ service Vantage {
}
```
`CommandStream` is the only streaming RPC: the agent authenticates once with `AgentReady`, then the server pushes `ServerCommand`s and the agent replies with `CommandResult`, `StepResult`, or `StepOutputChunk`.
`CommandStream` is the only streaming RPC: the agent authenticates once with `AgentReady`, then the server pushes `ServerCommand`s and the agent replies with `CommandResult`, `StepResult`, `StepOutputChunk`, or `PatchResult`. `AgentMessage` now also carries `PatchResult`, the answer to `ApplyUpdatesCmd`.
`ServerCommand` variants: `GenerateKeyCmd`, `DeleteKeyCmd`, `UpdateAgentCmd`, `ApplyUpdatesCmd`, `RunStepCmd`, `CleanupWorkspaceCmd`, `OpenProxyCmd`, `PingCmd`, `RefreshWorkloadsCmd`, `ControlWorkloadCmd`,
`WorkloadLogsCmd`.
@@ -955,6 +988,11 @@ status-pages GET,POST /status-pages · GET,PUT,DELETE /status-pages/:pageId (ow
GET,POST /status-pages/:pageId/incidents
PUT,DELETE /status-pages/:pageId/incidents/:incidentId
POST /status-pages/:pageId/incidents/:incidentId/updates
patching GET,POST /maintenance-windows · POST /maintenance-windows/preview
GET,PUT,DELETE /maintenance-windows/:id (writes: owner|admin)
GET,POST /patch-policies · GET,PUT,DELETE /patch-policies/:id
POST /patch-policies/:id/run-now (writes: owner|admin)
GET /patch-runs · GET /patch-runs/:runId · POST /patch-runs/:runId/cancel
audit GET /audit
agent GET /agent/latest-version
settings GET,PUT /settings · POST /settings/secrets-token (owner|admin)
@@ -1015,7 +1053,7 @@ plane, each of which this codebase enforces:
## MongoDB Collections
`servers` · `keys` · `assignments` · `orgs` · `users` · `auth_providers` · `settings` · `secrets` · `workflows` · `workflow_steps` · `workflow_runs` · `workflow_log_lines` · `workflow_log_seq` · `monitors` · `incidents` · `monitor_rollups` · `notification_channels` · `console_sessions` · `audit_logs` · `server_packages` · `vuln_findings` · `vuln_alert_rules` · `vulndb_meta` · `server_workloads` · `api_tokens` · `status_pages` · `status_incidents` · `migrations`
`servers` · `keys` · `assignments` · `orgs` · `users` · `auth_providers` · `settings` · `secrets` · `workflows` · `workflow_steps` · `workflow_runs` · `workflow_log_lines` · `workflow_log_seq` · `monitors` · `incidents` · `monitor_rollups` · `notification_channels` · `console_sessions` · `audit_logs` · `server_packages` · `vuln_findings` · `vuln_alert_rules` · `vulndb_meta` · `server_workloads` · `api_tokens` · `status_pages` · `status_incidents` · `maintenance_windows` · `patch_policies` · `patch_runs` · `migrations`
Every document except `migrations` carries `org_id`. Struct definitions are the source of truth - see `server/internal/models/`.