feat: verify household contract and document secure Grocy setup

This commit is contained in:
LucaPisl
2026-10-01 02:16:02 +03:00
parent 521087b4da
commit 02891b4357
26 changed files with 1342 additions and 144 deletions

View File

@@ -1,6 +1,6 @@
# Hermes Grocy MCP Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
**Goal:** Build a lightweight Rust stdio MCP that gives Hermes useful household reads and writes through Grocy's API, with protected desktop-wallet enrollment and understandable setup.
@@ -55,7 +55,7 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
**Interfaces:** `ApiBase::parse(input: &str, policy: TransportPolicy) -> Result<ApiBase>`; `ApiBase::route(template: &'static str, params: &BTreeMap<String, String>) -> Result<Url>`; `ApiClient::new(profile: PrivateProfile) -> Result<ApiClient>`; `async ApiClient::request(&self, request: ApiRequest) -> Result<ApiResponse>`. Internal `ApiRequest` contains a static reviewed route template, `HttpMethod`, separately encoded path/query parameters, `RequestBody::{Empty, Json(Value), Binary(Vec<u8>)}`, and `ResponseKind::{Json, Text, Binary}`. `ApiResponse` contains status, decoded bounded body and validated content type. No unchecked request interface is exposed to MCP.
- [ ] **Write `api_boundary` and `write_dispatch_boundary` parameterized tests:**
- [x] **Write `api_boundary` and `write_dispatch_boundary` parameterized tests:**
```rust
assert_eq!(https_base("https://grocy.example/household").path(), "/household/api");
assert_eq!(https_base("https://grocy.example/household/api/").path(), "/household/api");
@@ -69,10 +69,10 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
assert_eq!(write_fixture.attempts(), 1);
assert!(!upstream_error.exposed_text().contains("synthetic-private-marker"));
```
- [ ] **Run red:** `cargo test --test client_boundary`; expect missing implementation or failing boundary assertions.
- [ ] **Implement these interfaces:** reject ambiguous URL encodings before normalization; encode substitutions as individual segments and recheck final origin/prefix. Disable reqwest retries/redirects/proxies/compression; stream bodies with exact caps. Mark writes potentially dispatched before polling send; classify transport/cancellation uncertainty conservatively. Retain no raw upstream body in errors. Custom CA parsing uses the same verified TLS policy. Capture one invalid-certificate fixture in this suite.
- [ ] **Run green:** `cargo test --test client_boundary`; expect all cases passing.
- [ ] **Commit:** `feat: enforce bounded API-only Grocy transport` with only this task's source, tests, license and build files.
- [x] **Run red:** `cargo test --test client_boundary`; expect missing implementation or failing boundary assertions.
- [x] **Implement these interfaces:** reject ambiguous URL encodings before normalization; encode substitutions as individual segments and recheck final origin/prefix. Disable reqwest retries/redirects/proxies/compression; stream bodies with exact caps. Mark writes potentially dispatched before polling send; classify transport/cancellation uncertainty conservatively. Retain no raw upstream body in errors. Custom CA parsing uses the same verified TLS policy. Capture one invalid-certificate fixture in this suite.
- [x] **Run green:** `cargo test --test client_boundary`; expect all cases passing.
- [x] **Commit:** `feat: enforce bounded API-only Grocy transport` with only this task's source, tests, license and build files.
### Task 2: Protect profiles with explicit desktop-wallet selection
@@ -80,7 +80,7 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
**Interfaces:** `CredentialStore: Send + Sync` exposes `load(selection: ProfileSelection) -> BoxFuture<'static, Result<PrivateProfile>>`, `put(selection: ProfileSelection, profile: PrivateProfile, consent: EnrollmentConsent) -> BoxFuture<'static, Result<()>>`, and `forget(selection: ProfileSelection) -> BoxFuture<'static, Result<()>>`. `DesktopStore` implements it. `discover_providers() -> Result<Vec<ProviderCandidate>>`; `inspect_protection(identity: &ProviderIdentity) -> ProtectionEvidence`; `PreferenceStore::{load, save}` accepts only profile labels/provider identities. `EnrollmentConsent` distinguishes verified protection, acknowledged unverified protection and explicitly accepted unsafe backend storage.
- [ ] **Write `selected_wallet_only` and `protected_preferences` tests:**
- [x] **Write `selected_wallet_only` and `protected_preferences` tests:**
```rust
assert_eq!(fake_bus.last_destination(), selected_unique_owner);
assert_eq!(fake_bus.fallback_calls(), 0);
@@ -94,10 +94,10 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
assert_eq!(preferences_mode(), 0o600);
assert!(!preferences_text().contains("synthetic-key-or-url"));
```
- [ ] **Run red:** `cargo test --test credentials`; expect missing adapter or failed policy assertions.
- [ ] **Implement:** use GIO to list names and resolve the selected live unique owner, retaining executable identity for subsequent operations. Open libsecret `Service::open_sync(Service::static_type(), Some(unique_owner), OPEN_SESSION, cancellable)` on a worker thread, and require `session_algorithms() == "dh-ietf1024-sha256-aes128-cbc-pkcs7"` before any Get/SetSecret. Use cancellable deadlines, existing default alias/collection, exact application+profile attributes and no auto-unlocking convenience lookup. Runtime loads only unlocked matching items; setup may explicitly request native unlock. Replacements use the service's single-item replace capability; interruption is reported honestly. Bound protection probes to owned regular files and known headers; KWallet format stays unverified. Preferences use no-follow reads and atomic mode-0600 writes in a mode-0700 app directory. Never expose library error details or silently activate/fallback. Synthetic provider exists only under test modules.
- [ ] **Run green:** `cargo test --test credentials`; expect passing fake-bus and filesystem-policy cases without accessing real wallet secrets.
- [ ] **Commit:** `feat: store profiles in the selected protected desktop wallet`.
- [x] **Run red:** `cargo test --test credentials`; expect missing adapter or failed policy assertions.
- [x] **Implement:** use GIO to list names and resolve the selected live unique owner, retaining executable identity for subsequent operations. Open libsecret `Service::open_sync(Service::static_type(), Some(unique_owner), OPEN_SESSION, cancellable)` on a worker thread, and require `session_algorithms() == "dh-ietf1024-sha256-aes128-cbc-pkcs7"` before any Get/SetSecret. Use cancellable deadlines, existing default alias/collection, exact application+profile attributes and no auto-unlocking convenience lookup. Runtime loads only unlocked matching items; setup may explicitly request native unlock. Replacements use the service's single-item replace capability; interruption is reported honestly. Bound protection probes to owned regular files and known headers; KWallet format stays unverified. Preferences use no-follow reads and atomic mode-0600 writes in a mode-0700 app directory. Never expose library error details or silently activate/fallback. Synthetic provider exists only under test modules.
- [x] **Run green:** `cargo test --test credentials`; expect passing fake-bus and filesystem-policy cases without accessing real wallet secrets.
- [x] **Commit:** `feat: store profiles in the selected protected desktop wallet`.
### Task 3: Define reviewed household records and quantity rules
@@ -105,7 +105,7 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
**Interfaces:** `catalog::entities() -> &'static [EntitySpec]`; `catalog::operations(mode: AccessMode) -> Vec<OperationSpec>`; `catalog::validate(name: &str, args: Value, mode: AccessMode) -> Result<ValidatedCall>`. Specs carry schemas, route/method, read/write/destructive policy and verification strategy. `async records::run(client: &ApiClient, call: &ValidatedCall) -> Result<ToolOutput>`; `resolve_record(rows: &[Value], selector: &RecordSelector) -> Result<RecordId>`; `convert_quantity(amount: Decimal, from: RecordId, stock_unit: RecordId, conversions: &[Conversion]) -> Result<Decimal>`. Define `RecordSelector::{Id, ExactName}` and positive decimal `Conversion` factors; never resolve names by substring.
- [ ] **Write `household_catalog_and_validation`:**
- [x] **Write `household_catalog_and_validation`:**
```rust
assert!(read_only_operations().iter().all(|op| !op.mutates));
assert!(catalog_excludes_accounts_keys_sessions_permissions_and_readonly_view_writes());
@@ -117,10 +117,10 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
assert!(Page::new(0, 501).is_err());
assert!(stock_unit_change_requires_valid_supported_conversion());
```
- [ ] **Run red:** `cargo test --test catalog`; expect catalog/validation failures.
- [ ] **Implement:** reviewed schemas and finite entity CRUD for products, barcodes, locations, groups, units/conversions, shopping lists/items/locations, recipes/ingredients/nestings, meal plans/sections, chores, tasks/categories, batteries, equipment, userfields/entities/objects. Read-only views remain readable. Validate known editable fields against reviewed upstream definitions; reject server-owned/unknown fields. Userfield target entities must also be allowed. Add record description/name lookup tools, restricted assignment-user ID/name projection, deterministic pagination and explicit ambiguity candidates. Validate product references/units/location metadata and only Grocy-supported unit-change procedures. Do not fetch system configuration or permissions to guess capabilities.
- [ ] **Run green:** `cargo test --test catalog`; expect all shared policy cases passing. Review one product CRUD schema before expanding the same catalog pattern across other entities.
- [ ] **Commit:** `feat: add reviewed household records and unit validation`.
- [x] **Run red:** `cargo test --test catalog`; expect catalog/validation failures.
- [x] **Implement:** reviewed schemas and finite entity CRUD for products, barcodes, locations, groups, units/conversions, shopping lists/items/locations, recipes/ingredients/nestings, meal plans/sections, chores, tasks/categories, batteries, equipment, userfields/entities/objects. Read-only views remain readable. Validate known editable fields against reviewed upstream definitions; reject server-owned/unknown fields. Userfield target entities must also be allowed. Add record description/name lookup tools, restricted assignment-user ID/name projection, deterministic pagination and explicit ambiguity candidates. Validate product references/units/location metadata and only Grocy-supported unit-change procedures. Do not fetch system configuration or permissions to guess capabilities.
- [x] **Run green:** `cargo test --test catalog`; expect all shared policy cases passing. Review one product CRUD schema before expanding the same catalog pattern across other entities.
- [x] **Commit:** `feat: add reviewed household records and unit validation`.
### Task 4: Implement domain workflows, files and honest write results
@@ -128,7 +128,7 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
**Interfaces:** Each domain exposes `async run(client: &ApiClient, call: &ValidatedCall) -> Result<ToolOutput>`. `tools::execute(client: &ApiClient, call: ValidatedCall) -> Result<ToolOutput>` dispatches finite catalog entries. `async verify_write(client: &ApiClient, receipt: ApiResponse, target: VerificationTarget) -> Result<ToolOutput>` preserves confirmed dispatch independently of readback. `VerificationTarget::{Readback(ApiRequest), Unavailable}` selects one reviewed bounded read or explicitly unavailable verification. `Attachment` is created only after decoded-size/MIME validation. No upload/download interface accepts a local path or external URL.
- [ ] **Write `workflow_results_and_files`:**
- [x] **Write `workflow_results_and_files`:**
```rust
assert_eq!(volatile_result.data["due_products"][0]["id"], 7);
assert!(successful_204.write.unwrap().status == WriteStatus::Completed);
@@ -140,10 +140,10 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
assert!(filename_traversal_and_oversize_base64_are_rejected_before_request());
assert!(api_ical_is_text_calendar_and_shopping_export_contains_actual_items());
```
- [ ] **Run red:** `cargo test --test workflows`; expect unimplemented actions/results.
- [ ] **Implement:** stock reads/actions/entry edits, copy/merge, barcode variants, bookings/transactions and undo; shopping additions/removals/clear/missing/expired/overdue; recipe fulfillment/copy/consume/shopping additions and transaction undo; chores executions/undo/assignment recalculation/merge; battery charge/undo; task complete/undo and rescheduling through record updates. Inventory allows a new amount of zero; add/consume/transfer/open require positive amounts. Return one suitable bounded readback and distinguish incomplete verification. For grouped stock results, preserve groups and page each reported group explicitly. Files use household groups `equipmentmanuals`, `recipepictures`, `productpictures`, `userfiles`; exclude account avatar mutation. Use Grocy-required base64 filenames and binary PUT bodies; validate image signatures/types, otherwise embedded bounded resources. Export iCal through `/calendar/ical`; produce a shopping-list data export from API records, never call printer endpoints. Unsupported-version operations return explicit capability errors, with no fabricated fallback writes.
- [ ] **Run green:** `cargo test --test workflows`; expect passing domain/result/binary cases. Coverage inventory accounts for every reviewed household upstream route and excluded operation.
- [ ] **Commit:** `feat: implement household actions files and verified write receipts`.
- [x] **Run red:** `cargo test --test workflows`; expect unimplemented actions/results.
- [x] **Implement:** stock reads/actions/entry edits, copy/merge, barcode variants, bookings/transactions and undo; shopping additions/removals/clear/missing/expired/overdue; recipe fulfillment/copy/consume/shopping additions and transaction undo; chores executions/undo/assignment recalculation/merge; battery charge/undo; task complete/undo and rescheduling through record updates. Inventory allows a new amount of zero; add/consume/transfer/open require positive amounts. Return one suitable bounded readback and distinguish incomplete verification. For grouped stock results, preserve groups and page each reported group explicitly. Files use household groups `equipmentmanuals`, `recipepictures`, `productpictures`, `userfiles`; exclude account avatar mutation. Use Grocy-required base64 filenames and binary PUT bodies; validate image signatures/types, otherwise embedded bounded resources. Export iCal through `/calendar/ical`; produce a shopping-list data export from API records, never call printer endpoints. Unsupported-version operations return explicit capability errors, with no fabricated fallback writes.
- [x] **Run green:** `cargo test --test workflows`; expect passing domain/result/binary cases. Coverage inventory accounts for every reviewed household upstream route and excluded operation.
- [x] **Commit:** `feat: implement household actions files and verified write receipts`.
### Task 5: Expose useful and discoverable MCP tools
@@ -151,7 +151,7 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
**Interfaces:** `McpServer::new(store: Arc<dyn CredentialStore>, selection: ProfileSelection, mode: AccessMode) -> McpServer`; `async mcp::serve_stdio(store: Arc<dyn CredentialStore>, selection: ProfileSelection) -> Result<()>`; `to_mcp_result(result: Result<ToolOutput>) -> rmcp::model::CallToolResult`. Server keeps only selection/mode/store and a process-local mutation mutex, loads a fresh profile per call and advertises the Task 3 catalog through `ServerHandler::list_tools/call_tool`.
- [ ] **Write `protocol_returns_data_and_policy`:**
- [x] **Write `protocol_returns_data_and_policy`:**
```rust
assert_eq!(json_text_data(&inventory_result), inventory_result.structured_content.unwrap());
assert_eq!(inventory_data["items"][0]["id"], 7);
@@ -162,10 +162,10 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
assert!(parallel_mutations_are_serialized_and_reads_remain_bounded());
assert!(descriptions_explain_units_dates_paging_and_uncertain_write_recovery());
```
- [ ] **Run red:** `cargo test --test mcp_protocol`; expect discovery/response failures using SDK duplex transport, not a production plaintext profile switch.
- [ ] **Implement:** publish record CRUD/lookup/description tools and named finite domain operations with specific schemas, units and generic examples; truthful read-only/destructive/idempotency/open-world annotations. Register file/export tools with text plus image/resource content. Keep stdout protocol-only, sanitized diagnostics on stderr, bounded input/upload schemas and no request/body logging. Check the profile's current access mode again on every call; fail safely if re-enrollment changes advertised mode. Cancellation must not spawn a replay.
- [ ] **Run green:** `cargo test --test mcp_protocol`; expect actual SDK discovery/calls and result equivalence passing.
- [ ] **Commit:** `feat: expose data-rich household tools over stdio MCP`.
- [x] **Run red:** `cargo test --test mcp_protocol`; expect discovery/response failures using SDK duplex transport, not a production plaintext profile switch.
- [x] **Implement:** publish record CRUD/lookup/description tools and named finite domain operations with specific schemas, units and generic examples; truthful read-only/destructive/idempotency/open-world annotations. Register file/export tools with text plus image/resource content. Keep stdout protocol-only, sanitized diagnostics on stderr, bounded input/upload schemas and no request/body logging. Check the profile's current access mode again on every call; fail safely if re-enrollment changes advertised mode. Cancellation must not spawn a replay.
- [x] **Run green:** `cargo test --test mcp_protocol`; expect actual SDK discovery/calls and result equivalence passing.
- [x] **Commit:** `feat: expose data-rich household tools over stdio MCP`.
### Task 6: Make enrollment and Hermes connection understandable
@@ -173,7 +173,7 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
**Interfaces:** `async setup::run(options: SetupOptions, ui: &mut dyn SetupUi, store: Arc<dyn CredentialStore>) -> Result<SetupOutcome>`; `async setup::doctor(selection: ProfileSelection, store: Arc<dyn CredentialStore>) -> Result<DoctorReport>`; `hermes::render_config(executable: &Path, selection: &ProfileSelection) -> Result<String>`; `async hermes::connect(executable: &Path, selection: &ProfileSelection) -> Result<ConnectOutcome>`. `SetupUi` provides visible input, hidden secret input, defaulted choices, confirmation and messages; tests use scripted answers. Outcomes expose status/nonsecret recovery instructions, not a profile or key.
- [ ] **Write `guided_setup_and_hermes_preservation`:**
- [x] **Write `guided_setup_and_hermes_preservation`:**
```rust
assert_eq!(new_setup.required_private_inputs(), ["instance URL", "API key"]);
assert!(kde_provider_is_recommended_without_assuming_kwallet_password_protection());
@@ -185,10 +185,10 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
assert_eq!(unrelated_hermes_configuration_before, unrelated_hermes_configuration_after);
assert!(rendered_block_contains_no_key_server_url_or_private_metadata());
```
- [ ] **Run red:** `cargo test --test setup`; expect wizard/configuration failures.
- [ ] **Implement:** `setup`, `serve`, `doctor`, `wallets`, `print-config`, `connect-hermes`, `forget`. Choose the desktop provider/default wallet; show native steps for unavailable, locked, unverified or unprotected storage. Explain a dedicated Grocy user, household permissions, Manage API keys, upstream identity-provider 2FA and model/history privacy. Ask URL and masked key, discover version/time/household defaults via API only, reuse prior selections, and show one final review before storage/replacement. Timezone comes from server metadata or local IANA configuration; offer detected choices, with manual search only as recovery. Clarify TLS/custom-CA/Authelia/migration errors without root access. Inspect the installed Hermes CLI preserving-writer interface read-only, then use its conflict-safe writer and discovery check; if unavailable print a credential-free block and exact installation steps. Do not replace YAML wholesale. Forget only the matching wallet item/preferences and explain server-key revocation.
- [ ] **Run green:** `cargo test --test setup`; expect scripted first-run/retry/re-enrollment/conflict flows passing. Walk through the rendered help once as an unassisted user.
- [ ] **Commit:** `feat: guide protected enrollment and Hermes registration`.
- [x] **Run red:** `cargo test --test setup`; expect wizard/configuration failures.
- [x] **Implement:** `setup`, `serve`, `doctor`, `wallets`, `print-config`, `connect-hermes`, `forget`. Choose the desktop provider/default wallet; show native steps for unavailable, locked, unverified or unprotected storage. Explain a dedicated Grocy user, household permissions, Manage API keys, upstream identity-provider 2FA and model/history privacy. Ask URL and masked key, discover version/time/household defaults via API only, reuse prior selections, and show one final review before storage/replacement. Timezone comes from server metadata or local IANA configuration; offer detected choices, with manual search only as recovery. Clarify TLS/custom-CA/Authelia/migration errors without root access. Inspect the installed Hermes CLI preserving-writer interface read-only, then use its conflict-safe writer and discovery check; if unavailable print a credential-free block and exact installation steps. Do not replace YAML wholesale. Forget only the matching wallet item/preferences and explain server-key revocation.
- [x] **Run green:** `cargo test --test setup`; expect scripted first-run/retry/re-enrollment/conflict flows passing. Walk through the rendered help once as an unassisted user.
- [x] **Commit:** `feat: guide protected enrollment and Hermes registration`.
### Task 7: Verify the complete contract and prepare public delivery
@@ -196,7 +196,7 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
**Interfaces:** `scripts/docker_fixture.py` provides `start`, `test`, `stop` using one owned manifest in a temporary directory; start records the LinuxServer image digest, creates only tagged disposable resources and obtains a synthetic key through independent fixture bootstrap. `cargo test --test docker_contract -- --ignored` reads the synthetic fixture manifest in test code only. No released binary accepts fixture credentials. `ApiOnlyProxy` exposes root `/api` and deployment-prefix `/household/api`, rejects everything else and keeps only nonsensitive fixture request counters.
- [ ] **Write `docker_household_contract`:**
- [x] **Write `docker_household_contract`:**
```rust
assert!(sdk_discovery_and_calls_return_real_synthetic_inventory());
assert!(setup_and_doctor_pass_through_api_only_proxy());
@@ -207,11 +207,11 @@ Task 1 defines `Result<T> = std::result::Result<T, AppError>`, `AccessMode::{Rea
assert!(deployment_prefix_is_preserved());
assert!(readonly_profile_cannot_mutate());
```
- [ ] **Run red:** `python scripts/docker_fixture.py start` then `python scripts/docker_fixture.py test`; expect assertions to catch any route/schema/workflow gap. Bootstrap Grocy separately, including migrations if needed, before exposing the API-only proxy. Bind host ports to loopback and keep all state outside source.
- [ ] **Complete contract integration:** one scenario creates its own fixtures (units/locations/products, shopping, recipes/meal plan, chore/task/battery, equipment/custom data), exercises meaningful CRUD/action/undo per domain, and cleans up only its resources. Correct actual API-version mismatches and update the coverage table; no smoke-test claim in place of domain coverage. Record image digest and version in public compatibility docs. Add practical install/build/enroll/Hermes instructions, protection limits, no cross-client revision guarantee and uncertain-write recovery. CI runs fmt/clippy/focused tests; Docker remains an explicit contract command.
- [ ] **Run green and final checks once:** `cargo fmt --check`; `cargo clippy --all-targets -- -D warnings`; `cargo test`; `python scripts/docker_fixture.py test`; `cargo build --release --locked`. Run one RustSec dependency check plus a tracked-source/history privacy inspection for secrets/private names/hosts/addresses and accidental generated artifacts. Inspect CLI help and release discovery. Fix concrete findings and rerun only affected checks.
- [ ] **Perform one final spec review:** confirm API-only routing, all-domain coverage, wallet owner/IPC policy, correct results/write recovery and unassisted setup. Record actual tested limitations rather than claiming unsupported capabilities. Run `python scripts/docker_fixture.py stop` even after failure; never prune unrelated resources.
- [ ] **Commit:** `docs: document verified Grocy compatibility and secure setup`. Deliver the build location, meaningful verification results and any limitations. Publish only sanitized source/docs/tests using the contributor's identity after the verified result is concrete; real-instance enrollment requires the human to enter their hidden key locally.
- [x] **Run red:** `python scripts/docker_fixture.py start` then `python scripts/docker_fixture.py test`; expect assertions to catch any route/schema/workflow gap. Bootstrap Grocy separately, including migrations if needed, before exposing the API-only proxy. Bind host ports to loopback and keep all state outside source.
- [x] **Complete contract integration:** one scenario creates its own fixtures (units/locations/products, shopping, recipes/meal plan, chore/task/battery, equipment/custom data), exercises meaningful CRUD/action/undo per domain, and cleans up only its resources. Correct actual API-version mismatches and update the coverage table; no smoke-test claim in place of domain coverage. Record image digest and version in public compatibility docs. Add practical install/build/enroll/Hermes instructions, protection limits, no cross-client revision guarantee and uncertain-write recovery. CI runs fmt/clippy/focused tests; Docker remains an explicit contract command.
- [x] **Run green and final checks once:** `cargo fmt --check`; `cargo clippy --all-targets -- -D warnings`; `cargo test`; `python scripts/docker_fixture.py test`; `cargo build --release --locked`. Run one RustSec dependency check plus a tracked-source/history privacy inspection for secrets/private names/hosts/addresses and accidental generated artifacts. Inspect CLI help and release discovery. Fix concrete findings and rerun only affected checks.
- [x] **Perform one final spec review:** confirm API-only routing, all-domain coverage, wallet owner/IPC policy, correct results/write recovery and unassisted setup. Record actual tested limitations rather than claiming unsupported capabilities. Run `python scripts/docker_fixture.py stop` even after failure; never prune unrelated resources.
- [x] **Commit:** `docs: document verified Grocy compatibility and secure setup`. Deliver the build location, meaningful verification results and any limitations. Publish only sanitized source/docs/tests using the contributor's identity after the verified result is concrete; real-instance enrollment requires the human to enter their hidden key locally.
## Planning Self-Review