All checks were successful
Run Check Script / check (pull_request) Successful in 2m30s
Third Score companion (after AgentObservation and SmokeTest), per
ADR-023 P7 — new framework capabilities attach as companions rather
than as additions to the Score / Interpret public API. Powers the
dashboard's "View logs" UX: customer clicks the button, gets the last
N lines of a deployment's container output from the device.
Trait + transport-side impl + unit tests ship now. The agent-side
Verb::Logs handler and the operator dashboard handler land in
follow-up PRs against the contract locked here — splitting keeps each
diff focused and reviewable.
Three small types + a NATS-backed impl, zero edits to Score /
Interpret / Maestro:
LogChunk pure value: source identifier, captured_at, lines
(oldest-first), truncated flag. No transport,
no async, no IO — the dashboard renders it,
the transport layer constructs it.
LogQueryError six arms, each mapped to a distinct operator
action (DeviceOffline vs Timeout vs Agent vs
BadReply vs Transport vs InvalidReply). Mirrors
the FleetCommandsClient::CommandError shape used
by Verb::Ping so callers see uniform error
surfaces across verbs.
LogQuery<T> companion trait paired with a Score by associated
type — Q: LogQuery<T, Score = S> is the same
compile-time lock SmokeTest uses. A future
K8sLogQuery follows the same shape, no
Box<dyn LogQuery> needed (topologies are
compile-time per ADR-023 P6).
PodmanLogQuery NATS request/reply impl targeting
device-commands.<id>.logs. Splits routing
(LogQueryRouting) from transport so unit tests
verify the exact wire bytes without a NATS
client. Saturates LogsRequest.lines at
LOGS_MAX_LINES on the operator side as
defense-in-depth (the agent will clamp again).
reconciler-contracts gains Verb::Logs, LogsRequest, LogsReply, and
the LOGS_MAX_LINES bound. The wire shape lives there (not in the
deploy crate) so the agent build — which must not depend on harmony
— can serialize the same bytes. Adding the verb required zero
permission template changes: the agent's existing
device-commands.<id>.> subscription already covers it, and the
verb stays the trailing subject token so Verb::as_subject_token
keeps its invariant.
Tests assert behavior, not shape: subject_matches_documented_format
locks the wire so a callout permission change can't silently break
routing, request_body_clamps_oversized_n proves the
buggy-dashboard-show-all-button can't get through unchecked,
decode_reply_rejects_invalid_source_name proves a malicious agent
can't smuggle control characters past ProbeName validation, and
paired_score_type_is_podman_v0_score is a compile-time check that
catches refactors changing the associated type without updating
callers. 77 unit tests total across both crates, all passing without
requiring a real podman socket or NATS server.
Deferred (in scope of v0.3, separate PRs):
- Agent-side Verb::Logs handler in command_server.rs (parses
LogsRequest, resolves deployment->container with stricter
[a-zA-Z0-9_.-]{1,128} validation, runs podman logs --tail,
serializes LogsReply).
- Operator dashboard handler at
/deployments/<name>/devices/<id>/logs.
- End-to-end integration test through a real podman container.