Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f3ca9e0
feat(linux): add Linux CMake build scaffolding
Aug 18, 2026
e318562
feat(linux): port core library to Linux
Aug 18, 2026
06fdfb0
feat(linux): add Linux engine + CLI binary
Aug 18, 2026
c48caf8
test(linux): run CLI/migration suites on Linux, add engine smoke tests
Aug 18, 2026
b1e2493
feat(linux): add Qt6 GUI with tray, XDG autostart and systemd unit te…
Aug 18, 2026
e1141e4
fix(linux): repair main window layout and initial profile load
Aug 18, 2026
ec55708
feat(linux): add Velopack self-update, AppImage packaging and update E2E
Aug 19, 2026
f341ca9
ci(linux): add build-linux workflow with gated R2 publish
Aug 19, 2026
0761059
feat(linux): add ARM64 support and fix CI model/Windows build issues
Aug 19, 2026
a05ed31
fix(windows): resolve winsock v1/v2 clash in GUI build
Aug 19, 2026
75071e5
fix(core): restore winhttp.h include on Windows
Aug 19, 2026
f2fafd7
fix(core): include platform_compat.h in log_manager.h for IsLoggingEn…
Aug 19, 2026
6ae28e2
feat(linux-gui): full i18n — live language switching and 52 catalogs
Aug 19, 2026
5896e6e
fix(linux): stop update E2E from poisoning real AppImages via shared …
Aug 20, 2026
40672fc
fix(linux): ship model companions in AppImage; apply language changes…
Aug 20, 2026
0765d93
fix(core): atomic cross-process appends for agent_redactor.log on Win…
Aug 20, 2026
acd66a8
feat(linux): expose the CLI as 'agentredactor' on PATH for AppImage u…
Aug 20, 2026
3f74d9a
chore(scripts): add linux-clean-slate.sh to reset a test machine
Aug 20, 2026
e300e0d
fix(linux): fit window to work area; seed a default profile on first run
Aug 20, 2026
51145b7
fix(linux): bundle Qt wayland-decoration-client plugins in the AppImage
Aug 20, 2026
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
210 changes: 210 additions & 0 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
name: Build Linux

# Linux port CI: builds the core + engine + Qt GUI on Ubuntu (x64 and arm64,
# mirroring the win / win-arm64 split in release-selfrelease.yml), runs the
# cross-platform suites (cli, migration) plus the Linux-only suite, and packs
# the Velopack AppImage. Publishing is gated the same way as
# release-selfrelease.yml: only tag pushes (v*) or manual dispatches with
# publish checked upload to R2 (channels "linux" / "linux-arm64"); PRs get a
# pack dry-run with the packages as artifacts.
on:
push:
tags: ['v*']
branches: ['main']
pull_request:
workflow_dispatch:
inputs:
publish:
description: 'Publish to R2 (agentredactor-releases bucket, prefixes linux/linux-arm64). Off = dry-run.'
required: false
type: boolean
default: false

