Fix exotic CPU stub idle-mining guard and document Linux headless screenshots.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Return sane fallback CPU percent after first sample on unknown Unix platforms; add xvfb/scrot docs and stub tests; clear PROBLEMS.md open bugs.
This commit is contained in:
AetherForge
2026-06-07 06:27:19 -07:00
parent 72d0e6ac19
commit f404a76caa
9 changed files with 158 additions and 9 deletions

View File

@@ -69,13 +69,6 @@ Open issues only. Fixed items removed. Last sweep: 2026-06-07.
| **Terminal virtualization** | 400-line DOM cap only; full virtual scrollback deferred. | | **Terminal virtualization** | 400-line DOM cap only; full virtual scrollback deferred. |
| **Vite chunk weight** | `three` + vendor warnings; FleetTopologyMap lazy but heavy first open. | | **Vite chunk weight** | `three` + vendor warnings; FleetTopologyMap lazy but heavy first open. |
## Open bugs / behavior
| Issue | Notes |
|-------|-------|
| **Unknown Unix CPU stats stub** | `cpu_stub.go` may return 0 and break idle-mining guard on exotic platforms. |
| **Linux headless screenshot** | Needs `xvfb` + scrot or custom `command` in containers. |
## UX / visual (unfixed DV) ## UX / visual (unfixed DV)
| ID | Issue | | ID | Issue |

View File

@@ -1,5 +1,17 @@
//go:build linux //go:build linux
// Headless Linux hosts (Docker, CI, VPS) have no DISPLAY. Install Xvfb + scrot:
//
// apt-get install -y xvfb scrot
// Xvfb :99 -screen 0 1280x720x24 &
// export DISPLAY=:99
//
// Or pass a one-shot custom command via Crucible screenshot (command field), e.g.:
//
// DISPLAY=:99 scrot -q 55 -o /tmp/s.jpg && base64 -w0 /tmp/s.jpg
//
// See docs/E2E_VALIDATION.md § Linux headless screenshot.
package client package client
import ( import (
@@ -76,5 +88,5 @@ func captureLinuxScreenshotJPEG() ([]byte, error) {
return os.ReadFile(path) return os.ReadFile(path)
} }
return nil, fmt.Errorf("no screenshot tool found (scrot, import, gnome-screenshot)") return nil, fmt.Errorf("no screenshot tool found (scrot, import, gnome-screenshot); headless hosts need xvfb — see docs/E2E_VALIDATION.md")
} }

View File

@@ -0,0 +1,28 @@
//go:build linux
package client
import (
"strings"
"testing"
)
func TestLinuxScreenshotErrorMentionsHeadlessXvfb(t *testing.T) {
_, err := captureLinuxScreenshotJPEG()
if err == nil {
t.Skip("screenshot tool present on this host")
}
if !strings.Contains(strings.ToLower(err.Error()), "xvfb") {
t.Fatalf("headless error should mention xvfb, got %q", err.Error())
}
}
func TestLinuxScreenshotCustomCommandEmptyRejected(t *testing.T) {
_, err := capturePlatformScreenshot(" ")
if err == nil {
t.Skip("default capture succeeded without custom command")
}
if !strings.Contains(err.Error(), "scrot") && !strings.Contains(err.Error(), "xvfb") {
t.Fatalf("expected tool or xvfb hint, got %q", err.Error())
}
}

View File

@@ -0,0 +1,28 @@
//go:build !windows && !linux && !darwin
package client
import (
"strings"
"testing"
)
func TestScreenshotStubReturnsUnsupportedError(t *testing.T) {
b64, err := capturePlatformScreenshot("")
if b64 != "" {
t.Fatalf("expected empty base64, got len=%d", len(b64))
}
if err == nil {
t.Fatal("expected error from screenshot stub")
}
if !strings.Contains(err.Error(), "not supported") {
t.Fatalf("error should mention unsupported platform, got %q", err.Error())
}
}
func TestScreenshotStubIgnoresCustomCommand(t *testing.T) {
_, err := capturePlatformScreenshot("echo fake")
if err == nil {
t.Fatal("stub must reject custom screenshot commands on unsupported platforms")
}
}

View File

