Skip to content

Linux port: shared core, Qt6 GUI, Velopack AppImage self-update, CI - #14

Open
Negative-Star-Innovators wants to merge 20 commits into
mainfrom
feature/linux-port
Open

Linux port: shared core, Qt6 GUI, Velopack AppImage self-update, CI#14
Negative-Star-Innovators wants to merge 20 commits into
mainfrom
feature/linux-port

Conversation

@Negative-Star-Innovators

Copy link
Copy Markdown
Owner

Summary

Linux port of Agent Redactor: shared C++ core, a thin Linux engine/CLI binary, and a Qt6 Widgets GUI with tray, autostart and Velopack self-update via AppImage. Also adds the build-linux CI workflow.

Architecture: all backend logic lives in the shared core/ library (now compiled by both platforms); each OS has a thin engine stub (windows/engine/main.cpp, linux/engine/main.cpp) and a native GUI frontend (WinUI 3 on Windows, Qt6 on Linux). This structure is what a future macOS port (SwiftUI over the same core) will plug into.

What's in here

  • Core portability (e318562): POSIX sockets, libcurl, OpenSSL, libsecret and XDG paths behind platform abstractions. Every #ifdef _WIN32 branch is the original code path; Windows-visible changes are additive only (e.g. engineVersion in /status, LastStatus() on the control client). Windows project files got one-line updates for two headers/sources that moved into core/.
  • Linux engine + CLI (06fdfb0): same CLI surface and control API as Windows. Protection on Linux is a typed in-app master password (PBKDF2-HMAC-SHA256 → AES-256-GCM), not the OS login password.
  • Tests (c48caf8): tests/cli, tests/migration and a new tests/linux suite all run green on Ubuntu (37p/2s, 5p/2s, 8p).
  • Qt GUI (b1e2493, e1141e4): profile sidebar + settings cards mirroring the Windows Home surface, lock overlay, StatusNotifier tray (verified on Ubuntu GNOME), XDG autostart with two-way settings sync, systemd user unit template. English-only UI but structured for translation (tr() + TranslatorLoader driven by the existing appLanguage setting, RTL-aware).
  • Self-update + packaging (ec55708): Velopack C/C++ lib (pinned 1.2.0, fetched by linux/fetch-velopack.sh, not vendored in git) behind an AR_SELFRELEASE CMake option mirroring AGENTREDACTOR_SELFRELEASE. Startup + manual update check, restart-now/later prompt, engine respawn on version mismatch. linux/build-release.sh packs the AppImage (channel linux, same R2 bucket). Same loopback-only AGENTREDACTOR_UPDATE_FEED / AGENTREDACTOR_UPDATE_AUTOAPPLY test hooks as Windows, plus an update E2E that verifies a real AppImage self-update against a local feed.
  • CI (f341ca9): .github/workflows/build-linux.yml — builds, runs all three suites, packs the AppImage as a PR artifact; publishes the linux channel to R2 only on v* tags or manual publish dispatch (same gating/secrets as release-selfrelease.yml).
  • Feed worker: cloudflare/src/routes/updates.js allowlist extended with the linux channel and *.AppImage (additive; deploy stays manual).

Testing

  • Ubuntu 26.04 VM: all suites green; GUI manually verified on Wayland/GNOME (window + tray); AppImage built and self-update E2E passed with a real download/apply/restart cycle.
  • Windows behavior unchanged by construction (original code preserved behind _WIN32 branches); the existing Windows workflows are untouched and run on this PR for verification.

Not in scope (per plan)

macOS, Linux ARM64, Linux localization beyond English, any control-API shape changes.

Negative Star Innovators added 20 commits August 18, 2026 07:51
Top-level linux/CMakeLists.txt driver building the core static library
(engine and gui subdirectories slot in later), per-OS source selection in
core/CMakeLists.txt with curl/OpenSSL/libsecret/Threads on Linux, and
build instructions in linux/README.md.
- platform_compat.h: MSVC/POSIX shims (_snwprintf, _wtof, localtime_s,
  _wgetenv, SOCKET typedef, Winsock constants), keeping the public
  SOCKET-based http_server surface unchanged
- http_server: POSIX sockets (select nfds fix, socklen_t); behavior
  (loopbackOnly, dual-stack, streaming SendChunk) identical
- utils: UTF-8<->wstring via codecvt, XDG config path fallback
  (~/.config/agentredactor, AGENTREDACTOR_CONFIG_DIR still wins),
  /proc/self/exe, RFC4122 v4 UUID, libcurl implementations of
  HttpGetString/HttpDownloadFile/HttpDownloadFileSegmented
