refactor(iot): extract iot-contracts crate for cross-boundary types #270

Merged
johnride merged 5 commits from feat/iot-contracts into feat/iot-walking-skeleton 2026-04-21 20:13:17 +00:00
Owner

Consolidate the data types, NATS bucket names, and KV key formats
that were scattered across the IoT operator, on-device agent, and
harmony's podman module. Each was defined in one place and quoted /
reimplemented in the others, which is exactly the kind of contract
drift the roadmap v0.1 §2 called for consolidating before we start
layering new features on top.

New crate iot/iot-contracts:

  • score.rs — IotScore, PodmanV0Score, PodmanService (moved
    from harmony::modules::podman::score). Pure data, no harmony
    deps.
  • kv.rs — BUCKET_DESIRED_STATE, BUCKET_AGENT_STATUS constants,
    desired_state_key(device, deployment), status_key(device).
    These values used to be hard-coded in five places (agent main.rs,
    operator main.rs, operator/deploy/operator.yaml, smoke-a1.sh,
    smoke-a3.sh). Tests lock the literals so a flip can't slip.
  • status.rs — typed AgentStatus { device_id, status, timestamp }.
    Replaces the anonymous serde_json::json!{} the agent was
    publishing, so the operator can deserialize the heartbeat
    payload via a shared struct when §12 v0.1 status aggregation
    lands.

Consumer updates:

  • harmony::modules::podman::score now holds only the
    Score<T> / Interpret<T> trait bindings; the pure types are
    re-exported from iot-contracts. Trait impls can't move because
    the trait lives in harmony, so this is the cleanest split.
  • iot-operator-v0 uses BUCKET_DESIRED_STATE and
    desired_state_key — the inline kv_key fn now delegates so
    the existing internal call sites stay untouched.
  • iot-agent-v0 uses BUCKET_DESIRED_STATE, BUCKET_AGENT_STATUS,
    status_key, and AgentStatus for the heartbeat publish.

No behavior change. Tests: cargo test -p iot-contracts passes
(8/8). Regression: smoke-a3.sh on x86_64 PASSes end-to-end
(reboot-reconnect loop included) — wire format is byte-identical
to the pre-refactor serialization.

