Skip to content

Phase 0: A2A bugs + configurable agent identity - #47

Merged
rohitg00 merged 5 commits into
mainfrom
phase0/bugs
Apr 26, 2026
Merged

Phase 0: A2A bugs + configurable agent identity#47
rohitg00 merged 5 commits into
mainfrom
phase0/bugs

Conversation

@rohitg00

@rohitg00 rohitg00 commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Tracks #45 — Phase 0 of MCP+A2A overhaul.

Summary

Three non-breaking A2A fixes plus configurable agent identity:

  • serviceEndpoint /a2a suffix. Agent card was advertising bare base_url; spec-compliant clients hit root and 404'd. Now advertises <base_url>/a2a with trailing-slash normalisation.
  • documentation_url configurable. Old hardcoded value pointed at the scrapped iii-connect repo. Now read from --docs-url flag, default https://github.com/iii-hq/workers/tree/main/a2a.
  • Agent identity flags. New --agent-name, --agent-description, --provider-org, --provider-url, --docs-url. Defaults preserve current behavior except for the dead docs URL.
  • Single-source DEFAULT_* consts shared between AgentIdentity::default() and clap defaults.
  • [lib] target added so integration tests can exercise build_agent_card directly.

Test plan

  • cargo check -p iii-a2a clean
  • cargo test -p iii-a2a green (3/3)
  • Manual: cargo run --release -p iii-a2a -- --agent-name "acme-ops"curl /.well-known/agent-card.json | jq '.supportedInterfaces[0].url' ends with /a2a

Sequencing

Land first — every other phase branch carries the dead docs URL and missing /a2a suffix and needs to inherit this fix.

Summary by CodeRabbit

  • New Features

    • Crate is now usable as a library for external integration and testing; agent-card functionality is exposed for reuse.
    • CLI accepts customizable agent card metadata (name, description, provider org, provider URL, documentation URL) with sensible defaults.
    • Automatic base-URL normalization prevents double-slash interface paths.
  • Tests

    • Added tests validating agent card creation for default and custom identities, URL normalization, and empty skills handling.

…docs URL

Three non-breaking A2A v0.3 fixes plus a configurable agent identity
surface for the agent card:

- supportedInterfaces[].url now appends '/a2a' (the JSON-RPC mount in
  register()), matching the spec — clients that POST to the advertised
  endpoint no longer 404 against the bare base URL.
- documentationUrl threaded through a new --docs-url flag, defaulting to
  the workers repo a2a folder. The previous hardcoded link pointed at
  iii-hq/iii-connect, which was scrapped.
- Identity (name, description, provider org/url) is now driven by
  AgentIdentity, populated from new --agent-name, --agent-description,
  --provider-org, --provider-url flags. Defaults preserve the existing
  hardcoded values byte-for-byte, so no in-flight client breaks.

Crate gains a thin lib.rs (re-exports handler + types) so integration
tests under a2a/tests/ can drive build_agent_card directly. Three tests
cover default identity (a2a suffix + docs URL), trailing-slash
normalisation, and full custom identity flow-through.
- Single-source DEFAULT_* consts shared between AgentIdentity::default()
  and clap default_value attributes
@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a library target and public entry for the a2a crate, introduces a public AgentIdentity and DEFAULT_* constants, updates register and build_agent_card signatures to accept identity and normalize base URLs, updates CLI to pass identity, and adds tests for agent-card generation and URL handling.

Changes

Cohort / File(s) Summary
Library manifest & entry
a2a/Cargo.toml, a2a/src/lib.rs
Bumped crate version and added a [lib] target; added src/lib.rs exporting pub mod handler and pub mod types.
Handler & types
a2a/src/handler.rs
Added pub struct AgentIdentity and DEFAULT_* constants; changed register(...) to accept AgentIdentity; made build_agent_card public with signature (..., base_url: &str, identity: &AgentIdentity) -> AgentCard; normalized base_url (trim trailing slash) and set supported interface URL to "{base}/a2a"; conditional inclusion of provider and documentation_url; removed leading slashes from configured api_paths.
Binary / CLI
a2a/src/main.rs
CLI now accepts identity fields (name, description, provider org/url, docs URL) defaulting to library constants; constructs handler::AgentIdentity and passes it to register; imports switched to iii_a2a::handler.
Tests
a2a/tests/agent_card.rs
Added tests for default agent-card shape, /a2a URL normalization with/without trailing slash, propagation of custom AgentIdentity, and empty skills when list_functions() errors.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through crates and trimmed a slash,
Gave names and docs a gentle stash,
I built a card, set /a2a's gate,
Tests blinked green — my hop was great,
A little crate, a rabbit's small dash.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Phase 0: A2A bugs + configurable agent identity' clearly summarizes the main changes: fixing A2A bugs and adding configurable agent identity via new CLI flags.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 phase0/bugs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
a2a/src/handler.rs (1)

