Skip to content

feat: add ITK integration for cross-SDK interoperability testing#917

Open
kabir wants to merge 2 commits into
a2aproject:mainfrom
kabir:itk
Open

feat: add ITK integration for cross-SDK interoperability testing#917
kabir wants to merge 2 commits into
a2aproject:mainfrom
kabir:itk

Conversation

@kabir
Copy link
Copy Markdown
Collaborator

@kabir kabir commented Jun 4, 2026

Add a new itk/ Maven module that builds a Quarkus-based ITK agent from the current SDK source. This agent processes protobuf-encoded instructions to forward messages to other agents via JSONRPC, gRPC, and REST transports, enabling cross-SDK interoperability testing with the A2A Integration Test Kit.

Includes:

  • ITK agent (AgentExecutorProducer, AgentCardProducer, AdditionalRoutes)
  • PR and nightly test scenarios (scenarios.json, scenarios_full.json)
  • Runner script (run_itk.sh) for Docker-based ITK execution
  • GitHub Actions workflows for PR validation and nightly runs
  • BOM verifier exclusions for the non-library ITK module

Add a new `itk/` Maven module that builds a Quarkus-based ITK agent from
the current SDK source. This agent processes protobuf-encoded instructions
to forward messages to other agents via JSONRPC, gRPC, and REST transports,
enabling cross-SDK interoperability testing with the A2A Integration Test Kit.