Next consumers on deck: operator-side status aggregation (§12 v0.1
#3) and journald log streaming (§12 v0.1 #5), both of which need
shared types across the operator/agent boundary and were the
reason this extraction was prioritized.

Consolidate the data types, NATS bucket names, and KV key formats that were scattered across the IoT operator, on-device agent, and harmony's podman module. Each was defined in one place and quoted / reimplemented in the others, which is exactly the kind of contract drift the roadmap v0.1 §2 called for consolidating before we start layering new features on top. New crate `iot/iot-contracts`: * score.rs — `IotScore`, `PodmanV0Score`, `PodmanService` (moved from `harmony::modules::podman::score`). Pure data, no harmony deps. * kv.rs — `BUCKET_DESIRED_STATE`, `BUCKET_AGENT_STATUS` constants, `desired_state_key(device, deployment)`, `status_key(device)`. These values used to be hard-coded in five places (agent main.rs, operator main.rs, operator/deploy/operator.yaml, smoke-a1.sh, smoke-a3.sh). Tests lock the literals so a flip can't slip. * status.rs — typed `AgentStatus { device_id, status, timestamp }`. Replaces the anonymous `serde_json::json!{}` the agent was publishing, so the operator can deserialize the heartbeat payload via a shared struct when §12 v0.1 status aggregation lands. Consumer updates: * `harmony::modules::podman::score` now holds only the `Score<T>` / `Interpret<T>` trait bindings; the pure types are re-exported from iot-contracts. Trait impls can't move because the trait lives in harmony, so this is the cleanest split. * `iot-operator-v0` uses `BUCKET_DESIRED_STATE` and `desired_state_key` — the inline `kv_key` fn now delegates so the existing internal call sites stay untouched. * `iot-agent-v0` uses `BUCKET_DESIRED_STATE`, `BUCKET_AGENT_STATUS`, `status_key`, and `AgentStatus` for the heartbeat publish. No behavior change. Tests: `cargo test -p iot-contracts` passes (8/8). Regression: `smoke-a3.sh` on x86_64 PASSes end-to-end (reboot-reconnect loop included) — wire format is byte-identical to the pre-refactor serialization. Next consumers on deck: operator-side status aggregation (§12 v0.1 #3) and journald log streaming (§12 v0.1 #5), both of which need shared types across the operator/agent boundary and were the reason this extraction was prioritized.
johnride added 1 commit 2026-04-21 18:57:51 +00:00
refactor(iot): extract iot-contracts crate for cross-boundary types
All checks were successful
Run Check Script / check (pull_request) Successful in 2m13s
24b94a362d
Consolidate the data types, NATS bucket names, and KV key formats
that were scattered across the IoT operator, on-device agent, and
harmony's podman module. Each was defined in one place and quoted /
reimplemented in the others, which is exactly the kind of contract
drift the roadmap v0.1 §2 called for consolidating before we start
layering new features on top.

New crate `iot/iot-contracts`:
  * score.rs — `IotScore`, `PodmanV0Score`, `PodmanService` (moved
    from `harmony::modules::podman::score`). Pure data, no harmony
    deps.
  * kv.rs — `BUCKET_DESIRED_STATE`, `BUCKET_AGENT_STATUS` constants,
    `desired_state_key(device, deployment)`, `status_key(device)`.
    These values used to be hard-coded in five places (agent main.rs,
    operator main.rs, operator/deploy/operator.yaml, smoke-a1.sh,
    smoke-a3.sh). Tests lock the literals so a flip can't slip.
  * status.rs — typed `AgentStatus { device_id, status, timestamp }`.
    Replaces the anonymous `serde_json::json!{}` the agent was
    publishing, so the operator can deserialize the heartbeat
    payload via a shared struct when §12 v0.1 status aggregation
    lands.

Consumer updates:
  * `harmony::modules::podman::score` now holds only the
    `Score<T>` / `Interpret<T>` trait bindings; the pure types are
    re-exported from iot-contracts. Trait impls can't move because
    the trait lives in harmony, so this is the cleanest split.
  * `iot-operator-v0` uses `BUCKET_DESIRED_STATE` and
    `desired_state_key` — the inline `kv_key` fn now delegates so
    the existing internal call sites stay untouched.
  * `iot-agent-v0` uses `BUCKET_DESIRED_STATE`, `BUCKET_AGENT_STATUS`,
    `status_key`, and `AgentStatus` for the heartbeat publish.

No behavior change. Tests: `cargo test -p iot-contracts` passes
(8/8). Regression: `smoke-a3.sh` on x86_64 PASSes end-to-end
(reboot-reconnect loop included) — wire format is byte-identical
to the pre-refactor serialization.

Next consumers on deck: operator-side status aggregation (§12 v0.1
#3) and journald log streaming (§12 v0.1 #5), both of which need
shared types across the operator/agent boundary and were the
reason this extraction was prioritized.
johnride changed target branch from feat/iot-arm-vm to master 2026-04-21 19:05:00 +00:00
johnride changed target branch from master to feat/iot-walking-skeleton 2026-04-21 19:06:01 +00:00
johnride reviewed 2026-04-21 19:10:58 +00:00
johnride left a comment
Author
Owner

This feels very weak overall.

I think we need to take a big step back and revisit what "iot" means. Harmony modules to deploy podman in a loop from definitions received from a nats kv store is not iot specific. It is rather the foundation for our decentralized infrastructure management.

I think we have to revisit the usage of "iot" here. Rasbperry Pi iot is just a concrete use case / implementation detail. The next use case won't necessarily be iot related.

This feels very weak overall. I think we need to take a big step back and revisit what "iot" means. Harmony modules to deploy podman in a loop from definitions received from a nats kv store is not iot specific. It is rather the foundation for our decentralized infrastructure management. I think we have to revisit the usage of "iot" here. Rasbperry Pi iot is just a concrete use case / implementation detail. The next use case won't necessarily be iot related.
@@ -0,0 +16,4 @@
/// A single container managed by podman on the device.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PodmanService {
Author
Owner

podman is not iot specific. Podman belongs as a first-class citizen in harmony . Containers / containerruntime is a first class abstraction (I think it belongs in domain) and podman is either modules or infra. Definitely does not belong hidden in an iot crate.

podman is not iot specific. Podman belongs as a first-class citizen in harmony . Containers / containerruntime is a first class abstraction (I think it belongs in domain) and podman is either modules or infra. Definitely does not belong hidden in an iot crate.
johnride added 2 commits 2026-04-21 19:19:07 +00:00
Replaces an 8-link `.arg("-t").arg("ed25519").arg("-N")…` chain with
a single `.args([...])` of string literals, plus one trailing `.arg()`
for the `&PathBuf` (kept separate so we don't force it through the
`IntoIterator<Item=&str>` channel). No behavior change.
feat(iot-contracts): type AgentStatus fields with Id + DateTime<Utc>
All checks were successful
Run Check Script / check (pull_request) Successful in 2m8s
0d01a71cd5
`AgentStatus.device_id` and `AgentStatus.timestamp` were stringly
typed. Both now carry real types that prevent a whole class of
wire-format typos while keeping the on-wire JSON shape intact.

**device_id: String → harmony_types:🆔:Id**

Agent config + heartbeat payload now share the same `Id` that the
example IoT pipeline already uses for `IotDeviceSetupConfig`. Mixing
a device id with a deployment name or arbitrary `String` is now a
type error. `Id` is re-exported from `iot-contracts` so consumers
don't need a direct `harmony_types` dependency just to name the
field.

To keep the wire format byte-compatible, `harmony_types::Id` gains
`#[serde(transparent)]`. Audit: no consumer in the tree relies on
the previous `{"value": "…"}` shape — `Id` is persisted by sqlite
via `to_string()`, never serialized directly — so this is a
latent-bug fix more than a behavior change.

**timestamp: String → chrono::DateTime<Utc>**

The agent was calling `chrono::Utc::now().to_rfc3339()` and stuffing
the String into the payload. It now holds a real `DateTime<Utc>`
which serde-serializes as RFC 3339 anyway. The smoke script's
reboot-gate lex comparison still works: time-digit prefixes resolve
before the trailing `Z` (chrono default) vs `+00:00` (prior format)
difference matters.

**Plumbing**

- `iot/iot-agent-v0/src/config.rs`: `AgentSection.device_id: Id`.
  TOML deserializes the bare string thanks to `#[serde(transparent)]`.
- `iot/iot-agent-v0/src/main.rs`: `watch_desired_state` and
  `report_status` take `Id` instead of `String`.
- `iot/iot-contracts/Cargo.toml`: adds `harmony_types` path dep and
  `chrono = { workspace, features = ["serde"] }`.

**Verification**

- `cargo test -p iot-contracts`: 8/8 passes. New assertions pin the
  wire format: `"device_id":"pi-01"` (not `{"value":"pi-01"}`) and
  `"timestamp":"2026-04-21T18:15:42Z"` (RFC 3339).
- x86_64 smoke-a3.sh PASSes end-to-end including the reboot-
  reconnect loop — wire format remains compatible with the existing
  smoke-script parsing.
johnride added 2 commits 2026-04-21 19:49:37 +00:00
Review feedback: `ContainerRuntime` is a first-class harmony
capability (already lives at
`harmony/src/domain/topology/container_runtime.rs`) and the Score
types that describe what containers a caller wants running belong
next to the trait impls, not hidden in an IoT-labeled contracts
crate. Putting `PodmanService`, `PodmanV0Score`, and `IotScore` in
`iot-contracts` conflated the product-shape (IoT fleet agent) with
a reusable container-orchestration primitive.

Move the data definitions (plus the three serde tests) back to
`harmony/src/modules/podman/score.rs` where they were before the
extraction in commit 24b94a3. That file now again holds the types
and their `Score<T>` / `Interpret<T>` trait impls in one place.

No behavior change:
- `harmony::modules::podman::{IotScore, PodmanV0Score, PodmanService}`
  re-exports still resolve (through the restored local module rather
  than a forwarded re-export from iot-contracts).
- The single external consumer that imports these types —
  `iot-agent-v0/src/reconciler.rs` — already went through
  `harmony::modules::podman::*`, so no import flip needed.

iot-contracts now holds only the cross-boundary bits that are
genuinely reconciler-wire-format-specific (bucket names + key
helpers, `AgentStatus`, `Id` re-export). A follow-up commit will
rename the crate itself to reflect that scope.

Verification: `cargo test -p harmony --features podman --lib podman`
(3 score tests pass in their restored home), `cargo test -p
iot-contracts` (5 remaining tests), `cargo check --all-features`
clean.
refactor(reconciler): rename iot-contracts → harmony-reconciler-contracts
All checks were successful
Run Check Script / check (pull_request) Successful in 2m29s
75c3ef9bb8
Review feedback: "iot" is the wrong scope label. The pattern this
crate encodes — a central operator writing desired state to NATS
JetStream KV, a remote agent watching KV and reconciling — is the
foundation for harmony's decentralized infrastructure management,
not an IoT thing. Raspberry Pi is one concrete use case; the next
consumers (OKD fleet agents, edge-compute reconcilers, any host
harmony can't reach directly over a control-plane API) aren't IoT
either.

Rename the crate to reflect what it actually is:

- `iot/iot-contracts/` → `harmony-reconciler-contracts/` (moved to
  the repo root, alongside the other support crates).
- Package name `iot-contracts` → `harmony-reconciler-contracts`.
- Consumer `Cargo.toml` path references updated in operator, agent.
- `use iot_contracts::…` → `use harmony_reconciler_contracts::…`
  across agent + operator sources.
- Crate-level prose in lib.rs + kv.rs rewritten to drop the IoT
  framing and describe the reconciler pattern in its own terms.
- harmony/Cargo.toml drops the dep entirely — after the preceding
  commit moved podman Score types back in-tree, harmony no longer
  pulls anything from this crate.

No behavior change. Wire format unchanged — the two existing public
modules (`kv`, `status`) are byte-identical.

Verified:
- `cargo check --all-targets --all-features` clean.
- `cargo test -p harmony-reconciler-contracts` — 5/5 pass.
- x86_64 `smoke-a3.sh` end-to-end PASS (reboot-reconnect included).

Out of scope / follow-up: the operator and agent crate names
(`iot-operator-v0`, `iot-agent-v0`) and `IotScore` are still
IoT-branded. Evaluating whether to flip those in this branch next.
johnride merged commit 8c94c8e61e into feat/iot-walking-skeleton 2026-04-21 20:13:17 +00:00
johnride deleted branch feat/iot-contracts 2026-04-21 20:13:19 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: NationTech/harmony#270
No description provided.