- proxy_engine: libcurl upstream client (streaming SSE, credential header
  substitution, gzip/deflate) behind #ifdef; WinHTTP branch untouched
- control_server: token via RAND_bytes, control.json with 0600 perms
- secure_storage: shared interface moves to core/include; Windows impl
  (DPAPI/CNG/Hello) unchanged; new Linux impl with OpenSSL AES-256-GCM,
  libsecret machine key with /etc/machine-id PBKDF2 fallback, and
  typed-master-password protection (PBKDF2-HMAC-SHA256 wrapped session key)
- settings_manager: per-OS password entry points
- localization.h/constants.h/log_manager/model_downloader/pii_detector:
  de-Windows-ified headers, XDG model fallback dir, CPU-only EP on Linux,
  UTF-8 model path for ORT
- core-smoke dev target verifies SettingsManager + RegexEngine against a
  temp config dir
- Move EngineApp into core (engine_app.h/engine_app.cpp) shared by both
  platforms; Windows vcxproj and main.cpp include paths updated.
- Linux engine main: SIGPIPE ignore, flock-based single-instance lock,
  config dir chmod 700, --console mode, termios no-echo password reads.
- Linux control API client over libcurl (control.json bearer auth).
- CLI gains typed master-password flow on Linux: 'password enable' prompts
  twice, gated commands prompt once, /unlock accepts {"password": ...}
  (Linux-only control API extension; Windows Hello path unchanged).
- fix(http_server): shutdown listen socket before joining listener thread
  in Stop() — closing first raced with select() and aborted on glibc
  (FD_SET bit out of range) when restarting listeners; latent on all
  platforms.
- engine version reported from windows/version.txt via global
  AR_VERSION_STRING compile definition.
- tests/cli/conftest.py: engine binary resolves to linux/build/engine/
  agentredactor off Windows (AGENTREDACTOR_ENGINE_BIN overrides);
  run_cli gains input= to pipe a typed master password.
- tests/cli/test_cli.py: skip the two Windows-Hello consent tests off
  Windows; branch the 'protection is not enabled' message per platform.
- tests/cli/test_cli_linux_password.py (new): typed-master-password gate
  mirroring the Hello consent gate — enable/disable round-trip, locked
  session after restart, EOF/wrong/correct password behavior.
- tests/linux/test_engine_smoke.py (new): engine start/stop, control.json
  0600, config dir 0700, single-instance lock keeps the first engine's
  control.json (second instance exits 0 quietly, mirroring Windows).
- tests/migration: run --selftest-migrate-settings against the Linux
  engine binary (AGENTREDACTOR_EXE still overrides).
- tests/gui: ignore the whole directory off Windows (no GUI there yet);
  kill list matches the extensionless Linux engine binary.
- READMEs: per-suite pytest invocations (the suites share the 'conftest'
  module name and cannot be collected in one process) and the
  python3-pytest-asyncio dependency.
…mplate

- linux/gui/: Qt6 Widgets app (agentredactor-gui) mirroring the Windows
  MainWindow/HomePage surface — profile sidebar, profile/regex/keywords/
  detection/password/statistics/session-redactions/logs/settings cards,
  lock overlay with typed master password (Linux has no Windows Hello),
  blocking model-download dialog, close-to-tray with control-panel
  fallback when no system tray is available, 10-minute inactivity
  re-lock, quit confirmation.
- AppState mirror: engine spawn via QProcess::startDetached when the
  control API is unreachable, 1 s /status + /settings poll on a worker
  thread (profilesRevision diff triggers profile reload; full settings
  diff refreshes cards), engine stopped on quit only when the GUI
  spawned it, otherwise PUT /settings/lock when protected.
- HTTP stack: reuses the tested curl ControlApiClient from
  linux/engine/ (one HTTP stack across the Linux product); ControlApiClient
  gains LastStatus() so the GUI can distinguish 403-while-locked.
- XDG autostart: the Start-on-boot toggle and GUI startup reconcile
  $XDG_CONFIG_HOME/autostart/agentredactor.desktop (Exec=<gui> --tray-only)
  with the persisted setting, so CLI changes apply too.
- Localization structure without translations yet: all strings via tr(),
  TranslatorLoader installs agentredactor_<tag>.qm from appLanguage
  (empty = system locale) and flips RTL via core IsLanguageRtl — the
  Windows Strings resw files can be converted to .ts later with no code
  changes.
- SIGTERM/SIGINT self-pipe bridge -> graceful quit so the engine
  stop/lock decision also runs under systemd and loginctl.
