fix(hub): slice-3 follow-ups — /host-report 413 oversize + contract golden (v0.7.1)

- handleHostReport: read maxHostReportBytes+1 (4 MiB const) and reject oversize with
  413 instead of silent LimitReader truncation. Controller handleReport (1 MiB) is
  unchanged. Test asserts 413.
- contract: hub/internal/api/testdata/host-report.golden.json (byte-identical with
  felhom-agent's copy) + TestHostReport_GoldenContract drives the real handler and
  asserts 200 + denorm + both guests upserted.
- CHANGELOG v0.7.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 18:31:44 +02:00
parent 23611c20ef
commit 4be3bdf486
5 changed files with 144 additions and 5 deletions
+12 -3
View File
@@ -226,6 +226,13 @@ func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) {
// per-host override UI yet — that is a later slice).
const defaultHostPollSeconds = 900
// maxHostReportBytes bounds a host-report body. Larger than the controller path's
// 1 MiB because host reports carry the full guest list + (later) storage/backup
// arrays. We read one byte past it and reject explicitly (413) rather than letting
// LimitReader silently truncate — a truncated-but-valid JSON would otherwise be
// accepted as a partial report, dropping guests from the mirror.
const maxHostReportBytes = 4 << 20 // 4 MiB
// hostReportPayload is the subset of the agent host-report (slice-3 contract,
// §3 / agent spec §4) the hub needs for denorm + guest reality. Unknown fields
// (storage_targets/backups/restore_tests/pbs_snapshots/audit_tail) are ignored,
@@ -258,13 +265,15 @@ func (h *Handler) handleHostReport(w http.ResponseWriter, r *http.Request) {
return
}
// 4 MiB: host reports carry the full guest list + future storage/backup arrays;
// the controller path's 1 MiB is too tight here.
body, err := io.ReadAll(io.LimitReader(r.Body, 4<<20))
body, err := io.ReadAll(io.LimitReader(r.Body, maxHostReportBytes+1))
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
if len(body) > maxHostReportBytes {
http.Error(w, "Payload too large", http.StatusRequestEntityTooLarge)
return
}
var rep hostReportPayload
if err := json.Unmarshal(body, &rep); err != nil || rep.HostID == "" {
http.Error(w, "Invalid payload: host_id required", http.StatusBadRequest)