feat(fleet-deploy): log-tail contract as a Score companion #295

Open
johnride wants to merge 2 commits from feat/v0-3-logs-companion into feat/smoke-test-contract
Owner

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 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 needed (topologies are
compile-time per ADR-023 P6).
PodmanLogQuery NATS request/reply impl targeting
device-commands..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..> 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//devices//logs.
  • End-to-end integration test through a real podman container.
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.
johnride added 1 commit 2026-05-25 12:24:32 +00:00
feat(fleet-deploy): log-tail contract as a Score companion
All checks were successful
Run Check Script / check (pull_request) Successful in 2m28s
cb83a1edfe
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.
johnride force-pushed feat/v0-3-logs-companion from cb83a1edfe to 0793e72a05 2026-05-26 11:07:07 +00:00 Compare
All checks were successful
Run Check Script / check (pull_request) Successful in 2m30s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/v0-3-logs-companion:feat/v0-3-logs-companion
git checkout feat/v0-3-logs-companion
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: NationTech/harmony#295
No description provided.