@@ -1,5 +1,21 @@
package stats package stats
// StubFallbackCPUPercent is returned after the first sample on platforms without
// OS CPU counters (cpu_stub.go). Kept below the default idle threshold (20%).
const StubFallbackCPUPercent = 5.0
// stubSystemCPUPercent implements idle-mining-safe defaults when real counters
// are unavailable: first sample returns 0 (no delta yet), then a low busy %.
func (r *Reporter) stubSystemCPUPercent() float64 {
r.mu.Lock()
defer r.mu.Unlock()
if !r.hasSample {
r.hasSample = true
return 0
}
return StubFallbackCPUPercent
}
func cpuBusyPercentFromDeltas(idleDelta, totalDelta float64) float64 { func cpuBusyPercentFromDeltas(idleDelta, totalDelta float64) float64 {
if totalDelta <= 0 { if totalDelta <= 0 {
return 0 return 0

View File

@@ -0,0 +1,30 @@
package stats
import "testing"
func TestStubSystemCPUPercentFirstSampleZero(t *testing.T) {
r := NewReporter()
if got := r.stubSystemCPUPercent(); got != 0 {
t.Fatalf("first sample want 0 got %v", got)
}
}
func TestStubSystemCPUPercentFallbackBelowIdleThreshold(t *testing.T) {
r := NewReporter()
_ = r.stubSystemCPUPercent()
got := r.stubSystemCPUPercent()
if got != StubFallbackCPUPercent {
t.Fatalf("fallback want %v got %v", StubFallbackCPUPercent, got)
}
if got >= 20 {
t.Fatalf("fallback %v must stay below default idle threshold 20", got)
}
}
func TestStubSystemCPUPercentSubsequentNonZero(t *testing.T) {
r := NewReporter()
_ = r.stubSystemCPUPercent()
if got := r.stubSystemCPUPercent(); got <= 0 {
t.Fatalf("subsequent stub sample must be >0 for idle guard, got %v", got)
}
}

View File

@@ -3,5 +3,5 @@
package stats package stats
func (r *Reporter) SystemCPUPercent() float64 { func (r *Reporter) SystemCPUPercent() float64 {
return 0 return r.stubSystemCPUPercent()
} }

View File

@@ -0,0 +1,15 @@
//go:build !windows && !linux && !darwin
package stats
import "testing"
func TestStubPlatformSystemCPUPercentUsesFallback(t *testing.T) {
r := NewReporter()
if got := r.SystemCPUPercent(); got != 0 {
t.Fatalf("first SystemCPUPercent want 0 got %v", got)
}
if got := r.SystemCPUPercent(); got != StubFallbackCPUPercent {
t.Fatalf("second SystemCPUPercent want %v got %v", StubFallbackCPUPercent, got)
}
}

View File

@@ -306,11 +306,38 @@ Get-Content ".\data-e2e\logs\<agent-id>.log" -Tail 100
| Defender off | ✅ | N/A (returns error) | N/A | | Defender off | ✅ | N/A (returns error) | N/A |
| systemd / LaunchAgent persistence | — | ✅ | ✅ | | systemd / LaunchAgent persistence | — | ✅ | ✅ |
| Idle-mode mining (CPU % sample) | ✅ | ✅ (fixed: /proc/stat) | ✅ (sysctl kern.cp_time) | | Idle-mode mining (CPU % sample) | ✅ | ✅ (fixed: /proc/stat) | ✅ (sysctl kern.cp_time) |
| Headless Linux screenshot | N/A | ✅ with Xvfb — see below | N/A |
Use **Tier 3 Windows VM** when validating spread, GPU, screenshot, or aggressive ops. Use **Tier 2 Linux** for faster C2 regression on recon + mining. Use **Tier 3 Windows VM** when validating spread, GPU, screenshot, or aggressive ops. Use **Tier 2 Linux** for faster C2 regression on recon + mining.
--- ---
## Linux headless screenshot
Docker/CI/VPS agents have no real display. `scrot`, `import`, and `gnome-screenshot` need an X server.
**Install and start a virtual framebuffer:**
```bash
apt-get install -y xvfb scrot
Xvfb :99 -screen 0 1280x720x24 &
export DISPLAY=:99
```
Then run the Crucible **Screenshot** action as usual (agent picks up `scrot` on PATH).
**One-shot custom command** (Crucible screenshot command field — no persistent `DISPLAY` in the agent process):
```bash
DISPLAY=:99 Xvfb :99 -screen 0 1280x720x24 & sleep 1; scrot -q 55 /tmp/s.jpg && base64 -w0 /tmp/s.jpg
```
On Alpine or minimal images, use the agent forge **custom screenshot command** field with the same pattern.
**Unsupported platforms** (`freebsd`, etc.) return `screenshot not supported on this platform` from `screenshot_stub.go`.
---
## References ## References
- Automated test phases: `tests/README.md` - Automated test phases: `tests/README.md`