diff --git a/api-docs/adr/ADR-127-homecore-state-machine-rust.md b/api-docs/adr/ADR-127-homecore-state-machine-rust.md index 1887ed3f..4df20d0e 100644 --- a/api-docs/adr/ADR-127-homecore-state-machine-rust.md +++ b/api-docs/adr/ADR-127-homecore-state-machine-rust.md @@ -82,6 +82,11 @@ The entity registry is a `RwLock>` 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). +`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 diff --git a/api-docs/adr/ADR-132-homecore-recorder-history-semantic-search.md b/api-docs/adr/ADR-132-homecore-recorder-history-semantic-search.md index d1b75fe7..1bfb7c09 100644 --- a/api-docs/adr/ADR-132-homecore-recorder-history-semantic-search.md +++ b/api-docs/adr/ADR-132-homecore-recorder-history-semantic-search.md @@ -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 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. +- **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 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 diff --git a/api-docs/adr/ADR-161-homecore-server-layer-security.md b/api-docs/adr/ADR-161-homecore-server-layer-security.md index 6fea0862..5787e073 100644 --- a/api-docs/adr/ADR-161-homecore-server-layer-security.md +++ b/api-docs/adr/ADR-161-homecore-server-layer-security.md @@ -224,8 +224,9 @@ touched: SHA-256-checks the module, Ed25519-verifies the signature against `publisher_key`, and enforces a `PluginPolicy` trust allowlist (secure-default rejects unsigned/untrusted/tampered modules). -- **HAP real pairing (P2)** — SRP/HKDF pairing + encrypted sessions; current - bridge is an accessory-mapping surface. **ACCEPTED-FUTURE (honestly stubbed).** +- **HAP real pairing (P2)** — **DONE (2026-07-27 addendum below).** SRP/HKDF + 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 honored; bounded queueing, restart-kill, and `max` concurrency are not yet 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. Python deterministic proof unchanged (homecore-api is off the signal proof 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. diff --git a/api-docs/adr/ADR-165-homecore-migrate-from-home-assistant.md b/api-docs/adr/ADR-165-homecore-migrate-from-home-assistant.md index 4f68b231..d9e95c03 100644 --- a/api-docs/adr/ADR-165-homecore-migrate-from-home-assistant.md +++ b/api-docs/adr/ADR-165-homecore-migrate-from-home-assistant.md @@ -2,7 +2,7 @@ | Field | Value | |-------|-------| -| **Status** | Accepted — P1 scaffold (full conversion deferred to P2) | +| **Status** | Accepted — registry/config persistence implemented | | **Date** | 2026-05-25 | | **Deciders** | ruv | | **Codename** | **HOMECORE-MIGRATE** | @@ -44,8 +44,8 @@ files are read, how schema versions are validated, and what happens on an unknow ## 2. Decision 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 -converts the entity registry; full conversion of the remaining artifacts is deferred to P2. +its configuration into HOMECORE. Registry and config-entry conversion are durable; automation +conversion and secret-reference resolution remain deferred. ### 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`), 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` (ready for import). -- `device_registry::load()` — `core.device_registry` → `Vec` (P1 diagnostic; - full conversion P2). -- `config_entries::load()` — `core.config_entries` → domain counts + integration names - (the format is undocumented per §6 Q5; treated diagnostically). +- `device_registry::read_device_registry()` converts the supported v13 device fields into + `homecore::DeviceEntry`; `write_device_registry()` emits an HA-compatible v13 envelope. +- `config_entries::convert_config_entries()` emits versioned `homecore.config_entries` + storage. Original rows are retained verbatim, while unsupported domains and fields produce + typed warnings instead of being discarded. - `secrets::load_secrets()` — `secrets.yaml` → `HashMap` (resolution P2). - `automations::load()` — `automations.yaml` → count + ID/alias list (conversion P2). -### 2.3 CLI (P1, shipped) +### 2.3 CLI - `homecore-migrate inspect ` previews what will be migrated (entity/device/config 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) @@ -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. 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). - **Review dimensions confirmed clean with evidence:** source is never mutated (no - `fs::write`/`remove`/`create` anywhere — P1 reads source, writes nothing); paths are + **Review dimensions confirmed clean with evidence:** source is never mutated; destination + writes are explicit `--to` paths and no-clobber; paths are 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 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 - only, persists nothing in P1). + hard-errors fail-closed; no SQL/shell injection surface. ### 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. - Side-by-side runtime mode (requires `homecore-recorder`, ADR-132; behind the `recorder` Cargo feature, currently a no-op stub). - `!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 - 2026-06 security review (`secrets::tests::malformed_secrets_error_never_contains_secret_value`, - `malformed_secrets_error_reports_location`). +- Targeted tests cover registry round trips, unknown versions, lossless unsupported config + fields/domains, malformed input, and crash-safe/no-overwrite destination behaviour. ## 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. - Reusing HA's own `.storage` and YAML formats means no intermediate export step; the tool 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.** -- P1 is a **scaffold**: only the entity registry is conversion-ready. Device registry, - config-entry→plugin, automation, and secret-resolution conversions are P2 and **not yet - built** — the Status field and crate docs say so. +- Imported config entries are durable but do not install or execute Python HA integrations. +- Automation conversion and secret-reference resolution are not built. - The side-by-side recorder export depends on ADR-132 and is currently a feature-gated no-op. - Performance figures in the README (envelope parse < 5 ms, 1 000-entity load < 50 ms) are