208-224: ⚠️ Potential issue | 🔴 Critical

Remove leading slashes from both api_path values; they will cause 404s.

The codebase explicitly documents this in coding/src/templates.rs (lines 725–728): iii-engine prepends / during path matching, so api_paths with leading slashes become double-slash routes (//a2a, //.well-known/agent-card.json) and return 404 at invoke time.

Change lines 211 and 220 to:

  • "api_path": ".well-known/agent-card.json"
  • "api_path": "a2a"

The agent card advertises /a2a (line 261); clients will hit that path and get a 404 if these registrations remain unchanged.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@a2a/src/handler.rs` around lines 208 - 224, The registered http trigger
api_path values include leading slashes causing double-slash routes and 404s;
update the two RegisterTriggerInput calls (the iii.register_trigger calls that
register "a2a::agent_card" and "a2a::jsonrpc") to remove the leading "/" from
the config.api_path entries so they become ".well-known/agent-card.json" and
"a2a" respectively, keeping the rest of the RegisterTriggerInput fields
unchanged.
🧹 Nitpick comments (3)
a2a/src/handler.rs (2)

260-264: Trailing-slash normalization — small edge case.

base_url.trim_end_matches('/') correctly handles http://host:port/ and even pathological cases like http://host:port///. Two adjacent edge cases worth being aware of (not necessarily fixing now):

  1. If an operator passes --base-url http://host:port/a2a (already includes the path), the advertised URL becomes http://host:port/a2a/a2a. Unlikely but silent.
  2. Whitespace isn't trimmed — "http://host:port/ " would advertise http://host:port/ /a2a. clap won't trim either.

A tiny let base = base_url.trim().trim_end_matches('/'); would cover both cheaply. Pure polish.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@a2a/src/handler.rs` around lines 260 - 264, The advertised A2A URL can end up
with extra slashes or embedded whitespace because base_url is only trimmed of
trailing slashes; change the construction used when building
supported_interfaces/AgentInterface so you first normalize the input (e.g., let
base = base_url.trim().trim_end_matches('/')) and then use base in the format!
call instead of base_url; update the code that sets supported_interfaces (the
AgentInterface url formatting) to reference this normalized base variable to
avoid doubled paths and stray spaces.

256-269: Provider is unconditionally Some(...) — consider making it omittable.

provider is always emitted, even if both provider_org and provider_url are empty strings. The A2A spec treats provider as optional, so a downstream client parsing the card may see {"organization": "", "url": ""} and treat the agent as having a (broken) provider rather than no provider. Two low-effort options:

♻️ Either omit when both fields are empty…
-        provider: Some(AgentProvider {
-            organization: identity.provider_org.clone(),
-            url: identity.provider_url.clone(),
-        }),
+        provider: if identity.provider_org.is_empty() && identity.provider_url.is_empty() {
+            None
+        } else {
+            Some(AgentProvider {
+                organization: identity.provider_org.clone(),
+                url: identity.provider_url.clone(),
+            })
+        },

…or model AgentIdentity::provider_org / provider_url as Option<String> end-to-end and let clap's Option<String> handling do the work. The latter is cleaner but a wider change.

Not a blocker for Phase 0 — defaults are non-empty so the wire shape is unchanged today.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@a2a/src/handler.rs` around lines 256 - 269, The AgentCard currently always
sets provider to Some(AgentProvider{...}) even when identity.provider_org and
identity.provider_url are empty; change the construction to set provider to None
when both identity.provider_org and identity.provider_url are empty, otherwise
set Some(AgentProvider { organization: identity.provider_org.clone(), url:
identity.provider_url.clone() }); update the AgentCard creation site (where
AgentCard and AgentProvider are constructed) to use this conditional so the
provider field is omitted on the wire when both values are empty.
a2a/src/main.rs (1)

95-102: Identity wiring is straightforward — LGTM.

CLI args flow into AgentIdentity and are passed to handler::register. Defaults preserved via the shared DEFAULT_* consts, which keeps clap defaults and AgentIdentity::default() in sync.

One optional thought: there's no validation that the URL flags (--provider-url, --docs-url) are well-formed URLs, and empty strings are silently accepted. If an operator typos --agent-name "", the card advertises an empty name. Probably fine for Phase 0, but worth a follow-up if you want defensive trimming/non-empty checks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@a2a/src/main.rs` around lines 95 - 102, The CLI currently wires args directly
into handler::AgentIdentity and calls handler::register without validating URL
or non-empty fields; add simple defensive validation in main before constructing
AgentIdentity: trim args.agent_name/agent_description and ensure required fields
(e.g., args.agent_name) are non-empty (return an error or exit with a clear
message), and validate args.provider_url and args.docs_url by attempting to
parse them as URLs (e.g., using url::Url::parse) and reject/notify on invalid
values; alternatively make those fields Option<String> in AgentIdentity and only
set them when non-empty/valid so handler::register receives sanitized inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@a2a/src/handler.rs`:
- Around line 208-224: The registered http trigger api_path values include
leading slashes causing double-slash routes and 404s; update the two
RegisterTriggerInput calls (the iii.register_trigger calls that register
"a2a::agent_card" and "a2a::jsonrpc") to remove the leading "/" from the
config.api_path entries so they become ".well-known/agent-card.json" and "a2a"
respectively, keeping the rest of the RegisterTriggerInput fields unchanged.

---

Nitpick comments:
In `@a2a/src/handler.rs`:
- Around line 260-264: The advertised A2A URL can end up with extra slashes or
embedded whitespace because base_url is only trimmed of trailing slashes; change
the construction used when building supported_interfaces/AgentInterface so you
first normalize the input (e.g., let base =
base_url.trim().trim_end_matches('/')) and then use base in the format! call
instead of base_url; update the code that sets supported_interfaces (the
AgentInterface url formatting) to reference this normalized base variable to
avoid doubled paths and stray spaces.
- Around line 256-269: The AgentCard currently always sets provider to
Some(AgentProvider{...}) even when identity.provider_org and
identity.provider_url are empty; change the construction to set provider to None
when both identity.provider_org and identity.provider_url are empty, otherwise
set Some(AgentProvider { organization: identity.provider_org.clone(), url:
identity.provider_url.clone() }); update the AgentCard creation site (where
AgentCard and AgentProvider are constructed) to use this conditional so the
provider field is omitted on the wire when both values are empty.

In `@a2a/src/main.rs`:
- Around line 95-102: The CLI currently wires args directly into
handler::AgentIdentity and calls handler::register without validating URL or
non-empty fields; add simple defensive validation in main before constructing
AgentIdentity: trim args.agent_name/agent_description and ensure required fields
(e.g., args.agent_name) are non-empty (return an error or exit with a clear
message), and validate args.provider_url and args.docs_url by attempting to
parse them as URLs (e.g., using url::Url::parse) and reject/notify on invalid
values; alternatively make those fields Option<String> in AgentIdentity and only
set them when non-empty/valid so handler::register receives sanitized inputs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e40cb982-8e66-4101-9a9f-3cd7b4601d17

📥 Commits

Reviewing files that changed from the base of the PR and between 004ff2f and 191ae71.

⛔ Files ignored due to path filters (1)
  • a2a/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • a2a/Cargo.toml
  • a2a/src/handler.rs
  • a2a/src/lib.rs
  • a2a/src/main.rs
  • a2a/tests/agent_card.rs

…tional provider/docs URL

- api_path: '.well-known/agent-card.json' / 'a2a' (no leading slash; engine
  prepends '/' so leading slash produces double-slash 404)
- base_url normalised via .trim().trim_end_matches('/')
- AgentCard provider/documentation_url omitted when fields empty (spec-optional)

@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: 1

🧹 Nitpick comments (1)
a2a/tests/agent_card.rs (1)

22-50: Optional: tighten default-identity coverage.

The default-identity test asserts name, provider, and docs URL but skips card.description. Since DEFAULT_AGENT_DESCRIPTION is part of the same single-source-of-truth in handler.rs (line 70-71) and is what users will see in clients, asserting it here closes the loop on accidental drift.

Also, the PR objective lists whitespace trimming on base_url as part of the normalization, but only trailing-slash is exercised in trailing_slash_in_base_url_is_normalised (line 53). A one-line case (e.g., " http://localhost:3111 ") would lock that behavior in too.

♻️ Proposed additions
     assert_eq!(card.name, "iii-engine");
+    assert_eq!(
+        card.description,
+        "iii-engine agent — invoke any registered function via A2A"
+    );
     let provider = card
         .provider
         .expect("default identity always has a provider");
 #[tokio::test]
 async fn trailing_slash_in_base_url_is_normalised() {
     let iii = unreachable_iii();
     let cfg = ExposureConfig::new(false, None);
     let identity = AgentIdentity::default();

     let card = build_agent_card(&iii, &cfg, "http://localhost:3111/", &identity).await;

     assert_eq!(
         card.supported_interfaces[0].url, "http://localhost:3111/a2a",
         "trailing slash on base_url must not produce a doubled `//a2a`"
     );
