Skip to content

fix(handoff): validate recipient against agent registry, deny with suggestion - #428

Merged
getappz merged 9 commits into
masterfrom
task/52
Aug 10, 2026
Merged

fix(handoff): validate recipient against agent registry, deny with suggestion#428
getappz merged 9 commits into
masterfrom
task/52

Conversation

@getappz

@getappz getappz commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Auto-opened on item done for OHqzZViG_1FS_6WHOy9xK.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handoff validation by rejecting unknown recipients.
    • Error messages now identify the valid recipient options when a handoff target is not recognized.

handoff_impl only special-cased "github" -- any other recipient
string, including a typo or thematic word, fell through and became
assignee_agent with no validation, silently orphaning the item since
nothing could ever claim or dispatch it (reproduced live: a handoff
to "gastown" did exactly this).

Reuses agent_registry::agent_by_name, the same lookup cli/work.rs's
--agent flag already validates against, and rejects with the list of
valid recipients instead of silently creating dead work.

Agentflare-Agent: claude-code
Agentflare-Branch: task/52
Agentflare-Item: 52
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 276a7f66-f8de-43b1-9a97-8d2b101db260

📥 Commits

Reviewing files that changed from the base of the PR and between fa113b3 and 75df02f.

📒 Files selected for processing (1)
  • src/mcp_server/handoff.rs
📝 Walkthrough

Walkthrough

The handoff implementation now validates recipients after GitHub handling. Unknown recipients produce invalid-parameter errors that list registered agents and github. A test verifies rejection and valid-recipient reporting.

Changes

Handoff recipient validation