- linux/systemd/agentredactor.service: user unit template for headless
  boot-time startup.
- tests/linux/test_gui_smoke.py: offscreen (QT_QPA_PLATFORM=offscreen)
  end-to-end checks — engine spawn/stop ownership, lock-on-quit when
  protected, autostart reconciliation in both directions.
- buildUi added the cards widget to the QSplitter instead of the
  QScrollArea, leaving the splitter managing three widgets (sidebar,
  empty scroll area, cards) and squeezing the cards into a narrow strip.
- prevProfilesRevision_ initialized to 0 matched a fresh engine's
  revision, so the first settings snapshot never triggered the initial
  profile load and the sidebar stayed empty until the first mutation.
- AppUpdateManager (Velopack C/C++ lib, pinned 1.2.0 via linux/fetch-velopack.sh)
  behind a new AR_SELFRELEASE CMake option, mirroring AGENTREDACTOR_SELFRELEASE:
  startup check + Settings 'Check for updates' button, restart now/later prompt,
  WaitExitThenApplyUpdates + restart. Same loopback-only AGENTREDACTOR_UPDATE_FEED
  and AGENTREDACTOR_UPDATE_AUTOAPPLY test hooks as Windows.
- GUI restarts the engine on version mismatch after a self-update (engine ships
  inside the AppImage next to the GUI); VelopackApp::Run first in main().
- linux/build-release.sh: Release build, AppDir staging (Qt libs/plugins bundled
  dynamically + LGPL-Qt-notice.txt, $ORIGIN rpaths), vpk pack channel linux.
- First run symlinks ~/.local/bin/agentredactor to the bundled CLI.
- Cloudflare worker: allow the linux channel and *.AppImage files.
- tests/linux/test_update_feed.py: packs a vNext feed and asserts the shipped
  AppImage self-updates against a loopback feed (real apply + restart verified).
ubuntu-24.04: build core+engine+GUI, run cli/migration/linux suites (separate
pytest processes), pack the AppImage on every PR (artifact dry-run), publish
channel 'linux' to R2 only on v* tags or manual dispatch with publish checked —
mirroring release-selfrelease.yml gating and secrets.
ARM64 (mirrors the win-arm64 split):
- fetch-velopack.sh also extracts the linux_arm64 velopack_libc .so; the GUI
  CMake picks x64/arm64 by CMAKE_SYSTEM_PROCESSOR.
- build-release.sh derives vpk runtime/channel from the host arch: x64 keeps
  channel 'linux', aarch64 packs -r linux-arm64 -c linux-arm64 and uses the
  aarch64 Qt plugin dir. Updater feed URL is arch-aware via __aarch64__.
- build-linux.yml gains an arm64 leg on ubuntu-24.04-arm (aarch64 onnxruntime
  tarball); publish uploads both channels and prunes both prefixes.
- Feed worker allowlist gains the linux-arm64 channel.

CI fixes surfaced by the first PR run:
- platform_compat.h: include winsock2.h/ws2tcpip.h before windows.h on _WIN32
  (core headers use SOCKET and no longer rely on the includer's pch ordering;
  fixes the Windows vcxproj build).
- build-linux.yml: stage the NER model into ~/.local/share/agentredactor/models
  (companions from windows/models/, weights from the R2 models endpoint,
  cached) — the engine keeps proxy ports closed until weights exist, and the
  cli/linux suites spawn the real engine.
- Run the AppImage self-update E2E in CI after packing.
platform_compat.h now includes winsock2.h on _WIN32 (needed since core
headers like http_server.h use SOCKET without relying on the includer).
windows/include/system_tray.h included bare <windows.h> before any core
header, compiling winsock v1 first and clashing with winsock2 in the
non-pch batch — apply the same _WINSOCKAPI_ guard pch.h uses so v1 stays
out and winsock2 (from platform_compat.h) provides SOCKET.
proxy_engine.h used to include <winhttp.h> directly; the port swapped it for
platform_compat.h, which didn't pull winhttp — breaking the Windows build of
proxy_engine.cpp. platform_compat.h now includes winhttp.h on _WIN32.
…abled macro

windows.h (winevent.h) maps IsLoggingEnabled to IsLoggingEnabledW under
UNICODE. On main the header self-included windows.h so declaration,
definition and all call sites were mangled consistently; the port dropped
that include, leaving the class declaration clean while log_manager.cpp's
definition (after utils.h pulls windows.h) was renamed. Restoring the
platform include in the header makes the macro visible at the declaration
again, matching every other TU.
- Tray Language submenu and Settings language combo, both applying live
  via the settings poll + QEvent::LanguageChange (no restart, unlike
  Windows); RTL layout flip included. CLI 'set app-language' already worked
  engine-side and now retranslates the running GUI too.
