Skip to content

fix(runtime-ingest): unify /behavior + /agent-liveness into one report envelope (JEF-336)#183

Merged
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-336-unify-behavior-agent-liveness-into-one-report-envelope-fixes
Jul 6, 2026
Merged

fix(runtime-ingest): unify /behavior + /agent-liveness into one report envelope (JEF-336)#183
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-336-unify-behavior-agent-liveness-into-one-report-envelope-fixes

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

What & why

Fixes a live prod bug: the dashboard shows "no agents connected". The agent posted the JEF-308 liveness beacon as a single AgentReport to /agent-liveness, but that handler expected Json<Vec<AgentReport>> — so every beacon 422'd and no liveness was ever recorded. Observations (/behavior) were unaffected.

Per the operator-approved design, this collapses the two runtime-ingest endpoints into one per-window envelope, so there is one schema, one auth path, and liveness always travels with the report (a quiet node still reports → HEALTHY-quiet, not blind — the "quiet ≠ blind" reason the beacon existed).

Envelope shape (shared behavior crate)

pub struct RuntimeReport {
    pub observations: Vec<RuntimeObservation>, // may be empty (quiet node)
    pub liveness: Option<AgentReport>,         // Option → third-party sensor may send observations only (ADR-0003)
}

Both fields are #[serde(default, skip_serializing_if = ...)], so an observations-only envelope is {"observations":[...]} and a quiet liveness-only one is {"liveness":{...}}.

Backward-compat: route (a) accept both

/behavior accepts EITHER the {observations, liveness} envelope OR a bare [...] array of observations, via a serde-untagged BehaviorIngest (envelope-first; an object matches the envelope, an array matches the bare form, so they never collide). Chosen over envelope-only because it is zero-break, truest to the ADR-0003 tool-agnostic port, and the per-PR e2e itself POSTs a bare array to /behavior (scripts/e2e.sh) — so option (b) would have broken the e2e gate.

Changes

  • behavior: add RuntimeReport + round-trip / omission tests.
  • engine (observe/runtime.rs): one /behavior handler ingests observations (semantics UNCHANGED, incl. Behavior::Alert → corroboration) and records envelope liveness; /agent-liveness route removed. Bearer auth + 256 KB body cap + rate limit are untouched Router layers; MAX_BATCH truncation still applies.
  • agent (reporter.rs / main.rs): fold liveness into the /behavior envelope; drop the separate beacon POST + liveness_url. Quiet-window heartbeat POSTs empty observations + liveness. PROTECTOR_AGENT_ENDPOINT (base URL) is unchanged — the agent still appends /behavior.

Tests

Added: envelope round-trips (with/without liveness); engine records liveness from the envelope; observations still ingest (incl. Alert path); a liveness-less bare payload ingests observations; a quiet node (empty observations + liveness) records liveness without waking the engine; both wire forms deserialize; agent envelope+bearer targets /behavior.

Gates (both workspaces): cargo fmt, cargo clippy --all-targets (warnings-as-errors) clean, cargo nextest run — 725 engine/behavior tests + 39 agent tests pass; the file-size guard (<1000 lines) passes.

On-node verification — BLOCKED (infra)

The ticket asked for a throwaway test-tag deploy to a real 7.0.0 node. The cluster control plane at 192.168.0.20:6443 is refusing connections (host pings, API server down), so I could not deploy/observe on a real node. The per-PR e2e (k3d) posts a bare array to /behavior against a real API server on the prod CNI, which exercises (and gates) the observation-ingest side + my accept-both compat; the 422 fix itself is reproduced by the unit/integration tests (envelope round-trip + engine records liveness). Recommend an architect-side live-node confirmation once the cluster is back before the prod release.

Closes JEF-336

🤖 Generated with Claude Code

https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

…t envelope (JEF-336)

The agent posted the JEF-308 liveness beacon as a single AgentReport to
/agent-liveness, but that handler expected Json<Vec<AgentReport>> — so every
beacon 422'd and no liveness was ever recorded, which is why the dashboard
showed "no agents connected". Observations (/behavior) were unaffected.

Collapse the two endpoints into ONE per-window RuntimeReport envelope
{ observations, liveness: Option<AgentReport> } in the shared behavior crate,
so there is one schema and one auth path and liveness always travels with the
report (a quiet node still reports → HEALTHY-quiet, not blind).

- behavior: add RuntimeReport (liveness is Option so a third-party sensor may
  send observations only, per ADR-0003); round-trip + omission tests.
- engine: the single /behavior handler ingests observations (semantics
  unchanged, incl. Behavior::Alert → corroboration) AND records envelope
  liveness; /agent-liveness route removed. Backward-compat route (a): a serde
  untagged BehaviorIngest accepts EITHER the envelope OR a bare [...] array
  (legacy/third-party), so nothing breaks. Bearer auth + body cap + rate limit
  are unchanged Router layers.
- agent: fold liveness into the /behavior envelope; drop the separate beacon
  POST + liveness_url. Quiet-window heartbeat POSTs empty observations +
  liveness.

Compat route (a) chosen over envelope-only: the per-PR e2e itself POSTs a bare
array to /behavior, and it stays truest to the ADR-0003 tool-agnostic port.

Closes JEF-336

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
@thejefflarson
thejefflarson merged commit 7f4f510 into main Jul 6, 2026
8 checks passed
@thejefflarson
thejefflarson deleted the thejefflarson/jef-336-unify-behavior-agent-liveness-into-one-report-envelope-fixes branch July 6, 2026 08:56
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.

1 participant