+
+    let card_ws =
+        build_agent_card(&iii, &cfg, "  http://localhost:3111/  ", &identity).await;
+    assert_eq!(
+        card_ws.supported_interfaces[0].url, "http://localhost:3111/a2a",
+        "surrounding whitespace on base_url must be trimmed before /a2a is appended"
+    );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@a2a/tests/agent_card.rs` around lines 22 - 50, Add assertions to the test
default_identity_advertises_a2a_suffix_and_docs_url to verify card.description
equals the canonical DEFAULT_AGENT_DESCRIPTION from handler.rs (so changes to
AgentIdentity::default()/handler.rs are caught), and add one small sub-case
calling build_agent_card with a base_url containing surrounding whitespace
(e.g., "  http://localhost:3111  ") to assert supported_interfaces[0].url still
normalizes to "http://localhost:3111/a2a"; update references to
AgentIdentity::default(), build_agent_card, and DEFAULT_AGENT_DESCRIPTION to
locate the checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@a2a/src/handler.rs`:
- Around line 252-259: The current provider construction uses a conjunction so
provider becomes None only when both identity.provider_org and
identity.provider_url are empty; change the conditional to use logical OR so
provider is set to None if either identity.provider_org.is_empty() or
identity.provider_url.is_empty(), and otherwise construct Some(AgentProvider {
organization: ..., url: ... }); update the conditional around the provider
variable generation in handler.rs to mirror the documentation_url pattern and
ensure AgentProvider never contains a half-empty field.

