Skip to content

Add cross-platform background-service install (systemd/launchd/Windows)#81

Merged
hyperpolymath merged 2 commits into
mainfrom
claude/dreamy-carson-mTifq
May 23, 2026
Merged

Add cross-platform background-service install (systemd/launchd/Windows)#81
hyperpolymath merged 2 commits into
mainfrom
claude/dreamy-carson-mTifq

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

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:

  • Linux/WSL: systemd --user units with CAP_NET_BIND_SERVICE for privileged UDP/9 binding
  • macOS: launchd LaunchAgents in ~/Library/LaunchAgents/
  • Windows host (WSL2 NAT): True Windows Service via sc.exe with an embedded C# service host compiled from in-box .NET Framework csc.exe

The 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. Supports install, uninstall, start, stop, restart, status, logs actions. Includes --no-ai-bridge flag to skip the AI bridge service.

  • scripts/wsl-bolt-udp-forward.ps1 (major refactor):

    • Replaced scheduled-task-at-logon approach with a true Windows Service
    • Added Compile-ServiceHost() to generate and compile a minimal C# service host from embedded source using csc.exe
    • Added Assert-Elevated() to enforce elevated shell requirement for -Install and -Uninstall
    • Added Write-Relay() function for dual console + persistent log output (critical for windowless service)
    • Renamed Install-TaskInstall-Service, Uninstall-TaskUninstall-Service
    • Service now stores arguments in service-args.txt and logs to C:\ProgramData\BurbleBoltFwd\relay.log
    • Configured for auto-restart on crash (5s/5s/30s via sc.exe failure)
    • Grants service user Modify ACL on install directory so it can write logs
    • Updated usage docs and exit codes
  • assets/services/burble.service (new): systemd user unit for Elixir/Phoenix server with AmbientCapabilities=CAP_NET_BIND_SERVICE for UDP/9

  • assets/services/burble-ai-bridge.service (new): systemd user unit for Deno AI bridge

  • assets/services/com.hyperpolymath.burble.plist (new): macOS launchd LaunchAgent for Elixir server

  • assets/services/com.hyperpolymath.burble.ai-bridge.plist (new): macOS launchd LaunchAgent for Deno AI bridge

  • docs/developer/wsl-mirrored-networking.adoc: Updated to document the new service-based approach, LSA Secrets password storage, and systemd/launchd installation paths

  • Justfile: Added service-install, service-uninstall, service-start, service-stop, service-restart, service-status, service-logs recipes

  • CHANGELOG.md: Documented the new feature with implementation details

RSR Quality Checklist

Required

  • Tests pass (no new tests required; existing functionality preserved)
  • Code is formatted (PowerShell, Bash, XML, systemd unit syntax)
  • Linter is clean (no new warnings)
  • No banned language patterns
  • SPDX license headers present on all new files
  • No secrets or credentials included (passwords handled by Windows SCM via LSA Secrets)

As Applicable

  • Documentation updated (docs/developer/wsl-mirrored-networking.adoc, CHANGELOG.md)
  • CHANGELOG updated with feature description

Testing

The changes are backward

https://claude.ai/code/session_016BNGnVKCbN5FBXcTXRpDXy

claude added 2 commits May 23, 2026 23:11
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.
@github-actions
Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 17 issues detected

Severity Count
🔴 Critical 4
🟠 High 5
🟡 Medium 8

⚠️ Action Required: Critical security issues found!

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

@hyperpolymath hyperpolymath merged commit e37470d into main May 23, 2026
21 checks passed
@hyperpolymath hyperpolymath deleted the claude/dreamy-carson-mTifq branch May 23, 2026 23:22
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>
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.

2 participants