fix(runtime-ingest): unify /behavior + /agent-liveness into one report envelope (JEF-336)#183
Merged
thejefflarson merged 1 commit intoJul 6, 2026
Conversation
…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
deleted the
thejefflarson/jef-336-unify-behavior-agent-liveness-into-one-report-envelope-fixes
branch
July 6, 2026 08:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Fixes a live prod bug: the dashboard shows "no agents connected". The agent posted the JEF-308 liveness beacon as a single
AgentReportto/agent-liveness, but that handler expectedJson<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
behaviorcrate)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
/behavioraccepts EITHER the{observations, liveness}envelope OR a bare[...]array of observations, via a serde-untaggedBehaviorIngest(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
RuntimeReport+ round-trip / omission tests.observe/runtime.rs): one/behaviorhandler ingests observations (semantics UNCHANGED, incl.Behavior::Alert→ corroboration) and records envelopeliveness;/agent-livenessroute removed. Bearer auth + 256 KB body cap + rate limit are untouched Router layers;MAX_BATCHtruncation still applies.reporter.rs/main.rs): fold liveness into the/behaviorenvelope; 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:6443is 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/behavioragainst 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