concurrency:
group: build-linux-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
build:
strategy:
# One arch failing must not cancel the other; publish only runs when
# both succeed anyway (same pattern as release-selfrelease.yml).
fail-fast: false
matrix:
arch: [x64, arm64]
# Native runners per arch, same pattern as the Windows ARM64 leg.
runs-on: ${{ matrix.arch == 'x64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }}
env:
ONNXRUNTIME_DIR: ${{ github.workspace }}/onnxruntime
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install build + test dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build pkg-config \
libsecret-1-dev libcurl4-openssl-dev libssl-dev nlohmann-json3-dev \
qt6-base-dev qt6-l10n-tools libgl1-mesa-dev fuse

# onnxruntime has no apt package; official tarball per arch, same as
# linux/README.md documents for local builds.
- name: Download onnxruntime
run: |
mkdir -p "$ONNXRUNTIME_DIR"
rid="${{ matrix.arch == 'x64' && 'x64' || 'aarch64' }}"
curl -sL "https://github.com/microsoft/onnxruntime/releases/download/v1.29.0/onnxruntime-linux-${rid}-1.29.0.tgz" \
| tar xz -C "$ONNXRUNTIME_DIR" --strip-components=1

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install test dependencies
run: pip install -r tests/requirements.txt

# The engine refuses to serve (proxy ports stay closed) until the NER
# model weights exist, and the CLI/linux suites spawn the real engine —
# so the ~1.6 GB weights must be present. Companions come from the repo
# checkout (windows/models/, same files the Windows build ships); only
# the gitignored weights are downloaded, from the same R2 models
# endpoint the app itself uses. Cached across runs.
- name: Cache ONNX model weights
id: model-cache
uses: actions/cache@v4
with:
path: ~/.local/share/agentredactor/models
key: onnx-model-ner-${{ runner.arch }}-v1

- name: Stage model files (cache miss only)
if: steps.model-cache.outputs.cache-hit != 'true'
run: |
dest=~/.local/share/agentredactor/models
mkdir -p "$dest/onnx"
cp windows/models/config.json windows/models/tokenizer.json \
windows/models/viterbi_calibration.json "$dest/"
cp windows/models/onnx/model_quantized.onnx "$dest/onnx/"
curl -fL --retry 3 -o "$dest/onnx/model_quantized.onnx_data" \
https://api.agentredactor.negativestarinnovators.com/models/model_quantized.onnx_data

- name: Build (dev config, updater off)
run: |
cmake -S linux -B linux/build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DONNXRUNTIME_INCLUDE_DIR="$ONNXRUNTIME_DIR/include" \
-DONNXRUNTIME_LIB="$ONNXRUNTIME_DIR/lib/libonnxruntime.so"
cmake --build linux/build

# The three suites must each run in their own pytest process: tests/cli
# and tests/migration both ship a top-level conftest.py and a shared
# module name breaks a combined run.
- name: Run CLI tests
run: python -m pytest tests/cli -q

- name: Run migration tests
run: python -m pytest tests/migration -q

- name: Run Linux tests (offscreen GUI smoke)
run: python -m pytest tests/linux -q

- name: Set up .NET (for vpk)
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"

# Pinned to the Velopack library version vendored by
# linux/fetch-velopack.sh — keep the two in sync.
- name: Install vpk 1.2.0 (Velopack CLI)
run: dotnet tool install -g vpk --version 1.2.0

# build-release.sh derives the vpk runtime/channel from the host arch
# (linux-x64/'linux', linux-arm64/'linux-arm64').
- name: Build and pack AppImage (build-release.sh)
env:
ONNXRUNTIME_INCLUDE_DIR: ${{ github.workspace }}/onnxruntime/include
ONNXRUNTIME_LIB: ${{ github.workspace }}/onnxruntime/lib/libonnxruntime.so
run: linux/build-release.sh

- name: Verify Velopack output
run: |
dir=linux/build-release/velopack
channel="${{ matrix.arch == 'x64' && 'linux' || 'linux-arm64' }}"
test -f "$dir/releases.${channel}.json" || { echo "missing releases.${channel}.json"; exit 1; }
ls "$dir"/*-full.nupkg >/dev/null || { echo "no full nupkg"; exit 1; }
ls "$dir"/*.AppImage >/dev/null || { echo "no AppImage"; exit 1; }
echo "Velopack output OK:" && ls "$dir"

- name: Upload Velopack output artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: AgentRedactor-velopack-linux-${{ matrix.arch }}
path: linux/build-release/velopack
retention-days: 30

# Full self-update cycle against a loopback feed: packs a vNext release
# from the staged AppDir and asserts the shipped AppImage swaps itself.
# Runs after packing since it consumes the pack output; skips locally
# when vpk or the pack output is missing.
- name: Run update E2E (AppImage self-update)
run: python -m pytest tests/linux/test_update_feed.py -q

# Publish is deliberately OUT of the matrix: both arch legs must pass first,
# then ONE job uploads both channels to R2 — same shape as
# release-selfrelease.yml. Tag pushes always publish; manual dispatches only
# when publish is checked. Same R2 secrets and bucket as the Windows flow.
publish:
needs: build
runs-on: ubuntu-24.04
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') || inputs.publish
steps:
- name: Download Velopack outputs (both arches)
uses: actions/download-artifact@v4
with:
pattern: AgentRedactor-velopack-linux-*
path: velopack-linux
# Lands in velopack-linux/AgentRedactor-velopack-linux-x64 and ...-arm64.

- name: Set up .NET (for vpk)
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"

- name: Install vpk 1.2.0 (Velopack CLI)
run: dotnet tool install -g vpk --version 1.2.0

- name: Check R2 secrets are configured
run: |
missing=""
[ -z '${{ secrets.R2_ACCOUNT_ID }}' ] && missing="$missing R2_ACCOUNT_ID"
[ -z '${{ secrets.R2_ACCESS_KEY_ID }}' ] && missing="$missing R2_ACCESS_KEY_ID"
[ -z '${{ secrets.R2_SECRET_ACCESS_KEY }}' ] && missing="$missing R2_SECRET_ACCESS_KEY"
[ -n "$missing" ] && { echo "Missing repo secrets:$missing"; exit 1; }

# Publishes the linux channel (releases.linux.json + nupkg + AppImage)
# to R2 so installed Linux x64 instances can auto-update. vpk rejects
# --region together with --endpoint (custom endpoint implies it).
- name: Publish linux (x64) channel to R2
run: vpk upload s3 --bucket agentredactor-releases --endpoint https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com --keyId ${{ secrets.R2_ACCESS_KEY_ID }} --secret ${{ secrets.R2_SECRET_ACCESS_KEY }} -c linux --prefix linux --outputDir velopack-linux/AgentRedactor-velopack-linux-x64

# Same for the linux-arm64 channel (prefix linux-arm64/).
- name: Publish linux-arm64 channel to R2
run: vpk upload s3 --bucket agentredactor-releases --endpoint https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com --keyId ${{ secrets.R2_ACCESS_KEY_ID }} --secret ${{ secrets.R2_SECRET_ACCESS_KEY }} -c linux-arm64 --prefix linux-arm64 --outputDir velopack-linux/AgentRedactor-velopack-linux-arm64

# vpk uploads its own bookkeeping files no client ever downloads
# (assets.*.json upload manifest, legacy RELEASES file); keep the bucket
# to client files only.
- name: Remove non-client files from R2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: |
for channel in linux linux-arm64; do
aws s3 rm "s3://agentredactor-releases/${channel}/" \
--endpoint-url https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com \
--region auto --recursive --exclude "*" --include "assets.*.json" --include "RELEASES*"
done
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Build outputs
windows/build/
windows/packages/
linux/build*/

# Vendored prebuilt Velopack C/C++ library (fetched via linux/fetch-velopack.sh)
linux/third_party/

# Temporary scripts and artifacts
temp/
Expand All @@ -25,3 +29,6 @@ windows/store-listing/

# Internal planning docs (not for public release)
docs/linux-port-plan.md

# Translation/CI tooling venv (lrelease, deep-translator)
.transvenv/
15 changes: 8 additions & 7 deletions cloudflare/src/routes/updates.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// Self-release update channels: x64 builds use the original 'win' channel,
// ARM64 builds use 'win-arm64'. Releases are served exclusively from the
// RELEASES_BUCKET R2 binding at <channel>/<file> (R2 is the single host —
// no GitHub fallback). CI uploads each release via `vpk upload s3`.
const CHANNEL_PATTERN = /^(win|win-arm64)$/;
// Self-release update channels: x64 builds use the original 'win' / 'linux'
// channels, ARM64 builds use 'win-arm64' / 'linux-arm64'. Releases are
// served exclusively from the RELEASES_BUCKET R2 binding at <channel>/<file>
// (R2 is the single host — no GitHub fallback). CI uploads each release via
// `vpk upload s3`.
const CHANNEL_PATTERN = /^(win|win-arm64|linux|linux-arm64)$/;

// Strict allowlist for files Velopack requests from the update feed:
// releases.<channel>.json, *.nupkg, *-Setup.exe, *-Portable.zip
// releases.<channel>.json, *.nupkg, *-Setup.exe, *-Portable.zip, *.AppImage
// Case-sensitive, matched against the basename only.
const FILE_PATTERN = /^(releases\.[a-z0-9-]+\.json|[^/\\]+\.nupkg|[^/\\]+-Setup\.exe|[^/\\]+-Portable\.zip)$/;
const FILE_PATTERN = /^(releases\.[a-z0-9-]+\.json|[^/\\]+\.nupkg|[^/\\]+-Setup\.exe|[^/\\]+-Portable\.zip|[^/\\]+\.AppImage)$/;

// Versioned nupkgs are immutable; the fixed-name feed/installer files
// (releases.*.json, *-Setup.exe, *-Portable.zip) change every release.
Expand Down
39 changes: 31 additions & 8 deletions core/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# Agent Redactor core library — OS-agnostic proxy / NER / redaction engine.
#
# Status (PR 1 — restructure): this file is scaffolding introduced alongside the
# AgentRedactor/ -> windows/ move. It currently builds on Windows only; several
# sources still carry Windows-specific code (localization.cpp uses MRT resources,
# utils.cpp / logging.h use Win32 APIs). The Linux port (PR 3) peels those shims
# behind a platform interface and makes this target cross-platform.
# The Windows build compiles these sources via the vcxproj files under
# windows/; this CMake target is used by the Linux build (linux/CMakeLists.txt).
# localization.cpp is MRT/WinRT-bound and stays Windows-only; the Linux engine
# uses the English-only LocString shim pattern (see windows/engine/engine_loc.cpp).
cmake_minimum_required(VERSION 3.24)
project(agentredactor-core LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(nlohmann_json CONFIG REQUIRED)
find_package(Threads REQUIRED)

# onnxruntime: no official CMake config on all platforms; locate via vcpkg or
# a system install. Include dir must contain onnxruntime_cxx_api.h.
Expand All @@ -22,12 +22,14 @@ if (NOT ONNXRUNTIME_INCLUDE_DIR OR NOT ONNXRUNTIME_LIB)
message(FATAL_ERROR "onnxruntime not found — install via vcpkg or set ONNXRUNTIME_INCLUDE_DIR/ONNXRUNTIME_LIB")
endif()

add_library(agentredactor-core STATIC
set(AR_CORE_SOURCES
src/api_key_profile.cpp
src/bpe_tokenizer.cpp
src/cli.cpp
src/control_server.cpp
src/engine_app.cpp
src/http_server.cpp
src/keyword_engine.cpp
src/localization.cpp
src/log_manager.cpp
src/model_downloader.cpp
src/pii_detector.cpp
Expand All @@ -39,12 +41,33 @@ add_library(agentredactor-core STATIC
src/migrations/settings_migrator.cpp
)

if (WIN32)
# MRT/WinRT-bound localization compiles on Windows only.
list(APPEND AR_CORE_SOURCES src/localization.cpp)
else()
find_package(CURL REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBSECRET REQUIRED IMPORTED_TARGET libsecret-1)
list(APPEND AR_CORE_SOURCES src/secure_storage_linux.cpp src/platform_compat.cpp)
endif()

add_library(agentredactor-core STATIC ${AR_CORE_SOURCES})

target_include_directories(agentredactor-core
PUBLIC include
PRIVATE ${ONNXRUNTIME_INCLUDE_DIR}
)

target_link_libraries(agentredactor-core
PUBLIC nlohmann_json::nlohmann_json
PRIVATE ${ONNXRUNTIME_LIB}
PRIVATE ${ONNXRUNTIME_LIB} Threads::Threads
)

if (NOT WIN32)
target_include_directories(agentredactor-core PRIVATE ${CURL_INCLUDE_DIRS})
target_link_libraries(agentredactor-core
PRIVATE CURL::libcurl OpenSSL::SSL OpenSSL::Crypto PkgConfig::LIBSECRET
)
target_compile_options(agentredactor-core PRIVATE -Wall -Wextra)
endif()
9 changes: 9 additions & 0 deletions core/include/cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,19 @@ struct CliTransport {
// unlocks the engine session (POST /unlock). Falls back to
// HelloConsentOutcome::Unavailable on platforms without Windows Hello.
std::function<HelloConsentOutcome()> consent;
// Typed-master-password mode (Linux, where Windows Hello does not exist):
// when set, gated commands prompt for the master password (via
// CliConsole::readSecret) instead of the Hello consent, and this hook
// verifies it via POST /unlock {"password": ...}. Null on Windows, where
// the Hello consent flow above is used.
std::function<bool(const std::wstring& password)> unlockWithPassword;
};

struct CliConsole {
std::function<void(const std::wstring& line)> print;
// No-echo secret prompt, used only in typed-master-password mode (Linux).
// Returns the entered text; an empty string means no input.
std::function<std::wstring(const std::wstring& prompt)> readSecret;
};

// Executes one CLI invocation (args excluding argv[0], e.g. {"get","api-key",
Expand Down
8 changes: 7 additions & 1 deletion core/include/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#include <string>
#include <vector>
#include <unordered_map>
#include <windows.h>
#include "platform_compat.h"

namespace AgentRedactor {

Expand All @@ -18,7 +18,9 @@ constexpr const wchar_t* APP_VERSION = AR_STRINGIZE(AR_VERSION_STRING);
#else
constexpr const wchar_t* APP_VERSION = L"1.0.0";
#endif
#ifdef _WIN32
constexpr UINT WM_TRAYICON = WM_USER + 1;
#endif

constexpr size_t MAX_TOKENS_PER_CHUNK = 128000;
constexpr size_t TOKEN_OVERLAP = 128;
Expand Down Expand Up @@ -51,12 +53,14 @@ inline const std::vector<PIICategory> PII_CATEGORIES = {
{L"DIGITAL", L"Digital & Secrets", {L"private_url", L"secret"}},
};

#ifdef _WIN32
enum MenuIDs : UINT {
ID_TRAY_OPEN = 1001,
ID_TRAY_LANGUAGE_FIRST = 2000,
ID_TRAY_START_ON_BOOT = 3001,
ID_TRAY_QUIT,
};
#endif

struct SupportedLanguage {
std::wstring tag;
Expand Down Expand Up @@ -153,6 +157,7 @@ inline bool LanguageMatches(const std::wstring& current, const std::wstring& sup

} // namespace AgentRedactor

#ifdef _WIN32
inline void RegisterStartupTask() {
HKEY hKey;
if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_WRITE, &hKey) == ERROR_SUCCESS) {
Expand All @@ -171,3 +176,4 @@ inline void UnregisterStartupTask() {
RegCloseKey(hKey);
}
}
#endif
Loading
Loading