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.
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.
`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.
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.
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.