Layer / File(s) Summary
Validate and test handoff recipients
src/mcp_server/handoff.rs
The handoff flow rejects unregistered recipients and reports valid entries, including registered agents and github. The test verifies the invalid recipient and valid-recipient list.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description contains only an automation note and omits the required summary, test plan, and reviewer notes. Add the required Summary, Test plan, and Notes for reviewers sections with change rationale, test status, risks, and backwards-compatibility details.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes recipient validation against the agent registry and the resulting denial behavior.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/52

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mcp_server/handoff.rs`:
- Around line 89-101: Update the recipient validation in the handoff flow to
retain the agent returned by agent_registry::agent_by_name(&recipient) and use
its canonical id for assignee_agent, metadata, response, and dispatch-related
fields. Preserve the existing invalid-recipient error and special handling for
"github", while ensuring aliases are never persisted or returned.
- Around line 691-695: Update the test around handoff_impl to assert that the
returned err has code ErrorCode::INVALID_PARAMS, while preserving the existing
message-content assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b3579b23-8d66-4c90-ac09-16b8cf839919

📥 Commits

Reviewing files that changed from the base of the PR and between 5a2fcab and fa113b3.

📒 Files selected for processing (1)
  • src/mcp_server/handoff.rs

Comment thread src/mcp_server/handoff.rs
Comment on lines +89 to +101
if agent_registry::agent_by_name(&recipient).is_none() {
let mut valid: Vec<&str> =
agent_registry::REGISTRY.iter().map(|s| s.id.as_str()).collect();
valid.push("github");
valid.sort_unstable();
return Err(ErrorData::invalid_params(
format!(
"recipient '{recipient}' is not a known agent -- valid recipients: {}",
valid.join(", ")
),
None,
));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(handoff\.rs|agent_registry|.*registry.*)\b' || true
printf '%s\n' '--- symbols and recipient references ---'
rg -n -C 4 'agent_by_name|recipient|reuse|assignee|dispatch' src/mcp_server/handoff.rs src 2>/dev/null | head -n 500
printf '%s\n' '--- handoff outline ---'
if command -v ast-grep >/dev/null 2>&1; then
  ast-grep outline src/mcp_server/handoff.rs
else
  echo 'ast-grep unavailable'
fi

Repository: getappz/agentflare

Length of output: 38565


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- handoff implementation ---'
sed -n '1,270p' src/mcp_server/handoff.rs
printf '%s\n' '--- agent registry implementation ---'
cat -n crates/agent-registry/src/lib.rs
printf '%s\n' '--- registry and router implementations ---'
cat -n crates/agent-registry/src/registry.rs
cat -n crates/agent-registry/src/router.rs
printf '%s\n' '--- canonicalize and agent_by_name call sites ---'
rg -n -C 8 'canonicalize|agent_by_name|assignee_agent|list_by_assignee_agent|dispatch_item' src crates --glob '*.rs' | head -n 1000

Repository: getappz/agentflare

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- remaining handoff response path ---'
sed -n '240,360p' src/mcp_server/handoff.rs
printf '%s\n' '--- exact assignee lookup implementations ---'
rg -n -l 'fn list_by_assignee_agent|list_by_assignee_agent' crates src --glob '*.rs' | while read -r f; do
  echo "--- $f"
  rg -n -C 18 'fn list_by_assignee_agent|list_by_assignee_agent' "$f"
done
printf '%s\n' '--- exact dispatch and claim assignee resolution ---'
rg -n -C 14 'assignee_agent|resolve_confirmed_agent|dispatch_item|current_owner' src crates --glob '*.rs' \
  | rg -B 14 -A 14 'assignee_agent|resolve_confirmed_agent|dispatch_item|current_owner' \
  | head -n 1200
printf '%s\n' '--- handoff tests around recipient validation and reuse ---'
sed -n '680,940p' src/mcp_server/handoff.rs

Repository: getappz/agentflare

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- supervisor resolution ---'
rg -n -C 25 'fn resolve_confirmed_agent|resolve_confirmed_agent\(' src/supervisor.rs src/supervisor --glob '*.rs' 2>/dev/null || true
printf '%s\n' '--- supervisor assignee filters and dispatch entry points ---'
rg -n -C 12 'assignee_agent|dispatch_item|list_by_label|READY_LABEL|run_discovery_tick' src/supervisor.rs src/supervisor --glob '*.rs' 2>/dev/null | head -n 800
printf '%s\n' '--- claim matching functions ---'
rg -n -C 20 'fn (agent_of|claim|current_owner)|agent_of\(|owner.*assignee|assignee.*owner' src crates --glob '*.rs' | head -n 800
printf '%s\n' '--- canonicalization data-flow verifier ---'
python3 - <<'PY'
from pathlib import Path
registry = Path("crates/agent-registry/src/registry.rs").read_text()
handoff = Path("src/mcp_server/handoff.rs").read_text()
backend = Path("crates/agentflare-backend/src/item.rs").read_text()
assert '"claude" | "claude code" | "claude-code-cli"' in registry
assert 'agent_registry::agent_by_name(&recipient).is_none()' in handoff
assert 'let canonical_recipient = agent_registry::canonicalize(&recipient);' in handoff
assert 'assignee_agent: Some(recipient.clone())' in handoff
assert 'i.assignee_agent = ?2' in backend
print("alias accepted: claude -> claude-code")
print("stored recipient expression: original recipient")
print("reuse lookup expression: canonical_recipient")
print("backend comparison: exact equality")
print("result: an existing item stored as claude-code is not reusable after handoff recipient claude")
PY

Repository: getappz/agentflare

Length of output: 50374


Persist the canonical agent identifier.

agent_by_name(&recipient) accepts aliases such as "claude" but the returned agent is discarded. The code stores the alias in assignee_agent, metadata, and the response, while reuse searches for the canonical "claude-code" value. This creates duplicate items. Autonomous dispatch also rejects the alias. Use the resolved agent identifier for all persisted and returned recipient fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp_server/handoff.rs` around lines 89 - 101, Update the recipient
validation in the handoff flow to retain the agent returned by
agent_registry::agent_by_name(&recipient) and use its canonical id for
assignee_agent, metadata, response, and dispatch-related fields. Preserve the
existing invalid-recipient error and special handling for "github", while
ensuring aliases are never persisted or returned.

