This commit is contained in:
ruvnet
2026-07-27 23:42:10 +00:00
parent cf1a5485f7
commit 60711cb4fd
4 changed files with 69 additions and 25 deletions
@@ -82,6 +82,11 @@ The entity registry is a `RwLock<HashMap<EntityId, EntityEntry>>` backed by an a
`DeviceRegistry` mirrors HA's `core.device_registry` schema (version 13). Devices are identified by a set of `(id_type, id_value)` tuples (the `identifiers` field), which matches HA's pattern of accepting multiple identifier types per device (MAC address, serial number, integration-specific ID). `DeviceRegistry` mirrors HA's `core.device_registry` schema (version 13). Devices are identified by a set of `(id_type, id_value)` tuples (the `identifiers` field), which matches HA's pattern of accepting multiple identifier types per device (MAC address, serial number, integration-specific ID).
`DeviceEntry` and the in-memory `DeviceRegistry` are implemented. On server
startup, entity and device registry files are restored in deterministic key
order with a configurable hard row bound; malformed individual entries are
isolated and reported.
--- ---
## 3. HA-side reference table ## 3. HA-side reference table
@@ -148,6 +148,12 @@ correctness, fail-closed write integrity, semantic-store NaN poisoning, and PII
- **Memory-DoS — `get_state_history` was unbounded.** No `LIMIT`, so a wide time window over a - **Memory-DoS — `get_state_history` was unbounded.** No `LIMIT`, so a wide time window over a
high-frequency entity loaded an unbounded row set into memory. Now capped at high-frequency entity loaded an unbounded row set into memory. Now capped at
`MAX_HISTORY_ROWS` (1,000,000); sibling search paths were already `k`-bounded. `MAX_HISTORY_ROWS` (1,000,000); sibling search paths were already `k`-bounded.
- **Startup state restoration.** `latest_states(limit)` selects one newest row
per entity with `(last_updated_ts, state_id)` tie-breaking, orders results by
entity ID, and caps requests at 100,000. Malformed rows are skipped with
typed warnings. `restore_latest` preserves recorded timestamps and installs
snapshots with a `homecore.restore` context before the recorder listener and
automation engine start.
- **Disk-DoS / documented-but-missing `purge`.** The README advertised `Recorder::purge`, but - **Disk-DoS / documented-but-missing `purge`.** The README advertised `Recorder::purge`, but
no retention path existed → unbounded disk growth. Added a **transactional** `purge(older_than)` no retention path existed → unbounded disk growth. Added a **transactional** `purge(older_than)`
with an **exclusive** cutoff (idempotent, no off-by-one) that deletes old `states`/`events` and with an **exclusive** cutoff (idempotent, no off-by-one) that deletes old `states`/`events` and
@@ -224,8 +224,9 @@ touched:
SHA-256-checks the module, Ed25519-verifies the signature against SHA-256-checks the module, Ed25519-verifies the signature against
`publisher_key`, and enforces a `PluginPolicy` trust allowlist `publisher_key`, and enforces a `PluginPolicy` trust allowlist
(secure-default rejects unsigned/untrusted/tampered modules). (secure-default rejects unsigned/untrusted/tampered modules).
- **HAP real pairing (P2)** — SRP/HKDF pairing + encrypted sessions; current - **HAP real pairing (P2)** — **DONE (2026-07-27 addendum below).** SRP/HKDF
bridge is an accessory-mapping surface. **ACCEPTED-FUTURE (honestly stubbed).** Pair-Setup, transcript-authenticated Pair-Verify, encrypted sessions, and
administrator-only pairing management now land as one fail-closed boundary.
- **`RunMode::Queued`/`Restart`/`max` ordering** — ~~`Single`/`Parallel` are - **`RunMode::Queued`/`Restart`/`max` ordering** — ~~`Single`/`Parallel` are
honored; bounded queueing, restart-kill, and `max` concurrency are not yet honored; bounded queueing, restart-kill, and `max` concurrency are not yet
wired (every non-Single mode is parallel).~~ **DONE — ADR-162 §A5.** Restart wired (every non-Single mode is parallel).~~ **DONE — ADR-162 §A5.** Restart
@@ -336,3 +337,35 @@ is still delivered (old code: 5s-timeout panic).
+1 api-root accept-guard, +1 WS lag-survival), 0 failed. Workspace green. +1 api-root accept-guard, +1 WS lag-survival), 0 failed. Workspace green.
Python deterministic proof unchanged (homecore-api is off the signal proof Python deterministic proof unchanged (homecore-api is off the signal proof
path). path).
## Addendum — HAP cryptographic boundary completed (2026-07-27)
The P2 HAP deferral recorded above is closed as a single security boundary in
`homecore-hap`; it was not replaced with a success-shaped partial protocol.
- Pair-Setup M1-M6 uses RustCrypto SRP-6a with the RFC 5054 3072-bit group,
SHA-512 and HAP proof compatibility, followed by the specified
HKDF-SHA512, ChaCha20-Poly1305, and Ed25519 transcript construction.
- Pair-Verify M1-M4 uses ephemeral X25519, strict Ed25519 transcript
verification, and separately derived directional control keys.
- The TCP server changes to authenticated HAP record framing only after the
plaintext M4 response is written. Record lengths are authenticated, plaintext
is capped at 1024 bytes, counters are independent and monotonic, and any
authentication/replay/framing failure closes without an oracle response.
- Accessory identity, signing seed, SRP verifier, and controller pairings share
one versioned, bounded, permission-checked, atomically replaced store. The raw
setup code is disclosed only on first provisioning and is not persisted.
- Protected endpoints require an encrypted Pair-Verify session. Pairing
management rechecks current persisted administrator authority, handles the
last-admin invariant, updates mDNS paired state, and revokes live sessions.
Evidence includes a deterministic HAP SRP vector, complete in-process
Pair-Setup and Pair-Verify ceremonies, malformed/proof/transcript tests, record
tamper/replay/oversize tests, persistence lifecycle tests, and a real TCP test
that verifies Pair-Verify, accesses `/accessories` over encrypted records, then
proves replay closes the connection.
This closes the cryptographic implementation item, not the entire Apple Home
product surface. Current-Apple/MFi interoperability has not been certified;
transient/split Pair-Setup, writable/timed characteristics, resource endpoints,
and persisted AID/IID allocation remain explicitly unsupported.
@@ -2,7 +2,7 @@
| Field | Value | | Field | Value |
|-------|-------| |-------|-------|
| **Status** | Accepted — P1 scaffold (full conversion deferred to P2) | | **Status** | Accepted — registry/config persistence implemented |
| **Date** | 2026-05-25 | | **Date** | 2026-05-25 |
| **Deciders** | ruv | | **Deciders** | ruv |
| **Codename** | **HOMECORE-MIGRATE** | | **Codename** | **HOMECORE-MIGRATE** |
@@ -44,8 +44,8 @@ files are read, how schema versions are validated, and what happens on an unknow
## 2. Decision ## 2. Decision
Ship `homecore-migrate` as a CLI + library that reads an existing HA filesystem and imports Ship `homecore-migrate` as a CLI + library that reads an existing HA filesystem and imports
its configuration into HOMECORE. P1 is a **scaffold**: it parses and inspects everything and its configuration into HOMECORE. Registry and config-entry conversion are durable; automation
converts the entity registry; full conversion of the remaining artifacts is deferred to P2. conversion and secret-reference resolution remain deferred.
### 2.1 Storage reader + versioned format gate (P1, shipped) ### 2.1 Storage reader + versioned format gate (P1, shipped)
@@ -57,22 +57,25 @@ converts the entity registry; full conversion of the remaining artifacts is defe
unknown `minor_version` is a **hard error** (`MigrateError::UnsupportedSchemaVersion`), unknown `minor_version` is a **hard error** (`MigrateError::UnsupportedSchemaVersion`),
never a silent best-effort parse. Better to refuse than to corrupt. never a silent best-effort parse. Better to refuse than to corrupt.
### 2.2 Per-artifact parsers (P1, shipped) ### 2.2 Per-artifact conversion (shipped)
- `entity_registry::load()``core.entity_registry``Vec<homecore::EntityEntry>` - `entity_registry::load()``core.entity_registry``Vec<homecore::EntityEntry>`
(ready for import). (ready for import).
- `device_registry::load()``core.device_registry` `Vec<DeviceImport>` (P1 diagnostic; - `device_registry::read_device_registry()` converts the supported v13 device fields into
full conversion P2). `homecore::DeviceEntry`; `write_device_registry()` emits an HA-compatible v13 envelope.
- `config_entries::load()``core.config_entries` → domain counts + integration names - `config_entries::convert_config_entries()` emits versioned `homecore.config_entries`
(the format is undocumented per §6 Q5; treated diagnostically). storage. Original rows are retained verbatim, while unsupported domains and fields produce
typed warnings instead of being discarded.
- `secrets::load_secrets()``secrets.yaml``HashMap<String, String>` (resolution P2). - `secrets::load_secrets()``secrets.yaml``HashMap<String, String>` (resolution P2).
- `automations::load()``automations.yaml` → count + ID/alias list (conversion P2). - `automations::load()``automations.yaml` → count + ID/alias list (conversion P2).
### 2.3 CLI (P1, shipped) ### 2.3 CLI
- `homecore-migrate inspect <ha-dir>` previews what will be migrated (entity/device/config - `homecore-migrate inspect <ha-dir>` previews what will be migrated (entity/device/config
counts, redacted secret/automation lists) (`src/cli.rs`, `src/main.rs`). counts, redacted secret/automation lists) (`src/cli.rs`, `src/main.rs`).
- `import-entities` and `export-for-sidecar` are declared but their full behaviour is P2. - `import-entities`, `import-devices`, and `import-config-entries` write destination files and
emit one-line JSON summaries. Writes use synced same-directory temporary files and atomic
no-clobber publication; an existing destination is never implicitly replaced.
### 2.4 Structured errors (P1, shipped) ### 2.4 Structured errors (P1, shipped)
@@ -88,27 +91,25 @@ converts the entity registry; full conversion of the remaining artifacts is defe
file path and a coarse location (`serde_yaml::Error::location()`), never the scalar content. file path and a coarse location (`serde_yaml::Error::location()`), never the scalar content.
Pinned by `secrets::tests::malformed_secrets_error_never_contains_secret_value` (asserts the Pinned by `secrets::tests::malformed_secrets_error_never_contains_secret_value` (asserts the
rendered error **and its full `#[source]` chain** never contain the secret value). rendered error **and its full `#[source]` chain** never contain the secret value).
**Review dimensions confirmed clean with evidence:** source is never mutated (no **Review dimensions confirmed clean with evidence:** source is never mutated; destination
`fs::write`/`remove`/`create` anywhere — P1 reads source, writes nothing); paths are writes are explicit `--to` paths and no-clobber; paths are
user-supplied dirs joined with fixed filenames (no `..`/absolute traversal beyond the user-supplied dirs joined with fixed filenames (no `..`/absolute traversal beyond the
user's own privileges); malformed/typed/truncated `.storage` JSON and YAML **error, never user's own privileges); malformed/typed/truncated `.storage` JSON and YAML **error, never
panic** (every production `unwrap`/`expect` is test-only); unknown schema `minor_version` panic** (every production `unwrap`/`expect` is test-only); unknown schema `minor_version`
hard-errors fail-closed; no SQL/shell/path injection surface (the tool emits diagnostics hard-errors fail-closed; no SQL/shell injection surface.
only, persists nothing in P1).
### 2.5 Deferred to P2+ (NOT built — honestly labelled) ### 2.5 Deferred to P2+ (NOT built — honestly labelled)
- Convert `config_entries` HOMECORE plugin manifests. - Execute imported config entries (a matching HOMECORE plugin must claim the preserved domain).
- Convert `automations.yaml``homecore-automation` YAML. - Convert `automations.yaml``homecore-automation` YAML.
- Side-by-side runtime mode (requires `homecore-recorder`, ADR-132; behind the `recorder` - Side-by-side runtime mode (requires `homecore-recorder`, ADR-132; behind the `recorder`
Cargo feature, currently a no-op stub). Cargo feature, currently a no-op stub).
- `!secret` reference resolution in non-secrets YAML files. - `!secret` reference resolution in non-secrets YAML files.
### 2.6 Test evidence (as shipped) ### 2.6 Test evidence
- 21 tests (`cargo test -p homecore-migrate`) — 19 as originally shipped plus 2 added by the - Targeted tests cover registry round trips, unknown versions, lossless unsupported config
2026-06 security review (`secrets::tests::malformed_secrets_error_never_contains_secret_value`, fields/domains, malformed input, and crash-safe/no-overwrite destination behaviour.
`malformed_secrets_error_reports_location`).
## 3. Consequences ## 3. Consequences
@@ -118,13 +119,12 @@ converts the entity registry; full conversion of the remaining artifacts is defe
schema drift fails loudly instead of corrupting an imported home. schema drift fails loudly instead of corrupting an imported home.
- Reusing HA's own `.storage` and YAML formats means no intermediate export step; the tool - Reusing HA's own `.storage` and YAML formats means no intermediate export step; the tool
reads a live HA install directly. reads a live HA install directly.
- P1 `inspect` gives users a no-risk dry run before any write. - `inspect` gives users a no-risk dry run before any write.
**Negative / honest limits.** **Negative / honest limits.**
- P1 is a **scaffold**: only the entity registry is conversion-ready. Device registry, - Imported config entries are durable but do not install or execute Python HA integrations.
config-entry→plugin, automation, and secret-resolution conversions are P2 and **not yet - Automation conversion and secret-reference resolution are not built.
built** — the Status field and crate docs say so.
- The side-by-side recorder export depends on ADR-132 and is currently a feature-gated - The side-by-side recorder export depends on ADR-132 and is currently a feature-gated
no-op. no-op.
- Performance figures in the README (envelope parse < 5 ms, 1 000-entity load < 50 ms) are - Performance figures in the README (envelope parse < 5 ms, 1 000-entity load < 50 ms) are