- retranslateUi now covers the menu bar, form labels, PII grid and the
  dynamic regex/keyword rows; the tray menu retranslates via TrayIcon.
- English wording aligned to the Windows resw values where semantics
  matched (updater, model download, validation, remove-profile/quit/clear
  dialogs, PII_Type_* labels) so those translations are reused for free.
- i18n/sync_ts.py scans tr() sources and fills agentredactor_<locale>.ts
  from windows/Strings/<tag>/Resources.resw (normalizing '&' accelerators
  and {0} <-> %1 placeholders): 61/100 strings per language reused.
  TrayMenu_StartOnBoot is excluded (de/fr/pt translations say 'with
  Windows').
- i18n/bootstrap_translations.py machine-translates the 39 Linux-only
  strings (typed master password flow, tray/quit wording, UI labels) with
  Google Translate, same bootstrap convention as Windows; placeholders are
  verified to survive. Native review still needed.
- CMake compiles the .ts catalogs into :/i18n with lrelease
  (qt6-l10n-tools; English-only fallback with a warning when absent).
- New tests: every CLI-supported language has a catalog; live language
  switching (de/ar/zh-CN/en) while the GUI runs.
…Velopack state

Velopack keeps downloaded packages in a machine-wide /var/tmp/velopack/<packId>
dir that the test's HOME/XDG isolation does not cover. The vNext package
staged by the self-update E2E survived the test, and the next real AppImage
launch applied it in place — silently reverting the user's freshly packed
AppImage to the older test build. The fixture now purges that state dir
before and after the test.

Also: skip CLI symlink creation under AppImage runs (applicationDirPath is
an ephemeral /tmp/.mount_* there, so ~/.local/bin/agentredactor dangled
after exit) and clean up such dangling links.
… immediately

- build-release.sh stages the small model files (tokenizer/config/
  calibration/model graph) next to the binaries, mirroring the Windows
  self-release split (build.ps1 /XF *.onnx_data). Without them
  EnsureModelFiles could never start the first-run weight download, so
  AppImage installs on clean machines stayed stuck with no detector.
- GUI applies a newly selected language (Settings combo and tray menu)
  immediately instead of waiting for the settings-poll round-trip.
…dows

The engine and the UI both append to the shared log. The CRT append
stream seeks to EOF and then writes, so racing processes overwrite each
other's lines — seen in the Self-Release E2E as a mangled
'[UpdateManager] Up to date' line (only '.5, latest 1.1.5)' survived),
which the updater health check polls for.

Open the log with FILE_APPEND_DATA only (no FILE_WRITE_DATA) so every
WriteFile lands atomically at EOF regardless of the file pointer, and
write UTF-8 (previously the CRT 'C' locale mangled non-ASCII to '?',
matching the Linux side which already writes UTF-8 under O_APPEND).
…sers

Running the AppImage once now drops a two-line wrapper at
~/.local/bin/agentredactor that re-launches the AppImage file with --cli;
the GUI binary (before Velopack/Qt startup) execs the bundled dual-mode
engine/CLI binary with the remaining args. A symlink cannot reach inside
the ephemeral /tmp/.mount_* AppImage mount, which is why the shim was
previously skipped entirely for AppImage runs. The wrapper is rewritten
on every launch, so moving the AppImage self-heals on the next run.
Installed layouts keep the direct symlink to the binary.

The update E2E now asserts the shim is created and drives the full chain
(wrapper -> AppImage -> --cli -> usage output) end-to-end.
Stops app processes, unmounts stale AppImage mounts, and removes config,
logs, downloaded model weights, the CLI shim, the autostart entry and
Velopack staging so a machine can be returned to a first-run state.
- The window hard-coded 1000x900; on a 1280x800 screen that opens taller
  than the display and, since Wayland apps cannot reposition themselves,
  the title bar can be lost off the top edge for good. Clamp the initial
  size to the available work area (content already scrolls).
- Mirror the Windows GUI (HomePage::LoadProfileList): when the engine
  reports zero profiles, seed one named 'Default' on the first free port
  from 8080 instead of landing the user on an empty form. The alias rides
  the existing Windows resw translations (sync_ts.py reuse, 52 catalogs).
GNOME does not provide server-side decorations; without the bundled
adwaita/bradient decoration plugins Qt logs 'No decoration plugins
available' and the window runs with no title bar at all (no minimize/
maximize/close).
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