Add cross-platform background-service install (systemd/launchd/Windows)#81
Merged
Conversation
Launching Burble currently opens a terminal that scrolls Bolt's
udp/7373+9 bind log past the user. Replace that with proper per-OS
service units so the control plane runs headless:
- Linux/WSL: systemd --user units in assets/services/. Bolt's udp/9
privileged bind is handled via AmbientCapabilities=CAP_NET_BIND_SERVICE.
- macOS: launchd LaunchAgents in assets/services/.
- Windows host: scripts/wsl-bolt-udp-forward.ps1 -Install now registers
the scheduled task to launch the relay WINDOWLESS via a generated
VBS shim (wscript.exe + Run "...", 0, False — eliminates the
PowerShell console flash that -WindowStyle Hidden alone can't avoid).
New -WithTray flag opts into a NotifyIcon system-tray UI (Status /
Open log / Restart / Exit) for users who want visibility.
Adds scripts/install-service.sh as a one-shot cross-platform installer
that detects the OS and dispatches. Justfile gets service-{install,
uninstall,start,stop,restart,status,logs}. Relay output is captured to
%LOCALAPPDATA%\BurbleBoltFwd\relay.log via a new Write-Relay helper so
the windowless install path still leaves a trace.
Replaces the scheduled-task + VBS shim path with a real Windows Service. A minimal C# service host (ServiceBase) is embedded in the script and compiled in-place by the in-box .NET Framework csc.exe — no NSSM, srvany, or external tooling. The service stub spawns powershell.exe running the relay as a child on OnStart and kills it on OnStop. Service runs under the installing user's account, not LocalSystem, because WSL distros are registered per-user (HKCU\…\Lxss). New-Service -Credential prompts via Get-Credential and the SCM stores the password via LSA Secrets. Failure actions: restart at 5s, 5s, 30s. Install dir is C:\ProgramData\BurbleBoltFwd\; the user gets Modify on it so the service can append to relay.log. Drops -Tray / NotifyIcon and the VBS shim entirely. -Install and -Uninstall now require an elevated shell (Assert-Elevated). -Status queries Get-Service instead of Get-ScheduledTask.
🔍 Hypatia Security ScanFindings: 17 issues detected
View findings[
{
"reason": "Issue in quality.yml",
"type": "missing_workflow",
"file": "quality.yml",
"action": "create",
"rule_module": "workflow_audit",
"severity": "high"
},
{
"reason": "Issue in security-policy.yml",
"type": "missing_workflow",
"file": "security-policy.yml",
"action": "create",
"rule_module": "workflow_audit",
"severity": "medium"
},
{
"reason": "Action hyperpolymath/standards/.github/workflows/governance-reusable.yml@main needs attention",
"type": "unpinned_action",
"file": "governance.yml",
"action": "pin_sha",
"rule_module": "workflow_audit",
"severity": "high"
},
{
"reason": "binary_to_term without :safe option -- deserialization attack (1 occurrences, CWE-502)",
"type": "elixir_send_unsanitised",
"file": "/home/runner/work/burble/burble/server/lib/burble/media/lmdb_playout.ex",
"action": "flag",
"rule_module": "code_safety",
"severity": "high"
},
{
"reason": "SSL verify_none disables certificate validation -- MITM risk (1 occurrences, CWE-295)",
"type": "elixir_no_ssl_verify",
"file": "/home/runner/work/burble/burble/server/lib/burble/bridges/mumble.ex",
"action": "flag",
"rule_module": "code_safety",
"severity": "high"
},
{
"reason": "believe_me undermines formal verification (2 occurrences, CWE-704)",
"type": "believe_me",
"file": "/home/runner/work/burble/burble/src/interface/abi/Foreign.idr",
"action": "flag",
"rule_module": "code_safety",
"severity": "critical"
},
{
"reason": "Nickel file missing SPDX-License-Identifier header (1 occurrences, CWE-1104)",
"type": "ncl_missing_spdx",
"file": "/home/runner/work/burble/burble/configs/config.ncl",
"action": "flag",
"rule_module": "code_safety",
"severity": "medium"
},
{
"reason": "Lock.unwrap() without poison handling (14 occurrences, CWE-754)",
"type": "lock_unwrap",
"file": "/home/runner/work/burble/burble/tools/selur-compose/crates/selur-compose-driver/src/mock.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "high"
},
{
"line": 24,
"reason": "Secret found: Generic API key",
"type": "secret_detected",
"file": "/home/runner/work/burble/burble/.envrc",
"action": "revoke_rotate_and_purge",
"rule_module": "security_errors",
"severity": "critical"
},
{
"line": 39,
"reason": "Secret found: Password",
"type": "secret_detected",
"file": "/home/runner/work/burble/burble/server/lib/burble/safety/proven_bridge.ex",
"action": "revoke_rotate_and_purge",
"rule_module": "security_errors",
"severity": "critical"
}
]Powered by Hypatia Neurosymbolic CI/CD Intelligence |
4 tasks
hyperpolymath
added a commit
that referenced
this pull request
May 24, 2026
Stacks on #81. Adds the round-trip tests that #81's README documented as "next step". ## What's in this PR Three platform-specific drivers that actually install → activate → stop → uninstall the service on the real service manager, complementing the lint-only checks in #81: | Driver | Platform | What it does | |---|---|---| | `tests/install/roundtrip-linux.sh` | Linux | `systemctl --user enable --now` round-trip. Also kills the unit's `MainPID` and asserts `Restart=on-failure` respawns it within `RestartSec=5`. | | `tests/install/roundtrip-macos.sh` | macOS | `launchctl bootstrap gui/$UID` / `bootout` round-trip. Patches stub `PATH=` into the plist (launchd ignores user shell env). | | `tests/install/roundtrip-windows.ps1` | Windows | Creates throwaway local user (`burble-ci-test`) with random password, installs Windows Service non-interactively, asserts SCM state, uninstalls, removes user in `finally{}`. | ### Supporting bits - **`tests/install/stubs/{mix,deno}`** — sleep-forever stand-ins so spawned units satisfy systemd/launchd's "Active" check without needing the full Elixir/Deno toolchain in CI. - **`scripts/wsl-bolt-udp-forward.ps1`** — new `-Credential [PSCredential]` parameter on `-Install` that skips the `Get-Credential` prompt when supplied. Human-interactive behaviour unchanged when omitted. This is what makes the Windows round-trip testable in CI at all. - **`.github/workflows/install-roundtrip.yml`** — three-OS matrix: - `ubuntu-latest` (with `loginctl enable-linger $USER` so user-systemd starts without a login session) - `macos-14` (Apple Silicon) - `windows-latest` Path-filtered to install-machinery changes only, same as #81's `install-tests.yml`. ### Safety All drivers clean up after themselves on failure: - shell: `trap 'ec=$?; cleanup; exit $ec' EXIT` (captures real exit code before cleanup runs) - PowerShell: `try { … } finally { Remove-LocalUser … }` Safe to run locally if you don't mind a brief service install on your box. ## Things found while writing this - `set -u` + unset `$USER` killed the script on a stripped container — falls back to `id -un` now - The naïve `trap 'cleanup' EXIT` was swallowing the preflight's `exit 2` — fixed with `ec=$?` capture - Shellcheck SC2154 false-positive on the `ec=$?` pattern inside the trap (disabled inline with a comment) ## Test plan - [ ] CI green on all three jobs of `install-roundtrip` workflow - [ ] CI green on `install-tests` workflow (the new round-trip ps1 + sh files are now in its shellcheck/PSScriptAnalyzer scope too) - [ ] On your Windows + WSL setup: from inside WSL, `tests/install/roundtrip-linux.sh` should pass once you `sudo loginctl enable-linger $USER && sleep 2`. From elevated Windows PowerShell, `tests\install\roundtrip-windows.ps1` should pass standalone (creates + removes its own user). - [ ] macOS coverage will only land via CI unless you also test on a Mac. ## Out of scope (still NOT tested) - Whether UDP packets actually relay through the Windows forwarder (needs a real WSL distro on the runner — GH runners don't have one). - Whether `mix phx.server` itself comes up clean under systemd/launchd (covered by `elixir-ci.yml`, not the install machinery). https://claude.ai/code/session_016BNGnVKCbN5FBXcTXRpDXy --- _Generated by [Claude Code](https://claude.ai/code/session_016BNGnVKCbN5FBXcTXRpDXy)_ Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a unified cross-platform service installation system so Burble can run as a background service without popping a terminal window. Detects the OS and installs:
--userunits withCAP_NET_BIND_SERVICEfor privileged UDP/9 binding~/Library/LaunchAgents/sc.exewith an embedded C# service host compiled from in-box .NET Frameworkcsc.exeThe Windows service runs under the installing user's account (prompted once via
Get-Credential, password stored securely by the SCM) because WSL distros are per-user — a LocalSystem service cannot see the user's distros.Changes
scripts/install-service.sh(new): Cross-platform service installer that dispatches to OS-specific handlers. Supportsinstall,uninstall,start,stop,restart,status,logsactions. Includes--no-ai-bridgeflag to skip the AI bridge service.scripts/wsl-bolt-udp-forward.ps1(major refactor):Compile-ServiceHost()to generate and compile a minimal C# service host from embedded source usingcsc.exeAssert-Elevated()to enforce elevated shell requirement for-Installand-UninstallWrite-Relay()function for dual console + persistent log output (critical for windowless service)Install-Task→Install-Service,Uninstall-Task→Uninstall-Serviceservice-args.txtand logs toC:\ProgramData\BurbleBoltFwd\relay.logsc.exe failure)ModifyACL on install directory so it can write logsassets/services/burble.service(new): systemd user unit for Elixir/Phoenix server withAmbientCapabilities=CAP_NET_BIND_SERVICEfor UDP/9assets/services/burble-ai-bridge.service(new): systemd user unit for Deno AI bridgeassets/services/com.hyperpolymath.burble.plist(new): macOS launchd LaunchAgent for Elixir serverassets/services/com.hyperpolymath.burble.ai-bridge.plist(new): macOS launchd LaunchAgent for Deno AI bridgedocs/developer/wsl-mirrored-networking.adoc: Updated to document the new service-based approach, LSA Secrets password storage, and systemd/launchd installation pathsJustfile: Addedservice-install,service-uninstall,service-start,service-stop,service-restart,service-status,service-logsrecipesCHANGELOG.md: Documented the new feature with implementation detailsRSR Quality Checklist
Required
As Applicable
docs/developer/wsl-mirrored-networking.adoc,CHANGELOG.md)CHANGELOGupdated with feature descriptionTesting
The changes are backward
https://claude.ai/code/session_016BNGnVKCbN5FBXcTXRpDXy