fix(p2p): harden CLI status and operator/key-rotation flows - #76
Conversation
Keep `p2p status` text output machine-parseable by removing ANSI severity colorization, and stop connect/disconnect/forget from throwing on malformed daemon responses (safe .value defaults, uint64 counters). Bound handshake and delta timeouts to kP2pMaxOperationTimeout up front, and ceiling direct sync_interval_ms to the 5-minute reconnect bound so invalid config fails early instead of aborting daemon startup. Degrade memory-sync status to peerCount=0 on a transient peer-registry read instead of failing the whole status IPC. Add an explicit `p2p forget` operation (proto FORGET=9 through dispatcher, manager, and registry) so operators can rotate a peer's pinned key while preserving fail-closed enrollment. Signed-off-by: trvon <git@trevon.dev>
There was a problem hiding this comment.
🟡 Changes recommended
The new forget() flow currently only removes the peer from the registry and does not revoke the running listener’s allowlist, so “forget” may not take effect immediately without additional lifecycle handling.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Hardens the direct-P2P CLI/daemon lifecycle by making status output parseable again, bounding potentially dangerous timeouts/intervals at config-validation time, and adding an explicit “forget” operation to enable operator-driven key rotation while preserving fail-closed enrollment semantics.
Changes:
- Add
p2p forget <node-id>end-to-end (IPC/proto enum + dispatcher + ServiceManager + P2pManager + registry removal) to support explicit key rotation. - Make CLI connect/disconnect/forget formatting resilient to malformed daemon payloads and remove ANSI colorization from
p2p statuskey/value output. - Enforce protocol/config hard ceilings for operation timeouts and direct-transport reconnect interval during validation, with corresponding unit tests.
File summaries
| File | Description |
|---|---|
| tests/unit/daemon/peer_registry_catch2_test.cpp | Adds a key-rotation test covering explicit peer removal then re-enrollment with a new pin. |
| tests/unit/daemon/p2p_delta_catch2_test.cpp | Adds a regression test ensuring delta exchange rejects timeouts above kP2pMaxOperationTimeout. |
| tests/unit/daemon/components/config_resolver_test.cpp | Adds a test that rejects overly-large sync_interval_ms for direct transport at config resolution time. |
| tests/scripts/portability_allowlist.txt | Updates allowlist line references after dispatcher shifts. |
| src/daemon/p2p/p2p_protocol.cpp | Bounds handshake timeout during config validation. |
| src/daemon/p2p/p2p_manager.cpp | Adds forget() to remove a peer from the registry. |
| src/daemon/p2p/p2p_delta.cpp | Bounds delta-exchange timeout during options validation. |
| src/daemon/ipc/proto_serializer.cpp | Updates enum coverage assert to include the new Forget operation. |
| src/daemon/components/ServiceManager.cpp | Degrades status when peer-registry reads fail; adds forgetP2pPeer. |
| src/daemon/components/RequestDispatcher.cpp | Wires new MemorySyncOperation::Forget request handling. |
| src/daemon/components/ConfigResolver.cpp | Rejects direct-transport reconnect interval (sync_interval_ms) above the 5-minute ceiling during config resolution. |
| src/cli/commands/p2p_command.cpp | Removes ANSI severity colorization from status output; adds forget subcommand and formatter; hardens connect/disconnect formatting. |
| include/yams/daemon/p2p/p2p_manager.h | Exposes P2pManager::forget. |
| include/yams/daemon/ipc/proto/ipc_envelope.proto | Adds FORGET = 9 to the MemorySyncRequest operation enum. |
| include/yams/daemon/ipc/ipc_protocol_requests.h | Updates generated MemorySyncOperation enum to include Forget. |
| include/yams/daemon/components/ServiceManager.h | Declares forgetP2pPeer. |
Review details
Suppressed comments (2)
src/cli/commands/p2p_command.cpp:137
- If the daemon response is malformed and
node_idis missing, this printsdisconnectedwith a blank id. Prefer a visible placeholder to avoid confusing output.
return "disconnected " + payload.value().value("node_id", std::string{});
src/cli/commands/p2p_command.cpp:148
- If the daemon response is malformed and
node_idis missing, this printsforgotwith a blank id. Prefer a visible placeholder to avoid confusing output.
return "forgot " + payload.value().value("node_id", std::string{});
- Files reviewed: 16/16 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| return {}; | ||
| } | ||
|
|
||
| Result<void> forget(std::string_view nodeId) { return registry_->removePeer(nodeId); } |
| // Direct transport reuses sync_interval_ms as the reconnect interval, which | ||
| // P2pManager::create bounds to p2p::kP2pMaxReconnectInterval (5 minutes). Reject here so a | ||
| // large value fails config resolution instead of aborting daemon startup later. | ||
| if (policy.transport == "direct" && *interval > 5 * 60 * 1000) { |
| return "connected " + value.value("peer_node_id", std::string{}) + | ||
| " (sent=" + std::to_string(value.value("deltas_sent", std::uint64_t{0})) + | ||
| ", received=" + std::to_string(value.value("deltas_received", std::uint64_t{0})) + ")"; |
Summary
Stacked draft child of #75. Closes the robustness/quality items from the deep review of the #68 stack that were not security-blocking but should land before the stack is moved to ready-for-review. No merge or deploy.
p2p statustext is machine-parseable again (P1) — removed ANSI severity colorization from thekey=valuestream; every field is now a plain value..at("peer_node_id")/.at("node_id")with.value(key, default)and widened thedeltas_sent/deltas_receiveddefaults tostd::uint64_t(nosize_t→unsigned inttruncation).kP2pMaxOperationTimeout(P2) —validateConfig/validateOptionsnow reject a timeout above 1 minute up front instead of passing config then dying atreadFrame.getMemorySyncStatusdegrades instead of failing (P2) — a transientp2p_peersread failure now logs and reportspeerCount=0rather than failing the whole status IPC.sync_interval_msis ceiling-checked for direct transport (P2) — a value above the 5-minute reconnect ceiling now fails config resolution instead of aborting daemon startup inP2pManager::create.forgetoperation (P2) — addedyams p2p forget <node-id>end-to-end (proto enumFORGET=9, dispatcher,ServiceManager::forgetP2pPeer,P2pManager::forget→PeerRegistry::removePeer). Fail-closed enrollment is preserved: re-enrolling with a different pin is still rejected; rotation requires explicit forget-then-enroll.RED evidence
formatConnect/formatDisconnectwould thrownlohmann::json::out_of_range/type_erroron a parseable-but-malformed response.sync_interval_ms = 300001passed config resolution and aborted daemon startup later.Acould not be rotated (enroll with pinbreturnedUnauthorizedand no removal path existed).Validation
Focused normal matrix (green, no regressions):
p2p_protocol495 / 14,p2p_delta301 / 10,p2p_manager180 / 12,peer_registry91 / 6service_manager789 / 47 passed + 1 skipdaemon_component_submodule1319 / 109 (config resolver + proto serializer)memory_sync838 / 60,memory_sync_service263 / 27New coverage: timeout ceiling, direct
sync_interval_msceiling, and an explicit key-rotation flow (enroll → fail-closed replace → forget → re-enroll rotated pin).Changed-file formatting, Windows-header, GPL, portability (574 reviewed / 0 new), and
git diff --checkpass. Audit OpenGrep reports 166 pre-existing findings (159 in generatedipc_protocol_requests.h, 5 inp2p_manager.cpp, 2 reviewed getenv reads re-allowlisted after a line shift) — none introduced by this change.TODO_PREPUSH
AI disclosure
Extensive AI assistance was used for implementation, test generation, and adversarial review. All changes were locally inspected and validated; the umbrella #68 remains blocked by the decision-grade live-performance gate.
Stack
fix/p2p-wire-hardening/ fix(p2p): harden direct-P2P framing and ingress admission #75 at24923e95835bf31633ff9c54b46fe2a5c0b1e55cfix/p2p-robustnessatcd956acdbee34e6c560af1a47fdfc1057ad7fa50Do not merge or deploy this child independently.