Includes:
- ITK agent (AgentExecutorProducer, AgentCardProducer, AdditionalRoutes)
- PR and nightly test scenarios (scenarios.json, scenarios_full.json)
- Runner script (run_itk.sh) for Docker-based ITK execution
- GitHub Actions workflows for PR validation and nightly runs
- BOM verifier exclusions for the non-library ITK module

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the Integration Test Kit (ITK) agent module (itk) to support cross-SDK interoperability testing, including runner scripts, test scenarios, and agent execution logic. Several critical issues were identified in the review: reading the request body in an early Vert.x handler will prevent subsequent handlers from processing POST requests; attempting to Base64-decode raw protobuf bytes will cause exceptions; catching InterruptedException silently during task holds leaves the emitter in an incomplete state; and a potential NullPointerException exists when handling ExecutionException causes. Additionally, the cleanup process in run_itk.sh should be made safer, and a bug in the inline Python script's exception handler needs to be fixed to avoid masking JSON parsing errors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +30 to +44
router.route().order(Integer.MIN_VALUE)
.handler(ctx -> {
String method = ctx.request().method().name();
String path = ctx.request().path();
String contentType = ctx.request().getHeader("Content-Type");
log.info("Incoming {} {} Content-Type={}", method, path, contentType);
if ("POST".equals(method)) {
ctx.request().body().onSuccess(buffer -> {
if (buffer != null) {
log.debug("POST {} payload: {}", path, buffer.toString());
}
});
}
ctx.next();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

Calling ctx.request().body() in an early handler (with order(Integer.MIN_VALUE)) initiates reading of the request body stream. In Vert.x, once the request stream is consumed, subsequent handlers (such as JAX-RS/RESTEasy or other route handlers) will not be able to read it again, causing all incoming POST requests (JSONRPC, REST) to fail or receive empty bodies. To prevent this, avoid reading the body stream in this early logging handler.

        router.route().order(Integer.MIN_VALUE)
                .handler(ctx -> {
                    String method = ctx.request().method().name();
                    String path = ctx.request().path();
                    String contentType = ctx.request().getHeader("Content-Type");
                    log.info("Incoming {} {} Content-Type={}", method, path, contentType);
                    ctx.next();
                });

Comment on lines +145 to +148
if (file instanceof FileWithBytes fwb) {
byte[] raw = Base64.getDecoder().decode(fwb.bytes());
return Instruction.parseFrom(raw);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

fwb.bytes() returns the raw binary payload of the protobuf file. Attempting to decode it as Base64 will throw an IllegalArgumentException because raw protobuf bytes are not Base64-encoded. You should parse the instruction directly from the raw bytes.

                            if (file instanceof FileWithBytes fwb) {
                                return Instruction.parseFrom(fwb.bytes());
                            }

Comment on lines +93 to +99
try {
Thread.sleep(HOLD_INTERVAL_MS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.info("Task {} interrupted during hold", emitter.getTaskId());
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Catching InterruptedException inside the loop and returning silently leaves the emitter in the TASK_STATE_WORKING state indefinitely, as neither complete() nor fail() is called. Since the outer try-catch block already catches InterruptedException and correctly fails the emitter, you can simplify this by letting Thread.sleep throw the exception directly.

                        Thread.sleep(HOLD_INTERVAL_MS);

Comment on lines +117 to +120
} catch (ExecutionException e) {
log.error("Remote agent call failed for task {}: {}",
emitter.getTaskId(), e.getCause().getMessage(), e.getCause());
emitter.fail();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

e.getCause() can potentially be null for an ExecutionException. Calling e.getCause().getMessage() without a null check will throw a NullPointerException, masking the original failure. Use defensive programming to handle a null cause safely.

            } catch (ExecutionException e) {
                Throwable cause = e.getCause();
                String errorMsg = cause != null ? cause.getMessage() : e.getMessage();
                log.error("Remote agent call failed for task {}: {}",
                        emitter.getTaskId(), errorMsg, cause);
                emitter.fail();

Comment thread itk/run_itk.sh
Comment on lines +1 to +19
#!/bin/bash
set -ex

# Set default log level
export ITK_LOG_LEVEL="${ITK_LOG_LEVEL:-INFO}"

# Initialize default exit code
RESULT=1

# Cleanup function to be called on exit
cleanup() {
set +x
echo "Cleaning up artifacts..."
docker stop itk-service > /dev/null 2>&1 || true
docker rm itk-service > /dev/null 2>&1 || true
docker rmi itk_service > /dev/null 2>&1 || true
rm -rf a2a-itk > /dev/null 2>&1 || true
echo "Done. Final exit code: $RESULT"
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

If the script fails while inside the a2a-itk directory, the cleanup trap will execute rm -rf a2a-itk from within that directory. Deleting the current working directory can fail or cause shell issues on some systems. It is safer to save the original directory at the start and cd back to it during cleanup.

#!/bin/bash
set -ex

# Save original directory to ensure cleanup runs from a safe location
ORIGINAL_DIR=$(pwd)

# Set default log level
export ITK_LOG_LEVEL="${ITK_LOG_LEVEL:-INFO}"

# Initialize default exit code
RESULT=1

# Cleanup function to be called on exit
cleanup() {
  set +x
  echo "Cleaning up artifacts..."
  cd "$ORIGINAL_DIR" > /dev/null 2>&1 || true
  docker stop itk-service > /dev/null 2>&1 || true
  docker rm itk-service > /dev/null 2>&1 || true
  docker rmi itk_service > /dev/null 2>&1 || true
  rm -rf a2a-itk > /dev/null 2>&1 || true
  echo "Done. Final exit code: $RESULT"
}

Comment thread itk/run_itk.sh
Comment on lines +110 to +127
echo "$RESPONSE" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
all_passed = data.get('all_passed', False)
results = data.get('results', {})
for test, passed in results.items():
status = 'PASSED' if passed else 'FAILED'
print(f'{test}: {status}')
print('--------------------------------------------------------')
print(f'OVERALL STATUS: {\"PASSED\" if all_passed else \"FAILED\"}')
if not all_passed:
sys.exit(1)
except Exception as e:
print(f'Error parsing results: {e}')
print(f'Raw response: {data if \"data\" in locals() else \"no data\"}')
sys.exit(1)
"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

If json.load(sys.stdin) fails due to an invalid JSON response, the except block tries to print data, which is undefined, resulting in a secondary exception and hiding the actual raw response. Reading the raw stdin first allows you to print the exact invalid response for easier debugging.

Suggested change
echo "$RESPONSE" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
all_passed = data.get('all_passed', False)
results = data.get('results', {})
for test, passed in results.items():
status = 'PASSED' if passed else 'FAILED'
print(f'{test}: {status}')
print('--------------------------------------------------------')
print(f'OVERALL STATUS: {\"PASSED\" if all_passed else \"FAILED\"}')
if not all_passed:
sys.exit(1)
except Exception as e:
print(f'Error parsing results: {e}')
print(f'Raw response: {data if \"data\" in locals() else \"no data\"}')
sys.exit(1)
"
echo "$RESPONSE" | python3 -c "
import sys, json
raw = sys.stdin.read()
try:
data = json.loads(raw)
all_passed = data.get('all_passed', False)
results = data.get('results', {})
for test, passed in results.items():
status = 'PASSED' if passed else 'FAILED'
print(f'{test}: {status}')
print('--------------------------------------------------------')
print(f'OVERALL STATUS: {\\\"PASSED\\\" if all_passed else \\\"FAILED\\\"}')
if not all_passed:
sys.exit(1)
except Exception as e:
print(f'Error parsing results: {e}')
print(f'Raw response: {raw}')
sys.exit(1)
"

- Add Main.java that launches quarkus-run.jar as a subprocess,
  bridging the exec:java entry point that current.py expects
- Remove reference-bom import from itk/pom.xml — parent POM already
  manages all SDK artifact versions, and the BOM isn't installed to
  .m2 by `mvn -pl itk -am install`, breaking standalone resolution
- Support podman as alternative container runtime in run_itk.sh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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