---

Nitpick comments:
In `@a2a/tests/agent_card.rs`:
- Around line 22-50: Add assertions to the test
default_identity_advertises_a2a_suffix_and_docs_url to verify card.description
equals the canonical DEFAULT_AGENT_DESCRIPTION from handler.rs (so changes to
AgentIdentity::default()/handler.rs are caught), and add one small sub-case
calling build_agent_card with a base_url containing surrounding whitespace
(e.g., "  http://localhost:3111  ") to assert supported_interfaces[0].url still
normalizes to "http://localhost:3111/a2a"; update references to
AgentIdentity::default(), build_agent_card, and DEFAULT_AGENT_DESCRIPTION to
locate the checks.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b54299bf-97ad-4176-ac5a-82ed6fc98371

📥 Commits

Reviewing files that changed from the base of the PR and between 191ae71 and 6fb246c.

⛔ Files ignored due to path filters (1)
  • a2a/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • a2a/Cargo.toml
  • a2a/src/handler.rs
  • a2a/tests/agent_card.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • a2a/Cargo.toml

Comment thread a2a/src/handler.rs Outdated
…ompliance)

Both AgentProvider.organization and AgentProvider.url are required by the
A2A v0.3 spec. Half-populated provider violates the spec; switch && to ||
so we omit the field entirely if either is missing.

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

🧹 Nitpick comments (1)
a2a/src/handler.rs (1)

127-131: Drop the unnecessary identity.clone() on line 131.

identity is an owned AgentIdentity and isn't referenced again after this binding, so the clone is wasted work — you can move it straight into card_identity. The closure still needs its own ident = card_identity.clone() since it's a Fn-style handler invoked repeatedly.

♻️ Proposed tweak
-pub fn register(iii: &III, exposure: ExposureConfig, base_url: String, identity: AgentIdentity) {
+pub fn register(iii: &III, exposure: ExposureConfig, base_url: String, identity: AgentIdentity) {
     let iii_card = iii.clone();
     let card_cfg = exposure.clone();
     let card_base_url = base_url.clone();
-    let card_identity = identity.clone();
+    let card_identity = identity;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@a2a/src/handler.rs` around lines 127 - 131, In register, you're cloning
`identity` unnecessarily into `card_identity`: since `identity` is owned and not
used afterward, move it directly into `card_identity` (i.e., take ownership
instead of calling `identity.clone()`), and keep the existing `ident =
card_identity.clone()` inside the Fn-style handler closure so the closure can
clone per invocation; update the binding of `card_identity` in the `register`
function to use ownership transfer rather than cloning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@a2a/src/handler.rs`:
- Around line 127-131: In register, you're cloning `identity` unnecessarily into
`card_identity`: since `identity` is owned and not used afterward, move it
directly into `card_identity` (i.e., take ownership instead of calling
`identity.clone()`), and keep the existing `ident = card_identity.clone()`
inside the Fn-style handler closure so the closure can clone per invocation;
update the binding of `card_identity` in the `register` function to use
ownership transfer rather than cloning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9808cd1f-fed0-4e98-af58-6c15b90327e7

📥 Commits

Reviewing files that changed from the base of the PR and between 6fb246c and 8903f54.

📒 Files selected for processing (1)
  • a2a/src/handler.rs

@rohitg00
rohitg00 merged commit 6db5e56 into main Apr 26, 2026
7 checks passed
@rohitg00
rohitg00 deleted the phase0/bugs branch April 26, 2026 12:01
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