All 6 timeout values configurable via DB settings. Fallback to defaults confirmed for fresh installs. Path traversal defense verified (403 + logging). Fixed hardcoded duration references in timeout.go log output. 163 tests pass, no regressions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 KiB
E-1c Verification Report
Date: 2026-03-29 Branch: culurien Verifier: Claude (automated)
Part 1: Build & Test Results
Builds
aggregator-server: go build ./... → BUILD_OK
aggregator-agent: go build ./cmd/... → BUILD_OK
Test Suite
Server: 103 passed, 0 failed (7 packages)
Agent: 60 passed, 0 failed (10 packages)
Total: 163 tests, 0 failures
PASS
Part 2: Migration 030 Audit
2a. Row Count and Keys — PASS
Exactly 6 rows inserted, all category = 'operational':
| Key | Default | Unit |
|---|---|---|
offline_check_interval_seconds |
120 | seconds |
offline_threshold_minutes |
10 | minutes |
token_cleanup_interval_hours |
24 | hours |
sent_command_timeout_hours |
2 | hours |
pending_command_timeout_minutes |
30 | minutes |
timeout_check_interval_minutes |
5 | minutes |
All defaults match original hardcoded values.
2b. Down Migration — PASS
DELETE FROM security_settings WHERE category = 'operational';
2c. Unique Constraint — PASS
Migration 020 line 20: UNIQUE(category, key) confirmed on security_settings table.
2d. Idempotency — PASS
ON CONFLICT (category, key) DO NOTHING present. No other statements. Running twice is safe (ETHOS #4).
Part 3: Timeout Configuration Audit
3a. getOperationalSetting() Fallback Chain — PASS
func getOperationalSetting(svc, key, defaultVal) int {
if svc == nil → return defaultVal // ✓ nil service
val, err := svc.GetSetting(...)
if err != nil → return defaultVal // ✓ DB error / row not found
if f, ok := val.(float64); ok && f > 0 // ✓ type check + positive check
→ return int(f)
return defaultVal // ✓ wrong type or non-positive
}
Zero-duration ticker protection: f > 0 prevents zero or negative values. PASS.
3b. Unit Conversions — PASS
| Variable | Key | Multiplier | Result |
|---|---|---|---|
offlineCheckInterval |
offline_check_interval_seconds (120) |
time.Second |
2m0s ✓ |
offlineThreshold |
offline_threshold_minutes (10) |
time.Minute |
10m0s ✓ |
tokenCleanupInterval |
token_cleanup_interval_hours (24) |
time.Hour |
24h0m0s ✓ |
sentTimeout |
sent_command_timeout_hours (2) |
time.Hour |
2h0m0s ✓ |
pendingTimeout |
pending_command_timeout_minutes (30) |
time.Minute |
30m0s ✓ |
checkInterval |
timeout_check_interval_minutes (5) |
time.Minute |
5m0s ✓ |
No unit mismatches.
3c. NewTimeoutService() Constructor — PASS
Accepts sentTimeout, pendingTimeout, checkInterval as time.Duration parameters.
Zero-value fallbacks:
sentTimeout <= 0→ 2h ✓pendingTimeout <= 0→ 30m ✓checkInterval <= 0→ 5m ✓
3d. Call Site — PASS
Line 275: services.NewTimeoutService(commandQueries, updateQueries, sentTimeout, pendingTimeout, checkInterval) — all 3 values passed.
3e. Startup Log — PASS
[INFO] [server] [config] operational_timeouts_loaded offline_check=%s offline_threshold=%s token_cleanup=%s sent_cmd_timeout=%s pending_cmd_timeout=%s timeout_check=%s
All 6 values logged. ETHOS format. No hardcoded literals remain in goroutines — offlineCheckInterval, offlineThreshold, tokenCleanupInterval variables used.
3f. Backward Compatibility (migration not run) — PASS
Trace: GetSetting("operational", key) → row not found → error returned → getOperationalSetting returns default → server starts with hardcoded defaults. Confirmed by reading the fallback chain. Existing installations work without migration 030.
Part 4: Path Sanitization Audit
4a. Sanitization Logic — PASS
filepath.Abs(pkg.BinaryPath)called (line 185)allowedDirresolved fromconfig.BinaryStoragePath(line 192)strings.HasPrefix(absPath, allowedDir + separator)check (line 196)- Outside → 403 + error log (lines 197-199)
- Inside →
c.File(absPath)(line 229)
4b. Symlink Concern — KNOWN LIMITATION
filepath.Abs() does NOT follow symlinks. A symlink at binaries/evil -> /etc would pass the prefix check.
TestDownloadsRejectsSymlinkEscape tests the concept by manually calling filepath.EvalSymlinks() then checking the resolved path against the prefix — this proves a symlink-resolved path outside the dir is detected. However, the handler itself does not call EvalSymlinks, so the test demonstrates awareness but not actual handler behavior.
Residual risk: LOW. Requires both DB compromise (to set BinaryPath) and filesystem access (to create symlink). Documented as DEV-038.
4c. Allowed Directory Config — PASS
REDFLAG_BINARY_STORAGE_PATHenv var read inconfig.goline 168- Default:
./binaries - Empty protection:
filepath.Abs("")returns CWD (not empty), soallowedDiris never empty string. TheHasPrefixcheck would then allow all files under CWD — wider than intended but not a bypass. The fallback on line 193-194 (if allowedDir == "") is defensive code that cannot actually trigger.
4d. Error Log Format — PASS
[ERROR] [server] [downloads] path_traversal_attempt package_id=%s resolved_path=%s allowed_dir=%s
No emoji. ETHOS compliant.
4e. Security Tests — PASS
TestDownloadsRejectsPathTraversal: Uses../../../etc/passwd, confirms prefix check rejects. ✓TestDownloadsAcceptsSafePath: Creates temp dir, file inside, confirms prefix check accepts. ✓TestDownloadsRejectsSymlinkEscape: Creates symlink, resolves via EvalSymlinks, confirms outside-dir path is caught. ✓ (tests concept, not handler — see 4b)
Part 5: Settings Integration Check
5a. UI Display — KNOWN LIMITATION
SecuritySettings.tsx hardcodes category sections (command_signing, update_signing, nonce_validation, machine_binding, signature_verification). The new operational category is NOT displayed in the UI. Settings are accessible via the API (GET /security/settings) but not visible in the admin dashboard.
5b. Runtime Updates — KNOWN LIMITATION
The server reads timeout values once at startup and stores them in local variables. Changing a setting via PUT /security/settings/operational/offline_threshold_minutes updates the DB but the running goroutines continue using the boot-time values. A server restart is required for changes to take effect.
5c. Validation — KNOWN LIMITATION
ValidateSetting() has no cases for operational.* keys. The switch/case falls through to return nil (no validation). The validation_rules JSONB field stores {"min": 30, "max": 3600} but nothing in the code reads or enforces it. An admin could set offline_threshold_minutes to 0 or 9999 via the API. However, getOperationalSetting() would reject 0 (the f > 0 check), and 9999 minutes (6.9 days) would be allowed but operationally problematic.
Part 6: ETHOS Compliance
6a. Log Statements — PASS (after fix)
New log statements use log.Printf with [TAG] [server] [component] format:
[INFO] [server] [config] operational_timeouts_loaded(main.go:272)[INFO] [server] [timeout] service_started(timeout.go:48)[INFO] [server] [timeout] timed_out_commands(timeout.go:108 — FIXED during verification)[ERROR] [server] [agents] mark_offline_failed(main.go:444)[ERROR] [server] [downloads] path_traversal_attempt(downloads.go:197)
Issue fixed: Line 108 in timeout.go had hardcoded >2h and >30m in the log string, which would be misleading with non-default durations. Changed to log actual ts.sentTimeout and ts.pendingTimeout values.
6b. Banned Words — PASS
Zero results for enhanced, seamless, robust in new code.
6c. Idempotency — PASS
- Migration 030: ON CONFLICT DO NOTHING ✓
- TimeoutService zero-value fallback ✓
- getOperationalSetting fallback ✓
Part 7: Pre-Integration Checklist
- 163 tests pass, zero failures
- Migration 030 seeds 6 settings correctly
- ON CONFLICT DO NOTHING confirmed (idempotent)
- Unique constraint on (category, key) confirmed
- All 6 timeout values read from DB at startup
- Unit conversions correct (seconds/minutes/hours)
- Fallback to defaults when DB not seeded
- Zero-duration protection in getOperationalSetting
- Startup log shows all 6 resolved values
- Path sanitization uses filepath.Abs()
- Empty allowedDir protected (resolves to CWD, never empty)
- Path traversal returns 403 + error log
- REDFLAG_BINARY_STORAGE_PATH in config.go
- Settings updatable via API (restart required — documented)
- ETHOS compliant throughout
Issues Found & Fixed
| # | Issue | Severity | Fix |
|---|---|---|---|
| 1 | timeout.go log line 108 had hardcoded >2h / >30m text |
LOW | Changed to log ts.sentTimeout / ts.pendingTimeout values |
| 2 | timeout.go comments on lines 79, 92 referenced hardcoded durations | LOW | Updated to say "configurable, default X" |
Known Limitations (Not Bugs)
| # | Limitation | Impact |
|---|---|---|
| 1 | operational settings not visible in SecuritySettings UI |
Manageable — accessible via API |
| 2 | Timeout changes require server restart | Acceptable — documented in E1c_Fix_Implementation.md |
| 3 | No min/max validation for operational settings | LOW — f > 0 prevents zero; extreme values operationally problematic but not dangerous |
| 4 | Symlinks not resolved in path sanitization | LOW — requires both DB and filesystem compromise (DEV-038) |
Git Log
5ae114d feat(config): E-1b/E-1c TypeScript strict compliance, configurable timeouts, path sanitization
73f54f6 feat(ui): E-1a complete stubbed features
7b46480 docs: E-1 incomplete features audit
4ec9f74 verify: D-2 ETHOS compliance sweep verified
b52f705 fix(ethos): D-2 ETHOS compliance sweep
0da7612 test(ethos): D-2 pre-fix tests for ETHOS compliance violations
47aa1da docs: D-2 ETHOS compliance audit — pre-existing violations
d43e5a2 verify: D-1 machine ID fixes verified