Skip to content

fix(p2p): harden CLI status and operator/key-rotation flows - #76

Merged
trvon merged 1 commit into
fix/p2p-wire-hardeningfrom
fix/p2p-robustness
Aug 30, 2026
Merged

fix(p2p): harden CLI status and operator/key-rotation flows#76
trvon merged 1 commit into
fix/p2p-wire-hardeningfrom
fix/p2p-robustness

Conversation

@trvon

@trvon trvon commented Aug 30, 2026

Copy link
Copy Markdown
Owner

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.

  1. p2p status text is machine-parseable again (P1) — removed ANSI severity colorization from the key=value stream; every field is now a plain value.
  2. CLI connect/disconnect/forget stop throwing on malformed daemon responses (P1) — replaced .at("peer_node_id")/.at("node_id") with .value(key, default) and widened the deltas_sent/deltas_received defaults to std::uint64_t (no size_tunsigned int truncation).
  3. Handshake/delta timeout options are bounded to kP2pMaxOperationTimeout (P2)validateConfig/validateOptions now reject a timeout above 1 minute up front instead of passing config then dying at readFrame.
  4. getMemorySyncStatus degrades instead of failing (P2) — a transient p2p_peers read failure now logs and reports peerCount=0 rather than failing the whole status IPC.
  5. sync_interval_ms is ceiling-checked for direct transport (P2) — a value above the 5-minute reconnect ceiling now fails config resolution instead of aborting daemon startup in P2pManager::create.
  6. Key rotation is possible via an explicit forget operation (P2) — added yams p2p forget <node-id> end-to-end (proto enum FORGET=9, dispatcher, ServiceManager::forgetP2pPeer, P2pManager::forgetPeerRegistry::removePeer). Fail-closed enrollment is preserved: re-enrolling with a different pin is still rejected; rotation requires explicit forget-then-enroll.

RED evidence

  • formatConnect/formatDisconnect would throw nlohmann::json::out_of_range/type_error on a parseable-but-malformed response.
  • sync_interval_ms = 300001 passed config resolution and aborted daemon startup later.
  • A peer enrolled with pin A could not be rotated (enroll with pin b returned Unauthorized and no removal path existed).

Validation

Focused normal matrix (green, no regressions):

  • p2p_protocol 495 / 14, p2p_delta 301 / 10, p2p_manager 180 / 12, peer_registry 91 / 6
  • service_manager 789 / 47 passed + 1 skip
  • daemon_component_submodule 1319 / 109 (config resolver + proto serializer)
  • memory_sync 838 / 60, memory_sync_service 263 / 27

New coverage: timeout ceiling, direct sync_interval_ms ceiling, 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 --check pass. Audit OpenGrep reports 166 pre-existing findings (159 in generated ipc_protocol_requests.h, 5 in p2p_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

Do not merge or deploy this child independently.

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>
@trvon
trvon marked this pull request as ready for review August 30, 2026 15:48
Copilot AI lite review requested due to automatic review settings August 30, 2026 15:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 status key/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_id is missing, this prints disconnected with 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_id is missing, this prints forgot with 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) {
Comment on lines +124 to +126
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})) + ")";
@trvon
trvon merged commit d012d95 into experimental Aug 30, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants