- evaluateSupplyChainHold checks version row osv_status for target
version, closing hole where we OSV-check one version but install
another.
- confirm_dependencies uses TransitionByPackageFrom with source-state
guard (StatusInstalling → Installed/Failed).
- Clears pinned version on successful install.
- Version bump to v0.2.6.7.
- useHeartbeatStatus reads from agent metadata (single source of
truth) instead of separate endpoint (eliminated split-brain).
- Online/offline threshold fixed to 10min to match server (was 15min).
- Dashboard polling lowered from 30s to 15s.
- Toggle feedback: invalidate agent queries immediately, clear
loading state after 2s.
- Removed command-table fallback from GetHeartbeatStatus endpoint,
dead helper functions, and unused GetRecentHeartbeatCommands.
getServerUrl() was stripping the port when hostname != localhost,
generating install commands on port 80. Now uses window.location.port
directly — the browser's host:port is always reachable by the agent.
The Windows installer relied on a Mandatory=$true -Token param, but the
one-liner ran the script with no -Token -> PowerShell dropped to an
interactive prompt and the install looked hung.
Bake the token/server into the rendered script like the Linux template
already does, drop param() and #Requires (both no-op under iex; runtime
admin check still enforces elevation), and switch the command to
'irm ... | iex' so the body actually pipes. -Token/-Server/-Skip become
RF_TOKEN/RF_SERVER/RF_SKIP_SERVICE_INSTALL env overrides.
observe-only: the agent folds detected integrations into its system-info
report under metadata.integrations; the dashboard renders what it reports.
nothing reaches into the host — an integration can be watched, not commanded.
the breaker fails open so a down osv never blocks a patch — good — but the
auto-confirm gate is fail-closed, so a dark feed quietly stops auto-approval
and parks packages unvetted. that truth was sitting in the logs where nobody
looks. now it's a banner.
- /api/v1/health/advisory: breaker state + count of deferred packages
(self-healing — a successful recheck clears the flag) + a degraded flag
- amber bar in the layout, only when degraded: "feed offline, auto-approval
suspended, manual still works." says feed-down isn't patching-down
- narrow slice of the gate-visibility work; the full posture panel stays in
its own session
builds clean, tsc's happy.
- Layout/Dashboard refresh buttons use queryClient.invalidateQueries, not reload()
- LiveOperations view-update/view-agent use navigate(), not window.open new tab
- Updates.tsx filter sync uses setSearchParams, not window.history.replaceState
- ChatTimeline: View Agent shows hostname; package links go to /updates/package/:type/:name
- #1: ReconcileAll goroutine uses context.Background, not request ctx
- #4: InstallVersion now checks maintenance window before dry-run
- #5+#23: tickAliases fetches stale slugs once before loop; rename shadowing var
- #6: UpsertReconciled sql.ErrNoRows on manual conflict is a no-op, not a warn
- #7: normalizeRepoToEcosystem uses ordered slice, not non-deterministic map
- #8: MatchByContainer ILIKE escapes % and _ metacharacters via REPLACE
- #9: EnqueueDryRun uses target_version key for selected_version, keeps available_version for freshness
- #13: InstallVersion drops second GetUpdateByID, mutates struct locally
- #14: EnrichFromMetadata reserved map is package-level var, not per-call alloc
- #21: recordGateOverride shared helper; recordSupplyChain/SoakOverride delegate to it
- store.ts: remove dead notificationsEnabled setting (no callers outside store)
Reversible AES-256-GCM encryption for registration tokens so the
install one-liner can auto-fill the token value again. Migration 049
adds token_encrypted column; token_hash kept for lookup.
AgentManagement.tsx gets platform cards (Linux, Windows; macOS = soon)
with generateInstallCommand for all platforms. Inert until backend lands.
system_event_logger.go extracted from deleted event_stream.go —
SystemEventLogger survives, unified substrate does not.
security_settings_service.go and secrets_manager.go simplified.
GetFleetActivity replaces GetAllUnifiedHistory — filters now apply once on
the outer aliased result instead of per-arm, fixing the agent_id ambiguity
when the logs arm joins update_packages (the 500).
Three new UNION arms: update_events, update_version_history, system_events.
Package name joined to update_logs via update_package_id.
Dead code removed: event_stream.go (handler + service), UnifiedEventTimeline.tsx,
useEvents.ts — orphaned from the abandoned unified path (HANDOFF-2026-06-05).
SystemEventLogger extracted to system_event_logger.go (unstaged, next commit).
Lifecycle:
- ReopenUpdate + ResolveUpdate replace RetryUpdate; routes for
GET /updates/:id/lifecycle and POST reopen/resolve
- confirmUpdateCommand marks update_agent completed on version attestation
- migration 048: started/running added to update_logs.result
- helper atomic_replace_binary: copy-to-sibling then rename() (ETXTBSY)
Live operations:
- event_stream service + /events endpoints, LiveOperations page
- capability-token queries for the live view
History + CVE:
- History page reads /events/recent: filterable lifecycle/command/
system/orchestrator timeline with agent crosslinks
- CVE drill-down: OSV parse carries CVSS vector, fixed version, published
date, severity; issuer-linked aliases (CVE->NVD, GHSA->GitHub, ALSA->errata)
- DependencyClosureTree: one shared closure component in update detail
- STARTED (blue spinner) and PARTIAL (amber) result badges
The schema only allowed success/failed/partial. The agent sends 'started' as a
progress report and 'partial_failure' when a multi-scanner scan had mixed
results. Both were being remapped to 'failed' by the server's fallthrough
default, so the timeline showed a red FAILED badge for 'starting agent update'
and for scans where only some scanners errored.
Two-value migration (no-data):
- Add 'started' and 'running' to the update_logs.result CHECK constraint
- Add them to isValidResult so they pass through without remapping
- Fix the fallthrough switch: partial_failure -> partial (not -> failed)
- Clean up if/else chain to a switch while we're in there
UI:
- STARTED badge (blue spinner) for progress reports
- PARTIAL badge (amber triangle) for partial results
- Both ChatTimeline and HistoryTimeline updated
Event renderer:
- 'started' -> 'Agent binary update initiated'
- 'partial' for install/update_agent cases
dnf DryRun: --assumeno cancels the transaction, so DNF exits non-zero
even on a dry run that resolved cleanly — the old check failed those
(curl-style single-package upgrades with no extra deps showed FAILED).
But non-empty stdout is not success either: 'No match for argument',
'Nothing to do', and 'Error:' all print output and exit non-zero, and
treating them as success would mint a capability token for a transaction
that never installs (fail-open). Gate on an actually-resolved
transaction (a 'Transaction Summary' block, which never coexists with
'Nothing to do') instead.
web logout: clear the zustand persist key (auth-storage) alongside
auth_token and user, so a JWT from a prior server reinstall (JWT_SECRET
rotation) does not survive a logout + re-login cycle. Drop the redundant
localStorage removal in Layout — the store owns logout cleanup.
Route every current_package_state status change through one transitionStatus
path: read the observed status, validate against PackageStatusTransitions,
run a status-guarded UPDATE, record terminal history. Replaces ten raw-SQL
transition functions whose WHERE guards validated nothing and silently
no-op'd on an illegal state. ApproveUpdate, the Reject/Install/Set* family,
BulkApprove and UpdatePackageStatus now share the core; illegal moves return
a named from->to error instead of a silent miss, and concurrent callers are
caught by the guarded row count.
Migration 047 renames the terminal success state updated -> installed in
current_package_state and update_version_history, realigning both CHECK
constraints with the Go PackageStatus/HistoryStatus constants.
UpdateStats updated_updates -> installed_updates to match.
UpdateCurrentStateInTx documents its reconcile CASE as the SQL twin of
models.ReconcileFromScan so the two stay in lockstep.
Dashboard: vulnerable-package count surfaced in AttentionPanel, plus a
Vulnerable quick-filter on the Updates view.
SEC-001: Registration tokens stored as SHA-256 hashes. Migration 046 adds
token_hash column, backfills from plaintext, drops token column. All queries
use hash. Token plaintext shown once at creation (reveal panel in UI), never
retrievable again. Follows the refresh-token pattern.
SEC-005: README "no sanitization" claims corrected — code correctly sanitizes
against log injection (ANSI stripping, control char replacement, truncation).
Wording updated to match reality.
SEC-008: Command creation with idempotency_key uses ON CONFLICT DO NOTHING
instead of blind insert. Prevents duplicate command execution.
Trust model: Ed25519 key rotation documented — signing_keys table supports
multiple concurrent active keys with a sliding window for zero-downtime
rotation. OSV.dev ecosystem coverage updated (apt, dnf added).
A retried command carries the same action/result as its original, so the
history read as a fresh attempt. The lineage already lived in
agent_commands.retried_from_id — it just was not projected.
GetAllUnifiedHistory now selects is_retry + retried_from_id (both UNION
halves; logs are always false/null); UnifiedHistoryItem carries them; the
handler prefixes the narrative with "Retry — ". ChatTimeline composes its
own command sentences (narrative is only a log fallback), so it gets the
same prefix guarded by entry.is_retry, matching the is_retry/
retried_from_id convention LiveOperations already consumes.
Also drop a leftover heartbeat console.log debug block in Agents.tsx.
Extract the agent polling loop from main.go into internal/agent/loop.go
so the Windows service and the CLI agent share one code path. The loop
now reads jitter cap and backoff curve from PollingConfig (struct with
merge + file/env defaults) instead of hardcoding 30s/10s/300s. Machine
ID resolution uses the canonical system.GetMachineID() in both the
registration and runtime paths, removing the inline 'unknown-' fallback.
Stuck command retries are parameterized (maxRetries arg) rather than
hardcoded to < 5.
Restructure settings into a uniform hub-of-cards pattern: extract inline
Account Settings into /settings/general, un-orphan SecuritySettings with
working /settings/security/:tab routes. Add fleet-wide polling resilience
tuning (jitter_max_seconds, backoff_base_seconds, backoff_max_seconds)
as operational settings — stored in security_settings, delivered over
GET /api/v1/agents/:id/config, merged into the agent's local config at
runtime with a 15-minute refresh cadence. Frontend includes AgentPolling
page, hook, hub card, and route.
Lands the long-dropped in-flight work plus two slices of the pinning-mirror direction.
Registry-gap closure (in-flight, was repeatedly dropped):
- Agent resolves canonical artifact hashes from its own signed repo metadata
(dnf download + rpm header; apt-cache policy+show) — server no longer serves a
placeholder dnf URL and says so honestly.
- Server pins the agent-reported closure and mints the capability token at the
dependency-confirmation boundary; receipt updates package status.
Slice 1 — package detail pane:
- GET /updates/:id/fleet (cross-agent view). Detail pane gains Supply Chain card
(pinned sha256, published/age, age-gate verdict, resolved closure) and Affected
Agents card (per-host version delta + status, click-to-pivot).
Package-centric Updates list:
- ListAggregatedPackages rollup (GET /packages): one row per package across the
fleet — agent/version counts, max severity, vuln + hash-pin rollups, status
breakdown. List view rewritten to package rows that drill into the fleet view.
Slice 2 — version timeline catalog:
- migration 043 package_versions; idempotent upsert populated at scan, enriched at
approval (OSV posture, publish date, hash) and at closure pin (per-artifact hash).
- GET /updates/:id/versions + Version Timeline card.
UI: description overflow fix, shared table density px-6->px-4, status label cleanup.
Version: 0.2.0.7 across versions.go, docker-compose, Makefile (Makefile was stale at
0.2.0.3/0.2.0).
- AgentUpdatesEnhanced: ['active-commands'] → ['activeCommands'] (hyphenated key
never matched the camelCase query key, so invalidation was silently dead)
- useUpdates (install + approve): invalidate ['dashboard-stats'] and
['activeCommands'] on success so Dashboard and Live Operations react without
waiting for their independent poll cycles
- Updates.tsx handleConfirmDependencies: replace window.location.reload() with
targeted queryClient.invalidateQueries calls (BUG-017 pattern)
- LiveOperations: updateId = cmd.params?.update_id || cmd.id so "View Update
Details" navigates to the correct update package, not the command record
- useUpdates query: add refetchInterval: 30000 / staleTime: 15000 so agent-side
completions surface without window-focus or manual refresh
- updates.go ReportLog: emit system_event (agent_update/failed) when an
update_agent or verify_command command returns result=failed, using
RenderUpdateLog for operator-facing narrative
Three structural pieces that only make sense as a unit: the dispatch
chokepoint queries the policy table, and the event renderer is consumed by
the same handlers the policy gates guard.
B. Auto-heartbeat at the dispatch chokepoint (agents.go, models/command.go)
- models.RequiresRapidPolling(commandType): central classification (no
per-handler opt-in for rapid-polling commands).
- signAndCreateCommand auto-queues enable_heartbeat (Source=system)
ahead of any rapid-polling command, unless the agent is already in an
active heartbeat window.
- TimeoutService.reconcileAgentUpdates: effectiveUpdateTimeout() reads
operational.update_stuck_minutes live (no restart needed).
C. Event renderer (services/event_renderer.go NEW)
- RenderSystemEvent / RenderUpdateLog: single source of operator-facing
verbiage.
- Narrative field on SystemEvent / UpdateLog / UnifiedHistoryItem
(JSON-only, not persisted). Populated in agent_events.go GetAgentEvents
and updates.go GetAllLogs.
- ChatTimeline.tsx consults narrative only at the prior fallback line —
real stdout / package extraction branches untouched.
D. Policy table (migration 038 + security_settings_service helpers)
- policy.allow_dry_runs (default true): updates.go::InstallUpdate
returns 403 when false.
- policy.require_nonce (default true): agent_updates.go::UpdateAgent
skips nonce validation when false (logged at INFO).
- policy.auto_heartbeat_enabled (default true): agents.go gates the
auto-heartbeat side-effect.
- operational.update_stuck_minutes (default 5): TimeoutService reads
live for reconcile threshold.
- GetPolicyBool / GetOperationalInt on SecuritySettingsService.
Forward-only (no policy.allow_downgrade) is ETHOS §2 doctrine, not a knob.
- CRITICAL-008: Fix sudoers templates in sudoers.go + linux.sh.tmpl to match
actual agent DNF/APT commands. Remove stale dnf refresh subcommand from
security.go AllowedCommands; align both templates with agent's real flags.
- CRITICAL-004+005 Phase 1: Remove demo-mode lie from windows.go installUpdates().
Failed installs now return error instead of false success. Add stderr checks
to wuauclt path. Hardcode GetPendingUpdates() replaced with error noting
go-ole COM API will be the real implementation.
- CRITICAL-006: Replace 15 lines of fmt.Printf debug noise in getWindowsCPUInfo()
with 5 structured log.Printf lines (ETHOS [TAG] format). Remove intermediate
parse-progress lines that had no diagnostic value.
- CRITICAL-007: Extract shared RunPollingLoop exported from agent package so
the Windows service (service/windows.go) calls the same loop as the console
agent instead of maintaining a forked copy with 5 duplicated handler functions.
Delete the 5 dead forked handlers (~500 lines). Add StopCh field for clean
service shutdown.
- P3-BUG-004: Delete dead deriveKeyFromNonce() and decryptAES256GCM() from
agent_update.go (~35 lines). Remove now-unused crypto/aes and crypto/cipher
imports.
Also included: pre-existing uncommitted work on dispatch, agent_updates,
AgentUpdatesModal, and reboot handler.
Server / agent install pipeline:
- Restore dry_run_update, confirm_dependencies, install_updates,
enable_heartbeat, disable_heartbeat handlers on the agent side (lost in
the TD-001 god-function refactor at 9da5134e); wire them through
handlers/dispatch.go so the cross-platform agent loop dispatches them
alongside scans and update_agent.
- Wire JWT renewal into the polling loop on 401 (RenewToken existed in the
client but was dead code in loop.go).
- Self-update path now shells through sudo for cp/chmod/systemctl restart,
matching the redflag-agent user's hardened systemd unit.
Server build orchestrator:
- BuildAndSignAgent now reuses the existing signed package row when the
on-disk binary's checksum matches the stored one. Previously the server
re-signed and inserted 4 fresh rows on every boot, leaving dozens of
duplicate agent_update_packages entries.
- CreateUpdatePackage is now ON CONFLICT (version, platform, architecture)
DO UPDATE so a fresh build of the same version replaces in place.
- New migration 037: dedupes existing rows (keep newest per tuple) and
enforces UNIQUE (version, platform, architecture).
- Drop dead verification.go endpoint stub — architecturally broken in a
pull-only polling model.
Dashboard:
- AgentUpdatesModal filters packages to the selected agents' os_type and
os_architecture, dedupes by (version, platform, arch), and renders
platform/arch together so 32/64-bit differentiate visually. Drops the
platform dropdown (now agent-driven).
Install script template:
- Fix server_public_key + initial_binary.sig ownership so the agent user
can overwrite them; convert hex key to raw 32 bytes inline.
- Add sudoers entries for the agent's self-update cp/chmod/systemctl path.
Downloads handler resolves ?version=latest to AgentVersion so install
scripts pull a signed package instead of a 404.
Version bumped to 0.2.0.3 across versions.go, docker-compose, Makefile,
downloads.go, security min_agent_version.
Offline agent status is already prominent in the main agents list and stats
panel. Duplicating it in the attention feed added noise without urgency signal.
Removed WifiOff import and offline severity rank.
generateEnvContent was appending REDFLAG_SIGNING_PRIVATE_KEY after
server-side createSharedEnvContentForDisplay had already embedded it,
producing two copies in the setup output.
Surface the gitea/gitlab/bitbucket/git adapters in the
UpstreamTracking page so operators can actually pick them from the
form. Three changes:
- UpstreamSource union in types/index.ts gains 'gitea' | 'gitlab' |
'bitbucket' | 'git'. The /admin/upstream API already returns these
in its "sources" array (handler reads Registry.Names()), so the
dropdown picks them up automatically once the union allows.
- New sourceRefHint(source) returns per-source placeholder + help
text so the source_ref field documents itself instead of always
saying "e.g. postgresql". Each entry names the required env vars
so operators don't 404 their way to discovering REDFLAG_GITHUB_TOKEN.
- New sourceHref(s) replaces the hard-coded ternary that only knew
repology and endoflife. Sources without a stable public URL pattern
(gitea/anitya/npm/pypi, where host varies per deployment) return
null and skip the "Source" link rather than guessing wrong.
COMMON_SEEDS chips deliberately unchanged — those are the "one-click
to a working dashboard" path for new operators, all on endoflife
because EOL coverage is what justifies the chip. The form is the path
for everything else.
The agent runs apt/dnf/winget/windows on their own schedules (per-scanner
agent_subsystems rows still exist server-side) but the UI now collapses
them into a single "System Update Scanner" row. Chips on the description
line (APT, DNF, WINGET, Windows Update) light up from
agent.metadata.available_scanners — the real signal from
syncAvailableScanners — rather than the old os_type string heuristic
that sniffed "fedora" / "debian" out of the platform field.
The row is synthesized client-side from the per-scanner backers; no
'updates' DB row needed (the zombie kill landed in 7fb61a36). Toggle,
auto-run, interval, and Scan on the unified row cascade to every backing
per-scanner subsystem that actually exists for the agent.
Widened AgentSubsystem.subsystem type to string — the narrow union was
stale (DB has apt/dnf/winget/windows/storage/system/docker, plus the
synthetic 'updates' aggregate the UI produces).
Removed the os-string heuristic helper and its console.log noise.