Comment thread src/mcp_server/handoff.rs
Comment on lines +691 to +695
let err = mcp.handoff_impl(req).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("gastown"), "{msg}");
assert!(msg.contains("claude-code"), "{msg}");
assert!(msg.contains("github"), "{msg}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- relevant file locations ---'
git ls-files | rg '(^|/)(handoff\.rs|Cargo\.toml|Cargo\.lock)$|mcp_server|agent_registry'
printf '%s\n' '--- handoff.rs outline and size ---'
wc -l src/mcp_server/handoff.rs
if command -v ast-grep >/dev/null 2>&1; then
  ast-grep outline src/mcp_server/handoff.rs | sed -n '1,220p'
else
  echo 'ast-grep unavailable'
fi
printf '%s\n' '--- handoff validation and test context ---'
sed -n '60,120p' src/mcp_server/handoff.rs
sed -n '620,720p' src/mcp_server/handoff.rs
printf '%s\n' '--- relevant symbols and rmcp APIs ---'
rg -n -C 3 'handoff_impl|agent_by_name|invalid.?param|ErrorCode|test database|sqlite|persist|items' src Cargo.toml Cargo.lock 2>/dev/null | sed -n '1,320p'

Repository: getappz/agentflare

Length of output: 26300


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- dependency versions ---'
rg -n -C 4 '^rmcp|rmcp =' Cargo.toml Cargo.lock | sed -n '1,100p'
printf '%s\n' '--- handoff implementation ---'
sed -n '1,220p' src/mcp_server/handoff.rs
sed -n '220,430p' src/mcp_server/handoff.rs
printf '%s\n' '--- test helpers and relevant tests ---'
sed -n '540,710p' src/mcp_server/handoff.rs
printf '%s\n' '--- backend item APIs and test DB access ---'
rg -n -C 5 'pub (fn|struct)|fn (get|list|create)|backend_db_override|test_mcp|with_backend_db' crates src/mcp_server.rs src/mcp_server/tests | rg 'item|test_mcp|backend_db|with_backend_db|src/mcp_server/handoff.rs' -n | sed -n '1,260p'
printf '%s\n' '--- current diff summary and relevant diff ---'
git diff --stat
git diff -- src/mcp_server/handoff.rs | sed -n '1,260p'

Repository: getappz/agentflare

Length of output: 50374


🌐 Web query:

rmcp 1.8.0 ErrorData ErrorCode invalid_params code Rust API

💡 Result:

In the rmcp crate (the official Rust SDK for the Model Context Protocol), ErrorData is a structure used to represent JSON-RPC error responses [1][2]. The invalid_params code corresponds to the standard JSON-RPC error code -32602, which indicates an invalid method parameter [2][3]. You can trigger this error using the constructor method ErrorData::invalid_params(message, data) [1][2]. When implementing an MCP server, it is important to distinguish between protocol errors and tool-level errors [4][5]: 1. Protocol Errors (Use ErrorData): These are returned as Err(ErrorData) and should be used when the server cannot route or process the request at all (e.g., malformed input, missing capabilities, or internal infrastructure errors) [4][5]. Clients typically render these errors opaquely, meaning the end-user will likely not see the specific message you provide [4][5]. 2. Tool-Level Errors (Use CallToolResult::error): These should be returned as Ok(CallToolResult::error(...)) when the request is valid and successfully routed to your tool, but the tool fails to produce a result for reasons relevant to the user (e.g., a database query returns no rows, or an external API returns a 500 status) [4][5]. This approach ensures your custom message is passed to and rendered by the client for the user [4][5]. Example usage for an invalid parameter check: if args.query.is_empty { return Err(ErrorData::invalid_params("query must be non-empty", None)); } [4][5] Additionally, note that for protocol versions 2026-07-28 and newer, some errors that were previously classified as RESOURCE_NOT_FOUND (-32002) are now promoted to INVALID_PARAMS (-32602) to align with updated specifications [1][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
import urllib.request
urls = [
    "https://docs.rs/crate/rmcp/1.8.0/source/src/model.rs",
    "https://raw.githubusercontent.com/modelcontextprotocol/rust-sdk/rmcp-v1.8.0/crates/rmcp/src/model.rs",
]
for url in urls:
    print(f"--- {url} ---")
    try:
        text = urllib.request.urlopen(url, timeout=10).read().decode()
    except Exception as exc:
        print(type(exc).__name__, exc)
        continue
    lines = text.splitlines()
    for i, line in enumerate(lines):
        if "struct ErrorData" in line or "enum ErrorCode" in line or "invalid_params" in line:
            start = max(0, i - 8)
            end = min(len(lines), i + 35)
            print("\n".join(f"{n+1}:{lines[n]}" for n in range(start, end)))
            print()
PY

Repository: getappz/agentflare

Length of output: 614


🏁 Script executed:

#!/bin/bash
set -e
for url in \
  'https://docs.rs/crate/rmcp/1.8.0/source/src/model.rs' \
  'https://raw.githubusercontent.com/modelcontextprotocol/rust-sdk/rmcp-v1.8.0/crates/rmcp/src/model.rs'
do
  echo "--- $url ---"
  curl -k -L --fail --silent "$url" |
    rg -n -C 12 'struct ErrorData|enum ErrorCode|fn invalid_params|INVALID_PARAMS|InvalidParams' |
    sed -n '1,180p' || true
done

Repository: getappz/agentflare

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -e
curl -k -L --fail --silent \
  'https://docs.rs/crate/rmcp/1.8.0/source/src/model.rs' |
python3 -c '
import sys, re, html
s = html.unescape(sys.stdin.read())
s = re.sub(r"<[^>]+>", "", s)
s = s.replace("\r", "")
lines = s.splitlines()
for i, line in enumerate(lines):
    if "pub struct ErrorData" in line:
        print("\n".join(f"{n+1}:{lines[n]}" for n in range(max(0,i-5), min(len(lines),i+18))))
        break
'

Repository: getappz/agentflare

Length of output: 1280


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
text = Path("src/mcp_server/handoff.rs").read_text()
start = text.index("pub fn handoff_impl(")
end = text.index("\n    /// Publishes", start)
body = text[start:end]
invalid = body.index("if agent_registry::agent_by_name")
branch_end = body.index("\n        let ext =", invalid)
prefix = body[:branch_end]
print("invalid_recipient_validation_before_db_access:",
      "ErrorData::invalid_params" in prefix and "with_backend_db" not in prefix)
print("first_with_backend_db_offset:", body.index("with_backend_db"))
print("invalid_recipient_offset:", invalid)
test_start = text.index("fn recipient_not_in_the_agent_registry_is_rejected")
test_end = text.index("\n    #[test]", test_start)
test = text[test_start:test_end]
print("test_asserts_error_code:", ".code" in test or "ErrorCode" in test)
print("test_asserts_message_fragments:",
      all(f'msg.contains("{x}")' in test for x in ("gastown", "claude-code", "github")))
PY

Repository: getappz/agentflare

Length of output: 342


Assert the protocol error code.

The message checks do not prove that handoff_impl returns ErrorCode::INVALID_PARAMS. Assert err.code == ErrorCode::INVALID_PARAMS; validation already occurs before any database write.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp_server/handoff.rs` around lines 691 - 695, Update the test around
handoff_impl to assert that the returned err has code ErrorCode::INVALID_PARAMS,
while preserving the existing message-content assertions.

@getappz getappz changed the title handoff: validate recipient against agent registry, deny with suggestion fix(handoff): validate recipient against agent registry, deny with suggestion Aug 10, 2026
shiva added 3 commits August 10, 2026 14:52
Agentflare-Agent: claude-code
Agentflare-Branch: task/52
Agentflare-Item: 52
Agentflare-Agent: claude-code
Agentflare-Branch: task/52
Agentflare-Item: 52
Agentflare-Agent: claude-code
Agentflare-Branch: task/52
Agentflare-Item: 52
@getappz
getappz enabled auto-merge (squash) August 10, 2026 09:49
@getappz
getappz merged commit 10a013f into master Aug 10, 2026
16 checks passed
@getappz
getappz deleted the task/52 branch August 10, 2026 11:21
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.

1 participant