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

View File

@@ -1,12 +1,12 @@
# Hermes Grocy MCP: design
Date: 2026-10-01. Status: written design approved; implementation plan in preparation.
Date: 2026-10-01. Status: approved design implemented; compatibility and limits are documented in the public guides.
## Agreed outcome
Build a lightweight Rust stdio MCP for a self-hosted Grocy instance. Hermes must receive useful data and support full household reads and writes: creation, updates, deletion, and domain actions. Credentials and private connection information belong in the selected desktop wallet. Setup must work for a person without an LLM. Every MCP request to Grocy, including enrollment and diagnostics, goes through its API. Test against a disposable local Docker instance, not an existing household or the public demo. Maintain a clean AGPL-3.0-only repository using the contributor's existing public Git identity, without assistant attribution or personal artifacts.
The conversational direction and this written design are approved. The implementation plan is the remaining architectural review checkpoint. No further preference questions are needed to define the first release.
The design and implementation plan were approved. The first release follows the scope below; concrete compatibility evidence is recorded in docs/compatibility.md.
## Approach and boundaries