Add AllowedSelfUpdatePackageTypes and allowedCapabilityPackageType() so the
consumer accepts redflag_agent_self / redflag_helper_self / redflag_desktop_self
tokens without putting them in the package-manager allowlist. ProcessToken
dispatches self-update types to dedicated processAgentSelfToken,
processHelperSelfToken, processDesktopSelfToken handlers. ArtifactDownloader
interface extracted; inferred from reporter when available. Tests added for
allowlist invariants, stageClosureArtifact (local file path), and
installDesktopBinary (backup-and-replace).
Replace fmt.Printf / log.Printf calls in loop, config, crypto, and security
logger with TeeLogger.Info/Warning/Error. Buffer is persisted to events_buffer.json
and flushed to the server after each check-in. CRITICAL security events still
write to disk immediately then also enter the buffer for server delivery.
SecurityLogger.GetBatch() no longer falls through on CRITICAL before buffering.
Agent-side: reads /proc for all PIDs with 25+ fields (identity, resources,
state, disk I/O, elevation) plus related data on drill-down (open files,
sockets, pipes, env keys, memory map, namespaces, listening ports). Pure
/proc reads, no subprocess spawns.
Server-side: dedicated tables (agent_process_snapshots, agent_processes,
agent_process_related) with JSONB for related data. On-demand scan via
scan_processes command, last-10-snapshot retention. Four endpoints:
report, get latest, get detail, trigger scan.
UI: new Processes tab in agent detail with sortable/filterable table,
search by name/cmdline, state/user filters, and ProcessDetailModal with
tabs for Overview, Network, Files, Environment, Memory, Namespaces.
OSV fail-closed paths, safeTokenFilename traversal, bind check,
disallowed package types covered in agent/internal/supplychain.
Artifact hash resolution and dnf scanner test fixes. Server update
handlers extended for the local-approve flow with OSV version test
coverage.
ci.yml: vet, race tests, clippy, full web build, AI-attribution and
action-pin enforcement. release.yml: gate job verifies tag against
versions.go/docker-compose/Cargo/CHANGELOG, forward-only and on public,
before anything builds; web UI staged into the embed path (gitignored
dist made a bare go build ship an empty dashboard); binaries and docker
image must self-report the tag; release created via Gitea's own API.
scripts/release.sh is the operator path: checks runner, secret, branch,
versions, changelog — asks before every mutation, watches the run after.
bump-version.sh gains current-version display, dirty-tree warning,
duplicate check, changelog check, confirmation. build-secure-agent.sh
retired (bare go build, no version injection, single Makefile caller).
Old binary drops a marker (command_id, from/to) once the swap is
committed, on both the helper path and the legacy path. New binary
checks it at startup: running >= target just clears the marker
(check-in confirm still owns success); short of target means the swap
failed or rolled back, so it files a failed update_agent report under
the original command_id and the server clears is_updating right away
instead of sitting out the stuck-update timeout. Marker survives
failed reports for retry, drops on 409 or after 24h.
The agent runs as redflag-agent user and cannot read /proc/[pid]/environ
from the logged-in user's session processes (owner-only permissions).
Without this, discoverSessionDisplay() finds nothing — screenshot fails
on Wayland because XDG_RUNTIME_DIR/WAYLAND_DISPLAY are never discovered.
- Installer: setcap cap_sys_ptrace=eip after placing the binary
- Helper (agent self-update): restore setcap after atomic replace,
since rename() creates a new inode and strips file capabilities
Non-fatal if setcap is unavailable — agent degrades gracefully,
just loses screenshot and per-process telemetry.
GetTopProcesses(limit) reads /proc/[pid]/stat and /proc/[pid]/status
directly on Linux — no subprocess spawn, ~30ms for 5 processes.
Windows uses tasklist CSV, macOS uses ps aux.
Wired into reportSystemInfo via metadata[top_processes] — flows
through the existing merge path, no server schema change needed.
UI already reads agent.metadata.top_processes and renders the table.
Added tickCount to the polling loop for future N-tick throttling
(heartbeat-level reporting cadence).
Test output confirms real data:
1. Isolated Web Co (pid=10319) cpu=4.5% mem=3.4%
2. firefox (pid=4090) cpu=3.2% mem=4.6%
3. qs (pid=3611) cpu=1.6% mem=1.7%
FEAT-003 core (design: RAF/security/06-standalone-authority.md, approved
2026-06-10). On a host with no fleet server, the trust boundary preserved is
root-vs-unprivileged: a root-owned 0600 Ed25519 key signs capability tokens
via a new privileged helper invocation; redflag-local membership lets you
request a mint, never perform one.
Helper gains the mint subcommand: validates forward-only ops, mintable-type
allowlist (no agent-self), host agent-id bind, closure shape (64-hex sha256
fail-closed), hard-coded 15-minute gate-evidence freshness with future-dating
rejection, override-reason requirement for vulnerable/unreachable/overridden
verdicts, duplicate request_id dedupe, journal-before-emission. --init-key /
--retire-key manage the authority lifecycle (retire = the fleet-join swap).
Deny taxonomy 22-25. Round-trip test proves a minted token passes the execute
path's own verification and parses as the wire CapabilityToken.
Agent gains POST /v1/actions/approve-update (single-flight, 409/503 mapping):
fleet-mode refusal, dnf/apt dry-run closure resolve + hash pin (no pin, no
mint), best-effort OSV.dev closure check with honest verdicts (unreachable is
never silent-clear), mint via sudo systemd-run mirroring the execute grant,
then the unchanged verify+execute path. Provisioning script sets up the
journal dir (root:redflag-local 2750 setgid), mint request dir, key init, and
the pinned mint sudoers line.
Server becomes self-contained: web/dist embedded via go:embed
(server/internal/webui), SPA served from the binary with JSON-404 guard on
/api paths, nginx web container removed from compose (31336 now maps to the
server). Clean checkouts without the UI copy build API-only.
Agent local API gains its first write endpoint, POST /v1/actions/trigger-scan
(FEAT-002 write path): group-ACL authorized, single-flight, 202/409/503
semantics. Registered agents run the same HandleScanUpdates path as a signed
scan command (empty command_id, no ack tracking); standalone agents scan
through the orchestrator into the local read model only. Also repairs
localapi tests left uncompilable by the desktop-provider parameter.
- README: version v0.2.6.8, corrected stale gate claim, updated changelog
- .env.example: merged two competing files into one, deleted bootstrap duplicate
- ErrorBoundary: new component wrapping app, prevents white-screen crashes
- Layout sidebar: version display from /api/health, Docs link to GitHub
- client-logger: debug/trace logger gated behind localStorage.redflag_debug=1,
routes through existing /logs/client-error server endpoint (ETHOS #1)
- All web console.log calls rerouted through client-logger instead of deleted
- Server health endpoint returns version field
- Server accepts client_debug/client_trace in error_type validation
- Dockerfiles: pinned alpine:latest->3.21, nginx:alpine->1.27-alpine,
added HEALTHCHECK directives
- docker-compose: healthcheck blocks for server and web services
- .dockerignore: created to slim Docker build context
UI: move screenshot/Sunshine square into the System Information card
header (top-right, w-48 aspect-video) instead of a standalone block
above the card. Same click logic, smaller size to fit the header row.
Agent: captureScreenLinux now discovers DISPLAY/WAYLAND_DISPLAY/
XDG_RUNTIME_DIR from /proc environ entries so the service (which
doesn't inherit display vars from systemd) can reach the session.
Tool priority: scrot → grim → magick import → import.
Adds bytes/strconv/strings imports for discoverSessionDisplayEnv.
LocalAgentApp: dark default with ☀/☾ toggle; palette-driven inline
styles (DARK/LIGHT objects) so both modes work without Tailwind dark
mode configuration. Dense Novell-style layout: status strip, section
headers with red accent bar, monospace identifiers, dot indicators per
scanner status. No Tailwind class changes — web/package-lock.json
untouched.
Version bump 0.2.6.7 → 0.2.6.8.
After successful install (confirm_dependencies or capability receipt),
pin the installed version as selected_version instead of clearing it.
This prevents subsequent scans from silently advancing to a newer
unapproved version. The pin is the enforcement mechanism.
- 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.
Three layered causes:
1. config_v5_migration had no executor phase — structurally impossible
to mark complete.
2. StateManager.loadConfig unmarshaled into typed config.Config, but
install template writes version as JSON number while Config types
it as string — migration completion never persisted to disk.
3. readConfigVersion only parsed float64, so normalized string read 0
and re-triggered.
Fix: StateManager is map-based (immune to field-type drift). Executor
has real config_v5 phase (bump + mark complete). parseConfigVersion
accepts both number and string. validateMigration uses MkdirAll then
stat (fixes false 'state dir not found' on Windows).
Service:
- Build complete LoopContext in Windows service runAgent (was missing
ReceiptTracker, ConfirmedTracker, scanners, circuit breakers,
kernel enforcer — nil ReceiptTracker caused immediate panic).
- Wire process logger so log.Printf writes to agent.log on Windows.
- Parse CPU info JSON from PowerShell fallback (wmic absent on
modern Windows; old code tried CSV parse on JSON output).
Local state cache:
- Agent writes status, scan results, capability token state to
local disk cache. Enables local observability without a server
round-trip. Handlers expose /local/state, /local/status endpoints.
- Record agent status (online/backoff) and capability token metrics
to local cache during polling loop.
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.
getServerURL returned localhost:31337 (the server's own bind address), so
a remote agent's binary/manifest/config fetches pointed at itself and
failed with "Unable to connect". Prefer the host the client actually
reached us on (Host + X-Forwarded-Proto, which nginx forwards), falling
back to PublicURL then the configured bind addr. Also set charset=utf-8 so
irm stops mangling the ✓/⚠ glyphs.
Windows PowerShell 5.1's parser chokes on here-strings in LF-only .ps1
files — the embedded config template blew up with a cascade of
"Unexpected token ':'". Convert the windows script to CRLF at serve time
(parses clean in 5.1 and 7; .ps1 should be CRLF anyway). Linux stays LF.
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.
the supply-chain check was judging available_version while the gate could
install a different one. re-run OSV against the version we actually install
(operator-pinned, else gated), and auto-pin the newest soak-aged clean
version when the gate's enforced — forward-only, operator force-pin wins.
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.
last half of S8. the pool bump and bounded background took most of the pressure
off, but a full fleet can still pin every connection. now there's a valve:
- middleware reads db.DB.Stats(); at the saturation threshold it answers 503 +
retry-after instead of letting writes queue into a deadlock
- fails open — unlimited pool never sheds, default only trips at 100% in-use.
tunable via REDFLAG_DB_SHED_UTILIZATION and _RETRY_AFTER_SECONDS
- wired to the agent write group only. health, metrics, auth, register/renew
and the dashboard stay reachable so you can still watch it and log in while
it's hot
table-driven test across the branches, race clean.
the encrypt/decrypt hooks existed but the write path wrote values straight to
the column, so a sensitive setting would have gone in as plaintext. closed it:
- sensitive values serialize + encrypt before they persist (base64 aes-gcm over
the json), and updates now carry the is_encrypted flag through
- non-sensitive settings are untouched — still plain json, same as before
- audit log redacts sensitive old/new values instead of recording them raw
round-trip test proves sensitive values never hit the column in plaintext and
still decrypt back; non-sensitive stay readable. no backfill needed — nothing
writes secrets through this path yet, the defaults are empty.
builds clean, vet quiet, tests pass.
OBS-001A: an authenticated /metrics so something other than a human reading
logs can watch the box. no new dep — emits prometheus text straight from the
counters we already keep:
- /metrics behind a dedicated bearer token (sha-256 hash stored, plaintext only
from REDFLAG_METRICS_TOKEN for bootstrap). constant-time compare, rotates
without a restart, disabled by default, never an open route
- exports db pool, taskrunner snapshot, scheduler + queue, breaker state, and
the deferred-advisory count — read live on each scrape, bounded labels only
- settings + migration 054 for observability.metrics_enabled / _token_hash
also fixed migration 046 — it added a column and an index without IF NOT EXISTS
and backfilled off a column it then drops, so it couldn't survive a second run.
guarded every step; the idempotency lint is green again. only the migrations
the runner hasn't recorded see the change, so live dbs don't care.
builds clean, vet quiet, new tests pass.
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.
osv.dev or repology going dark used to mean every check sat there burning its
30s timeout, one after another. now there's a breaker (ported the agent's, it's
already proven) wrapping both:
- osv: one breaker over the batch + single-query paths. trips after 5 fails in
a minute, fails open while tripped — an unreachable advisory feed never blocks
a patch. that's the whole sovereignty bet
- repology: same deal, best-effort, 404 doesn't count against it
- both visible at /health/tasks so you can see them trip and heal
db-pool shedding (503 + retry-after) is the other half — left it for later, the
pool bump + bounded background already took most of that pressure off.
race detector's clean.
three more off the scale list:
- rate-limit map now gets swept on a cadence (taskrunner.Every) instead of
growing forever — nobody was calling the cleanup. first old ticker moved
onto the runner
- subsystem load was one db query per agent at startup; now it's a single
ANY($1) for all the online ones. 100 agents, 1 query
- outbound http clients (osv, registries, upstream, agent) were inheriting
the stock transport that keeps 2 idle conns per host — so every scan burst
re-dialed. shared tuned transport now, 10 per host, 90s idle
builds clean both modules.
server was sized for a campfire, not a fleet. 25 db connections, every agent
report flinging goroutines into the void, the syncer plodding one repo at a
time while clutching a lock nobody needed. loosened the choke points:
- db pool 25 -> 100 + connection lifetime, all env-tunable
- bounded pool for the report-path fire-and-forget work; /health/tasks to
watch it breathe. no more unbounded goroutine spray per report
- upstream syncer runs concurrent now, dropped the dead mutex around repology
fetches, reconciler single-flights instead of locking through the whole crawl
- scheduler caps jobs per tick so an aligned fleet can't stampede the db
- swatted a context-cancel bug that was quietly killing immediate syncs
builds clean, race detector's calm.
approval stopped re-scanning osv; it just reads what detection already
found. soak gate + age gate are real settings now (env→db→default), and
the dead soak-override column + table got composted.
Treats each ecosystem scan as the authoritative full set for that
(agent, ecosystem) pair. Packages absent from a successful scan that
are still in a waiting state (pending/approved) are closed to installed
with out-of-band provenance — no operator action required.
State machine:
- Added pending/approved → installed edges (out-of-band resolution path)
- Added installed → pending edge (reactivation when a new version reappears)
- ReconcileFromScan updated to match: installed now reopens, ignored/failed preserved
Server (ReportUpdates):
- closeScanAbsentRows goroutine: diff waiting rows against reported set,
transition absent rows via transitionStatus (guarded UPDATE, idempotent)
- Provenance stamping: redflag_receipt if a consumed capability token exists,
out_of_band otherwise
- System event emitted per closure for audit trail
- scanEcosystemSupported gate: dnf/apt only; failed/partial scans never close rows
Agent:
- UpdateReport extended with Ecosystem + ScanSucceeded fields
- APT/DNF scan handlers now always report on successful scan (even 0 updates)
- HandleScanAPT/DNF/Updates: report failure is non-fatal (transport problem,
scan succeeded locally)
Queries:
- GetTrackedNonResting: scoped to pending/approved only — in-flight states
(checking_dependencies, pending_dependencies, installing) are orchestrator-owned
- TransitionByID: routes closure through the state machine
- HasConsumedTokenForUpdate: provenance check for the reconciler
- UpdateCurrentStateInTx SQL CASE: installed now reopens to pending on re-scan
Tests: reconcile_test.go (5 unit tests including load-bearing
TestWaitingStatesResolveOutOfBand), reconcile_test.go handler tests (7 sub-tests).
Bump: v0.2.6.1
- 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