diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10c9e9d..c165ef9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,8 +46,19 @@ jobs: - name: Run tests run: npm test - - name: Run integration tests - run: npm run test:integration + - name: Run test coverage + run: npm run test:coverage + + - name: Check coverage threshold + run: | + # Extract coverage percentage from report and fail if below threshold + COVERAGE=$(npm run test:coverage 2>&1 | grep "All files" | awk '{print $4}' | sed 's/%//') + echo "Coverage: $COVERAGE%" + if (( $(echo "$COVERAGE < 65" | bc -l) )); then + echo "❌ Coverage $COVERAGE% is below minimum threshold of 65%" + exit 1 + fi + echo "✅ Coverage $COVERAGE% meets minimum threshold of 65%" build: runs-on: ubuntu-latest @@ -68,70 +79,6 @@ jobs: - name: Build CLI run: npm run build - - - name: Test CLI basic functionality (Node.js 20.x) - run: | - # Test basic CLI functionality - node dist/cli.js --version - node dist/cli.js --help - - - name: Test CLI with Node.js 18.x - uses: actions/setup-node@v4 - with: - node-version: '18.x' - cache: 'npm' - - - name: Test CLI basic functionality (Node.js 18.x) - run: | - # Test basic CLI functionality on Node.js 18.x - node dist/cli.js --version - node dist/cli.js --help - - test-binary-build: - runs-on: ubuntu-latest - needs: test - # Only test binary building on main/master pushes and PRs - if: github.event_name == 'pull_request' || (github.event_name == 'push' && contains(github.ref, 'refs/heads/main')) - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '18.x' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Test binary build process - run: | - npm run build - - # Test bundling - npx esbuild src/cli.ts \ - --bundle \ - --platform=node \ - --target=node18 \ - --outfile=dist/cli-bundled.js \ - --banner:js="#!/usr/bin/env node" - - # Test bundled CLI - node dist/cli-bundled.js --version - node dist/cli-bundled.js --help - - # Test binary creation (Linux only for CI speed) - mkdir -p dist/binaries - npx pkg dist/cli-bundled.js \ - --targets node18-linux-x64 \ - --output dist/binaries/capiscio-linux-x64 - - # Test created binary - chmod +x dist/binaries/capiscio-linux-x64 - ./dist/binaries/capiscio-linux-x64 --version - ./dist/binaries/capiscio-linux-x64 --help security: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e367f45..9e2b21d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,9 +41,6 @@ jobs: - name: Run tests run: npm test - - name: Run integration tests - run: npm run test:integration - - name: Build CLI run: npm run build diff --git a/.gitignore b/.gitignore index f507c5d..3c8afdb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,10 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* +# Binaries +bin/capiscio-core +bin/capiscio-core.exe + # Build outputs dist/ build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a51bba..0cae1a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.2.0] - 2025-12-10 + +### Changed +- **VERSION ALIGNMENT**: All CapiscIO packages now share the same version number. + - `capiscio-core`, `capiscio` (npm), and `capiscio` (PyPI) are all v2.2.0. + - Simplifies compatibility - no version matrix needed. +- **CORE VERSION**: Now downloads `capiscio-core` v2.2.0. + +## [2.1.3] - 2025-12-10 + +### Changed +- **CLI-ONLY ARCHITECTURE**: Package is now a pure CLI wrapper matching capiscio-python. + - Passthrough architecture - ALL commands delegated to capiscio-core binary. + - Removed programmatic API (validators, scorers, types). + - For programmatic usage, use `@capiscio/sdk` (coming soon). + +### Added +- **`--wrapper-version`**: Display the version of the Node.js wrapper package. +- **`--wrapper-clean`**: Remove the cached capiscio-core binary (forces re-download on next run). + +### Removed +- **BREAKING**: Removed `CoreValidator`, `validateAgentCard()` exports. +- **BREAKING**: Removed `A2AValidator`, `FetchHttpClient` exports. +- **BREAKING**: Removed `ValidateCommand`, `ConsoleOutput`, `JsonOutput` exports. +- **BREAKING**: Removed all TypeScript types exports. +- Removed unused dependencies: `commander`, `glob`, `inquirer`, `jose`. + +### Fixed +- All core commands (`badge`, `key`, `gateway`, `validate`) now work via passthrough. + ## [2.1.2] - 2025-11-20 ### Changed diff --git a/README.md b/README.md index 87259b9..745207b 100644 --- a/README.md +++ b/README.md @@ -1,285 +1,96 @@ -# CapiscIO CLI - A2A Protocol Validator +# CapiscIO CLI (Node.js) -> **Comprehensive validation for AI agent trust and protocol compliance** | Beyond schema validation - test cryptographic authenticity and live protocol functionality. +**The official command-line interface for CapiscIO, the Agent-to-Agent (A2A) validation platform.** -🌐 **[Learn more about CapiscIO](https://capisc.io)** | **[Download Page](https://capisc.io/downloads)** | **[Web Validator](https://capisc.io/validator)** - -[![npm version](https://badge.fury.io/js/capiscio-cli.svg)](https://badge.fury.io/js/capiscio-cli) -[![Downloads](https://img.shields.io/npm/dm/capiscio-cli)](https://www.npmjs.com/package/capiscio-cli) -[![CI](https://github.com/capiscio/capiscio-cli/workflows/CI/badge.svg)](https://github.com/capiscio/capiscio-cli/actions) -[![Coverage](https://img.shields.io/badge/coverage-70.2%25-green.svg)](https://github.com/capiscio/capiscio-cli) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/) -[![Security](https://img.shields.io/badge/security-audited-brightgreen.svg)](https://github.com/capiscio/capiscio-cli/security) -[![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/) +[![npm version](https://badge.fury.io/js/capiscio.svg)](https://badge.fury.io/js/capiscio) +[![Node.js](https://img.shields.io/badge/node-%3E%3D16.0.0-brightgreen.svg)](https://nodejs.org/) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) +[![Downloads](https://img.shields.io/npm/dm/capiscio)](https://www.npmjs.com/package/capiscio) -## Common A2A Integration Challenges - -**Agent cards can pass schema validation but fail in production due to real-world integration issues.** - -### What Often Goes Wrong: -- **🔌 Endpoint connectivity** - declared URLs return 404 or timeout -- **⚠️ Protocol implementation gaps** - JSONRPC/GRPC errors in production -- **🔒 Missing cryptographic signatures** - no way to verify agent authenticity -- **📋 Specification compliance** - subtle A2A protocol violations -- **❌ Schema vs reality** - valid JSON but broken functionality - -### How CapiscIO Helps: -- **🔒 JWS signature verification** - cryptographically verify agent authenticity -- **🌐 Live endpoint testing** - catch broken protocols before deployment -- **⚡ Zero-dependency validation** - no npm vulnerabilities or Python conflicts -- **🛡️ Comprehensive validation** - trust AND functionality in one command - ---- - -## Quick Start - -**💡 Prefer a web interface?** Try our [online validator at capisc.io](https://capisc.io/validator) - no installation required! - -### Option 1: Install via NPM (Requires Node.js) -```bash -# Install globally -npm install -g capiscio-cli - -# Validate your agent -capiscio validate ./agent-card.json - -# Test live endpoints -capiscio validate https://your-agent.com - -# Strict validation for production -capiscio validate ./agent-card.json --strict --json -``` - -### Option 2: Standalone Binary (Advanced) +## Overview -If you don't want to use Node.js or Python, you can download the standalone Go binary directly from the **capiscio-core** repository. This is the engine that powers the CLI. +This package provides a convenient Node.js distribution for the **CapiscIO CLI**. It acts as a smart wrapper that automatically manages the underlying `capiscio-core` binary (written in Go), ensuring you always have the correct executable for your operating system and architecture. -**[Download latest binaries from capiscio-core releases](https://github.com/capiscio/capiscio-core/releases)** +> **Note:** This is a wrapper. The core logic resides in [capiscio-core](https://github.com/capiscio/capiscio-core). -| Platform | Architecture | Binary Name | -|----------|-------------|-------------| -| **Linux** | x64 | `capiscio-linux-amd64` | -| **macOS** | Intel | `capiscio-darwin-amd64` | -| **macOS** | Apple Silicon | `capiscio-darwin-arm64` | -| **Windows** | Intel x64 | `capiscio-windows-amd64.exe` | +## Installation -#### Quick Download Example (Linux): ```bash -# Download the binary -curl -L -o capiscio https://github.com/capiscio/capiscio-core/releases/download/v1.0.2/capiscio-linux-amd64 - -# Make executable -chmod +x capiscio - -# Run -./capiscio validate ./agent-card.json +npm install -g capiscio ``` -## Key Features - -- **🔐 Two-Layer Validation** - ONLY CapiscIO validates both cryptographic trust AND protocol compliance -- **✅ JWS Signature Verification** - Cryptographic validation of agent authenticity (RFC 7515 compliant) -- **🚀 Live Protocol Testing** - Actually tests JSONRPC, GRPC, and REST endpoints (not just schemas) -- **⚡ High Performance** - Powered by a native Go binary for blazing fast validation -- **🛡️ Secure by Default** - Signature verification enabled automatically -- **🔧 CI/CD Ready** - JSON output with proper exit codes for automated pipelines -- **🌐 Smart Discovery** - Finds agent cards automatically with multiple fallbacks -- **💻 Cross-Platform** - npm or standalone binaries - ## Usage -### Basic Commands - -```bash -capiscio validate [input] [options] - -# Examples -capiscio validate # Auto-detect in current directory -capiscio validate ./agent-card.json # Validate local file (with signatures) -capiscio validate https://agent.com # Test live agent (with signatures) -capiscio validate ./agent-card.json --skip-signature # Skip signature verification -capiscio validate ./agent-card.json --verbose # Detailed output -capiscio validate ./agent-card.json --registry-ready # Check registry readiness -capiscio validate https://agent.com --errors-only # Show only problems -capiscio validate ./agent-card.json --show-version # Version analysis -``` - -### Key Options - -| Option | Description | -|--------|-------------| -| --strict | Strict A2A protocol compliance | -| --json | JSON output for CI/CD | -| --verbose | Detailed validation steps | -| --timeout | Request timeout (default: 10000) | -| --schema-only | Skip live endpoint testing | -| --skip-signature | Skip JWS signature verification | -| --test-live | Test agent endpoint with real messages | - -### Live Agent Testing - -The `--test-live` flag tests your agent endpoint with real A2A protocol messages: +Once installed, the `capiscio` command is available in your terminal. It passes all arguments directly to the core binary. ```bash -# Test agent endpoint -capiscio validate https://agent.com --test-live - -# Test with custom timeout -capiscio validate ./agent-card.json --test-live --timeout 5000 - -# Full validation for production -capiscio validate https://agent.com --test-live --strict --json -``` - -**What it validates:** -- ✅ Endpoint connectivity -- ✅ JSONRPC and HTTP+JSON transport protocols -- ✅ A2A message structure (Message, Task, StatusUpdate, ArtifactUpdate) -- ✅ Response timing metrics - -**Exit codes for automation:** -- `0` = Success -- `1` = Schema validation failed -- `2` = Network error (timeout, connection refused, DNS) -- `3` = Protocol violation (invalid A2A response) - -**Use cases:** -- CI/CD post-deployment verification -- Cron-based health monitoring -- Pre-production testing -- Third-party agent evaluation -- Multi-environment validation - -### Validation Modes +# Validate an agent card +capiscio validate ./agent-card.json -- **Progressive** (default): Balanced validation with warnings for compatibility issues -- **Strict**: Full compliance required, warnings become errors, registry-ready validation +# Validate with JSON output +capiscio validate https://my-agent.example.com --json -**Registry Ready:** Use `--registry-ready` for strict validation optimized for agent registry deployment. +# Issue a self-signed badge (development) +capiscio badge issue --self-sign -### Three-Dimensional Scoring +# Verify a badge +capiscio badge verify "eyJhbGciOiJFZERTQSJ9..." --accept-self-signed -CapiscIO CLI automatically provides detailed quality scoring across three independent dimensions: +# Check version +capiscio --version -```bash -# Scoring is shown by default -capiscio validate agent.json +# Get help +capiscio --help ``` -**Three Quality Dimensions:** -- **Spec Compliance (0-100)** - How well does the agent conform to A2A v0.3.0? -- **Trust (0-100)** - How trustworthy and secure is this agent? (includes confidence multiplier) -- **Availability (0-100)** - Is the endpoint operational? (requires `--test-live`) - -Each score includes a detailed breakdown showing exactly what contributed to the result. - -> **Note:** Legacy single-score output has been replaced by this multi-dimensional system in v2.0.0. - -## Why Use CapiscIO CLI? +### Wrapper Utilities -**Stop Integration Disasters Before They Happen:** +The Node.js wrapper includes specific commands to manage the binary: -### 🚨 What Breaks When You Don't Validate -- **Compromised agents inject malicious responses** - unsigned cards can't be trusted -- **JSONRPC methods return wrong error codes** - protocol violations cause failures -- **GRPC services are unreachable or misconfigured** - integration breaks silently -- **REST endpoints don't match declared capabilities** - runtime mismatches -- **Tampered agent cards** - man-in-the-middle attacks succeed -- **Production failures cascade** - one bad agent brings down your system +| Command | Description | +|---------|-------------| +| `capiscio --wrapper-version` | Display the version of this Node.js wrapper package. | +| `capiscio --wrapper-clean` | Remove the cached `capiscio-core` binary (forces re-download on next run). | -### ✅ CapiscIO Prevents These Failures -- **JWS signature verification** - cryptographically prove agent authenticity -- **Live endpoint connectivity testing** - catch broken protocols before deployment -- **A2A protocol compliance validation** - prevent specification violations -- **HTTPS-only JWKS security** - tamper-proof key distribution -- **Real connectivity validation** - beyond schema to actual functionality +## How It Works -**The only CLI that validates both cryptographic trust AND protocol compliance.** +1. **Detection**: When you run `capiscio`, the script detects your OS (Linux, macOS, Windows) and Architecture (AMD64, ARM64). +2. **Provisioning**: It checks if the correct `capiscio-core` binary is present in the cache. + - *Linux/macOS*: `~/.capiscio/bin` + - *Windows*: `%USERPROFILE%\.capiscio\bin` +3. **Download**: If missing, it securely downloads the release from GitHub. +4. **Execution**: It seamlessly delegates to the Go binary, passing all arguments through. -## Transport Protocol Testing & Security +## Supported Platforms -Unlike basic schema validators, CapiscIO CLI actually tests your agent endpoints and verifies cryptographic signatures: +- **macOS**: AMD64 (Intel), ARM64 (Apple Silicon) +- **Linux**: AMD64, ARM64 +- **Windows**: AMD64 -- **JSONRPC** - Validates JSON-RPC 2.0 compliance and connectivity -- **GRPC** - Tests gRPC endpoint accessibility -- **REST** - Verifies HTTP+JSON endpoint patterns -- **JWS Signatures** - Cryptographic verification of agent card authenticity (RFC 7515) -- **Consistency** - Ensures equivalent functionality across protocols +## Environment Variables -Perfect for testing your own agents and evaluating third-party agents before integration. +| Variable | Description | +|----------|-------------| +| `CAPISCIO_CORE_VERSION` | Override the default core binary version (e.g., `v1.0.2`) | +| `CAPISCIO_CORE_PATH` | Use a specific binary path instead of auto-downloading | -## Signature Verification (New in v1.2.0) +## Troubleshooting -CapiscIO CLI now includes **secure by default** JWS signature verification for agent cards: - -### 🔐 Cryptographic Validation -- **RFC 7515 compliant** JWS (JSON Web Signature) verification -- **JWKS (JSON Web Key Set)** fetching from trusted sources -- **Detached signature** support for agent card authentication -- **HTTPS-only** JWKS endpoints for security - -### 🛡️ Secure by Default +**"Permission denied" errors:** +Ensure your user has write access to the cache directory. You can reset the cache by running: ```bash -# Signature verification runs automatically -capiscio validate ./agent-card.json - -# Opt-out when signatures aren't needed -capiscio validate ./agent-card.json --skip-signature -``` - -### ✅ Benefits -- **Authenticity** - Verify agent cards haven't been tampered with -- **Trust** - Cryptographically confirm the publisher's identity -- **Security** - Prevent malicious agent card injection -- **Compliance** - Meet security requirements for production deployments - -Signature verification adds minimal overhead while providing crucial security guarantees for agent ecosystems. - -## CI/CD Integration - -### Using NPM Package: -```yaml -# GitHub Actions -- name: Validate Agent - run: | - npm install -g capiscio-cli - capiscio validate ./agent-card.json --json --strict -``` - -### Using Standalone Binary (Core): -```yaml -# GitHub Actions - No Node.js required -- name: Download and Validate Agent - run: | - # Download Core Binary (Linux AMD64) - curl -L -o capiscio https://github.com/capiscio/capiscio-core/releases/download/v1.0.2/capiscio-linux-amd64 - chmod +x capiscio - ./capiscio validate ./agent-card.json --json --strict +capiscio --wrapper-clean ``` -Exit codes: 0 = success, 1 = validation failed - -## FAQ - -**Q: What is the A2A Protocol?** -A: The Agent-to-Agent (A2A) protocol v0.3.0 is a standardized specification for AI agent discovery, communication, and interoperability. [Learn more at capisc.io](https://capisc.io). - -**Q: How is this different from schema validators?** -A: We actually test live JSONRPC, GRPC, and REST endpoints with transport protocol validation, not just JSON schema structure. We also verify JWS signatures for cryptographic authenticity. +**"Binary not found" or download errors:** +If you are behind a corporate firewall, ensure you can access `github.com`. -**Q: Can I validate LLM agent cards?** -A: Yes! Perfect for AI/LLM developers validating agent configurations and testing third-party agents before integration. +## Related Packages -**Q: What file formats are supported?** -A: Current spec uses `agent-card.json`. We also support legacy `agent.json` files and auto-discover from `/.well-known/agent-card.json` endpoints. +- **[capiscio](https://pypi.org/project/capiscio/)** - Python CLI wrapper (identical functionality) +- **[capiscio-sdk-python](https://pypi.org/project/capiscio-sdk/)** - Python SDK for programmatic usage +- **[capiscio-core](https://github.com/capiscio/capiscio-core)** - Go core engine ## License -Apache-2.0 © [CapiscIO](https://capisc.io) - ---- - -**Need help?** [Visit capisc.io](https://capisc.io) | [Open an issue](https://github.com/capiscio/capiscio-cli/issues) | [Documentation](https://capisc.io/cli) | [Web Validator](https://capisc.io/validator) - -**Keywords**: A2A protocol, AI agent validation, agent-card.json validator, agent.json validator, agent-to-agent protocol, LLM agent cards, AI agent discovery, agent configuration validation, transport protocol testing, JSONRPC validation, GRPC testing, REST endpoint validation, agent protocol CLI, AI agent compliance, JWS signature verification, agent card authentication +Apache-2.0 diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index b207311..b9230d8 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,38 +1,80 @@ -# CapiscIO Node.js CLI +--- +title: Installation +description: How to install the CapiscIO Node.js CLI +--- -The **CapiscIO Node.js CLI** is a lightweight wrapper that automatically downloads and executes the high-performance [CapiscIO Core](https://github.com/capiscio/capiscio-core) binary. +# Installation -It provides a seamless experience for JavaScript/TypeScript developers, allowing you to install the CLI via `npm` or `pnpm` without worrying about platform-specific binaries. +## Requirements + +- **Node.js** 16.0.0 or later +- **npm** or **yarn** or **pnpm** -## Installation +## Install via npm (Recommended) ```bash +# Global installation (recommended for CLI usage) npm install -g capiscio -# or + +# Verify installation +capiscio --version +``` + +## Install via yarn + +```bash +yarn global add capiscio +``` + +## Install via pnpm + +```bash pnpm add -g capiscio ``` -## Usage +## First Run -Once installed, the `capiscio` command is available in your terminal. It passes all arguments directly to the underlying Core binary. +On first run, the CLI will automatically download the `capiscio-core` binary for your platform: ```bash -# Validate an agent card -capiscio validate ./agent-card.json +$ capiscio --version +✔ Installed CapiscIO Core v1.0.2 +capiscio version 1.0.2 +``` -# Check version -capiscio version +The binary is cached in: + +- **macOS/Linux**: `~/.capiscio/bin/` +- **Windows**: `%USERPROFILE%\.capiscio\bin\` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `CAPISCIO_CORE_VERSION` | Override the default core binary version (e.g., `v1.0.3`) | +| `CAPISCIO_CORE_PATH` | Use a specific binary path instead of auto-downloading | + +## Updating + +To update to the latest version: + +```bash +npm update -g capiscio ``` -For full command reference, see the [CapiscIO Core Documentation](../../capiscio-core/reference/cli.md). +To force re-download of the core binary: -## How it Works +```bash +capiscio --wrapper-clean +capiscio --version # Downloads fresh binary +``` -1. **Detection**: When you run `capiscio`, the wrapper detects your Operating System (Linux, macOS, Windows) and Architecture (AMD64, ARM64). -2. **Download**: It checks if the correct `capiscio-core` binary is present in your global `node_modules` or user cache. If not, it downloads it securely from GitHub Releases. -3. **Execution**: It executes the binary with your provided arguments, piping input and output directly to your terminal. +## Uninstalling -## Requirements +```bash +# Remove the npm package +npm uninstall -g capiscio -* Node.js 18+ -* Internet connection (for initial binary download) +# Optionally remove cached binary +rm -rf ~/.capiscio +``` diff --git a/docs/guides/programmatic-usage.md b/docs/guides/programmatic-usage.md deleted file mode 100644 index 319225b..0000000 --- a/docs/guides/programmatic-usage.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -title: Programmatic Usage -description: How to use the capiscio CLI programmatically in Node.js applications. ---- - -# Programmatic Usage - -For Node.js applications that need validation results, spawn the CLI with `--json` output. - ---- - -## Basic Example - -```typescript -import { spawnSync } from 'child_process'; - -interface ValidationResult { - success: boolean; - score: number; - errors: Array<{ code: string; message: string }>; - warnings: Array<{ code: string; message: string }>; -} - -function validate(path: string): ValidationResult { - const result = spawnSync('npx', ['capiscio', 'validate', path, '--json'], { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'pipe'] - }); - - // CLI may exit with code 1 on validation failure but still outputs valid JSON - const output = result.stdout || ''; - if (!output) { - throw new Error(`Validation failed: ${result.stderr}`); - } - return JSON.parse(output); -} - -// Usage -const result = validate('./agent-card.json'); -console.log(`Valid: ${result.success}, Score: ${result.score}`); -``` - ---- - -## Async Version - -```typescript -import { spawn } from 'child_process'; - -function validateAsync(path: string): Promise { - return new Promise((resolve, reject) => { - const child = spawn('npx', ['capiscio', 'validate', path, '--json']); - let stdout = ''; - let stderr = ''; - - child.stdout.on('data', (data) => { stdout += data; }); - child.stderr.on('data', (data) => { stderr += data; }); - - child.on('close', (code) => { - // CLI may exit with code 1 on validation failure but still outputs valid JSON - if (stdout) { - resolve(JSON.parse(stdout)); - } else { - reject(new Error(stderr || 'Validation failed')); - } - }); - }); -} -``` - ---- - -## Batch Validation - -```typescript -import { glob } from 'glob'; - -async function validateAll(pattern: string) { - const files = await glob(pattern); - - const results = await Promise.all( - files.map(async file => ({ - file, - result: await validateAsync(file) - })) - ); - - const passed = results.filter(r => r.result.success); - const failed = results.filter(r => !r.result.success); - - console.log(`✅ Passed: ${passed.length}`); - console.log(`❌ Failed: ${failed.length}`); - - return results; -} - -// Usage -await validateAll('./agents/**/*.json'); -``` - ---- - -## Express Middleware - -```typescript -import express from 'express'; -import { spawnSync } from 'child_process'; -import { writeFileSync, unlinkSync } from 'fs'; -import { randomUUID } from 'crypto'; - -const app = express(); -app.use(express.json()); - -app.post('/api/validate', (req, res) => { - // Write to temp file - const tempFile = `/tmp/agent-${randomUUID()}.json`; - writeFileSync(tempFile, JSON.stringify(req.body)); - - try { - const result = spawnSync('npx', ['capiscio', 'validate', tempFile, '--json'], { - encoding: 'utf8' - }); - - const output = result.stdout || ''; - if (output) { - res.json(JSON.parse(output)); - } else { - res.status(500).json({ error: result.stderr || 'Validation failed' }); - } - } finally { - unlinkSync(tempFile); - } -}); -``` - ---- - -## Why Not a Native API? - -The `capiscio` npm package is a **distribution wrapper** for the Go-based validation engine. This approach: - -- ✅ Ensures consistent validation across all platforms -- ✅ Leverages the high-performance Go implementation -- ✅ Keeps the npm package lightweight -- ✅ Single source of truth for validation logic - -For native TypeScript integration, the spawning patterns shown above provide full access to all CLI features with proper error handling. diff --git a/docs/guides/scoring.md b/docs/guides/scoring.md deleted file mode 100644 index 0a9f9b8..0000000 --- a/docs/guides/scoring.md +++ /dev/null @@ -1,304 +0,0 @@ -# Using Scoring in the CLI - -> **Learn how to use the three-dimensional scoring system with CapiscIO CLI** - -## Quick Overview - -CapiscIO CLI uses a three-dimensional scoring system to evaluate agent cards: - -- **📄 Compliance (0-100)** - Protocol adherence and format validation (`complianceScore`) -- **🔐 Trust (0-100)** - Security practices and cryptographic verification (`trustScore`) -- **🚀 Availability (0-100)** - Operational readiness *(only with `--test-live`)* - ---- - -## Basic Usage - -### Viewing Scores - -Scores are included in `--json` output: - -```bash -# Validate and get JSON output with scores -capiscio validate agent-card.json --json - -# Add live endpoint testing for availability scores -capiscio validate https://agent.example.com --json --test-live -``` - -### Example Text Output - -``` -✅ A2A AGENT VALIDATION PASSED -Score: 100/100 -Version: 0.3.0 -Perfect! Your agent passes all validations -``` - -### Example Output with Warnings - -``` -✅ A2A AGENT VALIDATION PASSED -Score: 85/100 -Version: 0.3.0 -Agent passed with warnings - -ERRORS FOUND: -⚠️ [MISSING_DOCS] warning: No documentation URL provided -⚠️ [UNSIGNED] warning: Agent card is not cryptographically signed -``` - ---- - -## JSON Output Format - -When using `--json`, the output includes the full scoring result: - -```json -{ - "success": true, - "score": 100, - "version": "0.3.0", - "errors": [], - "warnings": [], - "scoringResult": { - "success": true, - "complianceScore": 100, - "trustScore": 85, - "availability": { - "score": 0, - "tested": false - }, - "issues": [], - "signatures": null - }, - "liveTest": null -} -``` - -### With Live Testing (`--test-live`) - -```json -{ - "success": true, - "score": 100, - "version": "0.3.0", - "errors": [], - "warnings": [], - "scoringResult": { - "success": true, - "complianceScore": 100, - "trustScore": 85, - "availability": { - "score": 95, - "tested": true, - "endpointUrl": "https://agent.example.com/.well-known/agent.json", - "latencyMs": 142 - }, - "issues": [] - }, - "liveTest": { - "success": true, - "endpoint": "https://agent.example.com/.well-known/agent.json", - "responseTime": 142, - "errors": [] - } -} -``` - -### Parsing JSON in Scripts - -```bash -# Extract compliance score -capiscio validate agent.json --json | jq '.scoringResult.complianceScore' - -# Extract trust score -capiscio validate agent.json --json | jq '.scoringResult.trustScore' - -# Check if production-ready (compliance >= 95, trust >= 60) -RESULT=$(capiscio validate agent.json --json) -COMPLIANCE=$(echo "$RESULT" | jq '.scoringResult.complianceScore') -TRUST=$(echo "$RESULT" | jq '.scoringResult.trustScore') - -if (( $(echo "$COMPLIANCE >= 95" | bc -l) )) && (( $(echo "$TRUST >= 60" | bc -l) )); then - echo "✅ Production ready" -else - echo "⚠️ Not production ready" -fi - -# Get all validation issues -capiscio validate agent.json --json | jq '.scoringResult.issues[]' -``` - ---- - -## CI/CD Integration - -### GitHub Actions Example - -```yaml -name: Validate Agent Card - -on: [push, pull_request] - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install CapiscIO CLI - run: npm install -g capiscio - - - name: Validate with scoring - run: | - capiscio validate agent-card.json --json > results.json - - # Extract scores - COMPLIANCE=$(jq '.scoringResult.complianceScore' results.json) - TRUST=$(jq '.scoringResult.trustScore' results.json) - - echo "📊 Compliance: $COMPLIANCE/100" - echo "🔐 Trust: $TRUST/100" - - # Fail if below thresholds - if (( $(echo "$COMPLIANCE < 95" | bc -l) )) || (( $(echo "$TRUST < 60" | bc -l) )); then - echo "❌ Failed: Scores below production thresholds" - exit 1 - fi - - echo "✅ Passed: Production-ready scores" -``` - -### GitLab CI Example - -```yaml -validate-agent: - stage: test - image: node:20 - script: - - npm install -g capiscio - - capiscio validate agent-card.json --json > results.json - - COMPLIANCE=$(jq '.scoringResult.complianceScore' results.json) - - TRUST=$(jq '.scoringResult.trustScore' results.json) - - | - if (( $(echo "$COMPLIANCE < 95" | bc -l) )) || (( $(echo "$TRUST < 60" | bc -l) )); then - echo "❌ Scores below thresholds" - exit 1 - fi -``` - ---- - -## Command Combinations - -### Validate Multiple Files - -```bash -# Validate all agent cards -for file in agents/*.json; do - echo "Validating $file..." - capiscio validate "$file" -done - -# Or use find -find agents/ -name "*.json" -exec capiscio validate {} \; -``` - -### Live Testing for Availability - -```bash -# Full validation with live endpoint testing -capiscio validate https://agent.example.com --test-live --json - -# Schema-only validation (no live test even if URL) -capiscio validate https://agent.example.com --schema-only -``` - -### Compare Agents - -```bash -# Compare two agents side-by-side -capiscio validate agent-a.json --json > a.json -capiscio validate agent-b.json --json > b.json - -# Extract key metrics -echo "Agent A - Compliance: $(jq '.scoringResult.complianceScore' a.json), Trust: $(jq '.scoringResult.trustScore' a.json)" -echo "Agent B - Compliance: $(jq '.scoringResult.complianceScore' b.json), Trust: $(jq '.scoringResult.trustScore' b.json)" -``` - ---- - -## Batch Validation Report - -```bash -#!/bin/bash -# generate-report.sh - Create CSV report of all agent scores - -echo "File,Success,Compliance,Trust,Issues" > report.csv - -for file in agents/*.json; do - RESULT=$(capiscio validate "$file" --json 2>/dev/null) - if [ $? -eq 0 ] || [ $? -eq 1 ]; then - SUCCESS=$(echo "$RESULT" | jq -r '.success') - COMPLIANCE=$(echo "$RESULT" | jq -r '.scoringResult.complianceScore') - TRUST=$(echo "$RESULT" | jq -r '.scoringResult.trustScore') - ISSUES=$(echo "$RESULT" | jq -r '.scoringResult.issues | length') - - echo "$file,$SUCCESS,$COMPLIANCE,$TRUST,$ISSUES" >> report.csv - fi -done - -echo "📊 Report generated: report.csv" -``` - ---- - -## Exit Codes - -The CLI uses exit codes to indicate validation results: - -| Exit Code | Meaning | -|-----------|---------| -| **0** | Validation passed (agent is valid) | -| **1** | Validation failed (agent has errors) | - -**Important:** The CLI exits with 0 even if scores are low, as long as the agent card is structurally valid. To enforce score thresholds, parse the JSON output: - -```bash -# Enforce minimum trust score -RESULT=$(capiscio validate agent.json --json) -TRUST=$(echo "$RESULT" | jq '.scoringResult.trustScore') - -if (( $(echo "$TRUST < 60" | bc -l) )); then - echo "❌ Trust score too low: $TRUST" - exit 1 -fi -``` - ---- - -## CLI Flags Reference - -| Flag | Description | -|------|-------------| -| `--json` | Output results as JSON (includes all scores) | -| `--test-live` | Test live agent endpoint for availability score | -| `--strict` | Enable strict validation mode | -| `--schema-only` | Validate schema only, skip endpoint testing | -| `--skip-signature` | Skip JWS signature verification | -| `--registry-ready` | Check registry deployment readiness | -| `--timeout` | Request timeout (default: 10s) | -| `--errors-only` | Show only errors and warnings | - ---- - -## See Also - -- [CLI Reference](../reference/cli.md) - Complete command-line reference and options -- [Programmatic Usage](./programmatic-usage.md) - Use the CLI from Node.js applications diff --git a/docs/index.md b/docs/index.md index ce56074..a174293 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,171 +1,65 @@ --- -title: CapiscIO npm Package -description: Install the capiscio CLI via npm. Validate A2A agent cards from the command line. +title: CapiscIO Node.js CLI - Documentation +description: Official documentation for the CapiscIO Node.js CLI wrapper. --- -# CapiscIO npm Package +# CapiscIO Node.js CLI -The `capiscio` npm package installs the CapiscIO CLI for validating A2A agent cards. +The **CapiscIO Node.js CLI** is a lightweight wrapper around the [CapiscIO Core](https://github.com/capiscio/capiscio-core) binary, designed for seamless integration into Node.js environments. -```bash -npm install -g capiscio -``` +!!! info "This is a Wrapper Package" + This package does NOT contain validation logic. It downloads and executes the `capiscio-core` Go binary, which performs the actual validation. ---- +
-## Quick Start +- **🚀 Getting Started** -```bash -# Validate a local file -capiscio validate ./agent-card.json + --- -# Validate a remote agent -capiscio validate https://your-agent.example.com + Install the CLI via npm. -# Strict mode with JSON output -capiscio validate ./agent-card.json --strict --json -``` + [:octicons-arrow-right-24: Installation](./getting-started/installation.md) ---- +- **⚙️ Reference** -## What This Package Does + --- -This npm package is a **distribution wrapper** for [capiscio-core](https://github.com/capiscio/capiscio-core), the Go-based validation engine. + Wrapper commands and usage. -``` -┌────────────────────────────────────┐ -│ npm install -g capiscio │ -└─────────────────┬──────────────────┘ - │ - ▼ -┌────────────────────────────────────┐ -│ capiscio-core (Go binary) │ -│ Downloaded automatically │ -└────────────────────────────────────┘ -``` + [:octicons-arrow-right-24: Commands](./reference/commands.md) -**Why a wrapper?** - -- ✅ Easy installation via npm -- ✅ No Go toolchain required -- ✅ Automatic binary management -- ✅ Cross-platform support - ---- +
-## Installation Options - -### Global Install (Recommended) +## Quick Start ```bash +# Install globally npm install -g capiscio -capiscio validate ./agent-card.json -``` - -### npx (No Install) - -```bash -npx capiscio validate ./agent-card.json -``` - -### Project Dependency - -```bash -npm install --save-dev capiscio -npx capiscio validate ./agent-card.json -``` - ---- -## CLI Reference - -### validate - -Validate an A2A agent card. - -```bash -capiscio validate [input] [options] -``` - -| Option | Description | -|--------|-------------| -| `--strict` | Strict validation mode | -| `--json` | JSON output (for CI/CD) | -| `--schema-only` | Skip network requests | -| `--skip-signature` | Skip JWS signature verification | -| `--test-live` | Test live agent endpoint | -| `--timeout ` | Request timeout (default: 10000) | -| `--verbose` | Detailed output | -| `--errors-only` | Show only errors | - -### Exit Codes - -| Code | Meaning | -|------|---------| -| `0` | Validation passed | -| `1` | Validation failed | - ---- - -## CI/CD Integration - -### GitHub Actions - -```yaml -- name: Validate Agent Card - run: npx capiscio validate ./agent-card.json --strict --json -``` - -### GitLab CI - -```yaml -validate: - script: - - npx capiscio validate ./agent-card.json --strict --json -``` - -!!! tip "Dedicated GitHub Action" - For richer CI integration, use [validate-a2a](https://github.com/capiscio/validate-a2a). - ---- - -## Programmatic Usage - -For Node.js applications that need validation results programmatically, spawn the CLI with JSON output: +# Validate an agent card +capiscio validate ./agent-card.json -```typescript -import { spawnSync } from 'child_process'; +# Validate with JSON output (includes scores) +capiscio validate ./agent-card.json --json -function validateAgentCard(path: string) { - const result = spawnSync('npx', ['capiscio', 'validate', path, '--json'], { - encoding: 'utf8' - }); - - // CLI may exit with code 1 on validation failure but still outputs valid JSON - const output = result.stdout || result.stderr; - return JSON.parse(output); -} +# Issue a self-signed badge (development) +capiscio badge issue --self-sign -const result = validateAgentCard('./agent-card.json'); -console.log(`Valid: ${result.success}, Score: ${result.score}`); +# Check core version +capiscio --version ``` ---- - -## Alternative Installation Methods - -If you prefer not to use npm: +## What This Package Does -| Method | Command | -|--------|---------| -| **pip** | `pip install capiscio` | -| **Binary** | [Download from GitHub](https://github.com/capiscio/capiscio-core/releases) | -| **Docker** | `docker pull ghcr.io/capiscio/capiscio-core` | +1. **Downloads** the correct `capiscio-core` binary for your platform (macOS/Linux/Windows, AMD64/ARM64) +2. **Caches** the binary in `~/.capiscio/bin` (or `%USERPROFILE%\.capiscio\bin` on Windows) +3. **Executes** the binary with your arguments via `execa` with inherited stdio ---- +All validation logic lives in `capiscio-core`. This wrapper just makes it easy to install via npm. -## See Also +## Wrapper-Specific Commands -- [CLI Reference](../reference/cli/index.md) - Complete command documentation -- [capiscio-core](https://github.com/capiscio/capiscio-core) - Underlying Go binary -- [validate-a2a](https://github.com/capiscio/validate-a2a) - GitHub Action +| Command | Description | +|---------|-------------| +| `capiscio --wrapper-version` | Display the wrapper package version | +| `capiscio --wrapper-clean` | Remove cached binary (forces re-download) | diff --git a/docs/reference/api.md b/docs/reference/api.md deleted file mode 100644 index 844dd3a..0000000 --- a/docs/reference/api.md +++ /dev/null @@ -1,411 +0,0 @@ ---- -title: TypeScript API Reference -description: Programmatic API documentation for the capiscio npm package. A2AValidator class and TypeScript types. ---- - -# TypeScript API Reference - -The `capiscio` package exports a pure TypeScript validation engine for programmatic use. - -!!! info "When to Use" - Use the TypeScript API when you need: - - - Runtime validation in Node.js applications - - Custom error handling and result processing - - Integration with Express, Fastify, or other frameworks - - Batch validation workflows - - For command-line usage, see the [CLI Reference](./cli.md). - ---- - -## Installation - -```bash -npm install capiscio -``` - ---- - -## Quick Start - -```typescript -import { A2AValidator } from 'capiscio'; - -const validator = new A2AValidator(); - -// Validate a local file -const result = await validator.validate('./agent-card.json'); - -// Validate a URL -const remoteResult = await validator.validate('https://agent.example.com'); - -// Check results -if (result.success) { - console.log(`Score: ${result.score}/100`); -} else { - result.errors.forEach(err => console.error(err.message)); -} -``` - ---- - -## A2AValidator - -The main validation class. - -### Constructor - -```typescript -constructor(httpClient?: HttpClient) -``` - -| Parameter | Type | Description | -|-----------|------|-------------| -| `httpClient` | `HttpClient` | Optional custom HTTP client for network requests | - -### Methods - -#### validate() - -Main validation method supporting files and URLs. - -```typescript -async validate( - input: AgentCard | string, - options?: ValidationOptions -): Promise -``` - -| Parameter | Type | Description | -|-----------|------|-------------| -| `input` | `AgentCard \| string` | Agent card object, file path, or URL | -| `options` | `ValidationOptions` | Validation configuration | - -**Example:** - -```typescript -const result = await validator.validate('./agent-card.json', { - strictness: 'progressive', - timeout: 10000, - skipSignatureVerification: false -}); -``` - -#### validateStrict() - -Convenience method for strict validation. - -```typescript -async validateStrict( - input: AgentCard | string, - options?: ValidationOptions -): Promise -``` - -**Example:** - -```typescript -const result = await validator.validateStrict('./agent-card.json'); -// Equivalent to: validator.validate(input, { strictness: 'strict' }) -``` - -#### validateProgressive() - -Convenience method for progressive validation (default mode). - -```typescript -async validateProgressive( - input: AgentCard | string, - options?: ValidationOptions -): Promise -``` - -#### validateConservative() - -Convenience method for conservative validation (minimal requirements). - -```typescript -async validateConservative( - input: AgentCard | string, - options?: ValidationOptions -): Promise -``` - -#### validateSchemaOnly() - -Schema validation without network requests. - -```typescript -async validateSchemaOnly( - card: AgentCard, - options?: ValidationOptions -): Promise -``` - -**Example:** - -```typescript -import { readFileSync } from 'fs'; - -const cardJson = JSON.parse(readFileSync('./agent-card.json', 'utf8')); -const result = await validator.validateSchemaOnly(cardJson); -``` - ---- - -## Types - -### ValidationOptions - -```typescript -interface ValidationOptions { - strictness?: 'strict' | 'progressive' | 'conservative'; - timeout?: number; // HTTP timeout in ms (default: 10000) - skipDynamic?: boolean; // Skip network requests - skipSignatureVerification?: boolean; // Skip JWS verification - verbose?: boolean; // Enable detailed logging - registryReady?: boolean; // Check registry readiness - showVersionCompat?: boolean; // Include version analysis -} -``` - -### ValidationResult - -```typescript -interface ValidationResult { - success: boolean; // Overall pass/fail - score: number; // 0-100 score - errors: ValidationError[]; // Blocking issues - warnings: ValidationWarning[]; - suggestions: ValidationSuggestion[]; - validations: ValidationCheck[]; - versionInfo?: VersionValidationInfo; - scoringResult?: ScoringResult; -} -``` - -### ValidationError - -```typescript -interface ValidationError { - code: string; // e.g., 'SCHEMA_VALIDATION_ERROR' - message: string; // Human-readable description - field?: string; // JSON path (e.g., 'skills.0.id') - severity: 'error'; - fixable?: boolean; // Can be auto-fixed -} -``` - -### ValidationWarning - -```typescript -interface ValidationWarning { - code: string; - message: string; - field?: string; - severity: 'warning'; - fixable?: boolean; -} -``` - -### ValidationCheck - -```typescript -interface ValidationCheck { - id: string; // e.g., 'schema_validation' - name: string; // e.g., 'Schema Validation' - status: 'passed' | 'failed' | 'skipped'; - message: string; - duration?: number; // ms - details?: string; -} -``` - -### AgentCard - -Full A2A v0.3.0 agent card type: - -```typescript -interface AgentCard { - // Required fields - protocolVersion: string; - name: string; - description: string; - url: string; - version: string; - capabilities: AgentCapabilities; - defaultInputModes: string[]; - defaultOutputModes: string[]; - skills: AgentSkill[]; - - // Optional fields - preferredTransport?: 'JSONRPC' | 'GRPC' | 'HTTP+JSON'; - additionalInterfaces?: AgentInterface[]; - provider?: AgentProvider; - iconUrl?: string; - documentationUrl?: string; - securitySchemes?: Record; - security?: Array>; - supportsAuthenticatedExtendedCard?: boolean; - signatures?: AgentCardSignature[]; - extensions?: AgentExtension[]; -} -``` - -### AgentSkill - -```typescript -interface AgentSkill { - id: string; // Required, unique - name: string; // Required, max 200 chars - description: string; // Required, max 2000 chars - tags: string[]; // Required, at least one - examples?: string[]; - inputModes?: string[]; - outputModes?: string[]; -} -``` - -### HttpClient - -Interface for custom HTTP clients: - -```typescript -interface HttpClient { - get(url: string, options?: RequestOptions): Promise; -} - -interface RequestOptions { - timeout?: number; - headers?: Record; - signal?: AbortSignal; -} - -interface HttpResponse { - status: number; - data: any; - headers: Record; -} -``` - ---- - -## Examples - -### Express Middleware - -```typescript -import express from 'express'; -import { A2AValidator } from 'capiscio'; - -const app = express(); -const validator = new A2AValidator(); - -app.post('/validate', express.json(), async (req, res) => { - const result = await validator.validate(req.body); - - res.json({ - valid: result.success, - score: result.score, - errors: result.errors, - warnings: result.warnings - }); -}); -``` - -### Batch Validation - -```typescript -import { A2AValidator } from 'capiscio'; -import { glob } from 'glob'; - -const validator = new A2AValidator(); -const files = await glob('./agents/**/*.json'); - -const results = await Promise.all( - files.map(async (file) => ({ - file, - result: await validator.validate(file) - })) -); - -// Summary -const passed = results.filter(r => r.result.success).length; -console.log(`${passed}/${results.length} agents passed validation`); - -// Failed agents -results - .filter(r => !r.result.success) - .forEach(r => { - console.log(`❌ ${r.file}:`); - r.result.errors.forEach(e => console.log(` ${e.message}`)); - }); -``` - -### Custom HTTP Client - -```typescript -import { A2AValidator, HttpClient, HttpResponse } from 'capiscio'; - -class AuthenticatedClient implements HttpClient { - constructor(private token: string) {} - - async get(url: string, options?: RequestOptions): Promise { - const response = await fetch(url, { - headers: { - 'Authorization': `Bearer ${this.token}`, - ...options?.headers - }, - signal: options?.signal - }); - - return { - status: response.status, - data: await response.json(), - headers: Object.fromEntries(response.headers) - }; - } -} - -const validator = new A2AValidator( - new AuthenticatedClient(process.env.API_TOKEN!) -); -``` - -### Error Code Reference - -| Code | Description | -|------|-------------| -| `SCHEMA_VALIDATION_ERROR` | Required field missing or invalid type | -| `VERSION_MISMATCH_ERROR` | Protocol version incompatibility | -| `SIGNATURE_VERIFICATION_FAILED` | JWS signature invalid | -| `PRIMARY_ENDPOINT_UNREACHABLE` | Main URL not responding | -| `TRANSPORT_URL_CONFLICT` | Conflicting transport declarations | -| `JSONRPC_ENDPOINT_ERROR` | JSON-RPC protocol test failed | - ---- - -## Exports - -The package exports these items: - -```typescript -// Classes -export { A2AValidator } from './validator/a2a-validator'; -export { FetchHttpClient } from './validator/http-client'; -export { ValidateCommand } from './commands/validate'; -export { ConsoleOutput } from './output/console'; -export { JsonOutput } from './output/json'; - -// Types -export * from './types'; -``` - ---- - -## See Also - -- [CLI Reference](./cli.md) - Command-line usage -- [Scoring System](../guides/scoring.md) - Understanding validation scores -- [Programmatic Usage](../guides/programmatic-usage.md) - Integration patterns diff --git a/docs/reference/cli.md b/docs/reference/cli.md deleted file mode 100644 index e35d065..0000000 --- a/docs/reference/cli.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: CLI Reference -description: Command-line reference for the capiscio npm package. ---- - -# CLI Reference - -Complete reference for the `capiscio` command-line interface. - ---- - -## validate - -Validate an A2A agent card from a file or URL. - -```bash -capiscio validate [input] [options] -``` - -### Arguments - -| Argument | Description | -|----------|-------------| -| `input` | Path to agent-card.json, URL, or omit to auto-detect | - -### Options - -| Flag | Type | Default | Description | -|------|------|---------|-------------| -| `--strict` | boolean | false | Strict validation mode | -| `--progressive` | boolean | true | Progressive validation (default) | -| `--schema-only` | boolean | false | Validate schema only, skip network requests | -| `--skip-signature` | boolean | false | Skip JWS signature verification | -| `--test-live` | boolean | false | Test live agent endpoint | -| `--registry-ready` | boolean | false | Check registry deployment readiness | -| `--json` | boolean | false | Output results as JSON | -| `--errors-only` | boolean | false | Show only errors and warnings | -| `--verbose` | boolean | false | Show detailed validation steps | -| `--timeout ` | string | 10000 | Request timeout in milliseconds | - ---- - -## Examples - -### Basic Validation - -```bash -# Local file -capiscio validate ./agent-card.json - -# Remote agent (auto-discovers /.well-known/agent-card.json) -capiscio validate https://my-agent.example.com - -# Auto-detect in current directory -capiscio validate -``` - -### Validation Modes - -```bash -# Progressive (default) - warnings for issues -capiscio validate ./agent-card.json - -# Strict - warnings become errors -capiscio validate ./agent-card.json --strict - -# Schema only - no network requests -capiscio validate ./agent-card.json --schema-only -``` - -### CI/CD - -```bash -# JSON output for parsing -capiscio validate ./agent-card.json --json - -# Production pipeline -capiscio validate ./agent-card.json --strict --json -``` - -### Live Testing - -```bash -# Test endpoint responds correctly -capiscio validate https://agent.example.com --test-live - -# With custom timeout -capiscio validate https://agent.example.com --test-live --timeout 15000 -``` - ---- - -## Exit Codes - -| Code | Meaning | -|------|---------| -| 0 | Validation passed | -| 1 | Validation failed | - ---- - -## JSON Output - -When using `--json`, output follows this structure: - -```json -{ - "success": true, - "score": 95, - "errors": [], - "warnings": [ - { - "code": "NO_SIGNATURES_FOUND", - "message": "No signatures present", - "severity": "warning" - } - ], - "validations": [ - { - "id": "schema_validation", - "status": "passed", - "message": "Agent card conforms to A2A v0.3.0" - } - ], - "scoringResult": { - "compliance": { "total": 95, "rating": "Excellent" }, - "trust": { "total": 80, "rating": "Good" }, - "availability": null - } -} -``` - ---- - -## See Also - -- [Scoring Guide](../guides/scoring.md) - Understanding validation scores -- [Programmatic Usage](../guides/programmatic-usage.md) - Use the CLI from Node.js -- [validate-a2a](https://github.com/capiscio/validate-a2a) - GitHub Action for CI/CD diff --git a/docs/reference/commands.md b/docs/reference/commands.md new file mode 100644 index 0000000..4d6f232 --- /dev/null +++ b/docs/reference/commands.md @@ -0,0 +1,177 @@ +--- +title: CLI Commands Reference +description: Complete reference for all CapiscIO CLI commands +--- + +# CLI Commands Reference + +The CapiscIO CLI passes all commands directly to the `capiscio-core` binary. This page documents all available commands. + +## Wrapper Commands + +These commands are handled by the Node.js wrapper itself: + +| Command | Description | +|---------|-------------| +| `--wrapper-version` | Display wrapper package version | +| `--wrapper-clean` | Remove cached binary, forcing re-download | + +## Core Commands + +All other commands are passed through to `capiscio-core`: + +### validate + +Validate an Agent Card for A2A protocol compliance. + +```bash +capiscio validate [file-or-url] [flags] +``` + +**Flags:** + +| Flag | Description | +|------|-------------| +| `--strict` | Enable strict validation mode | +| `--json` | Output results as JSON | +| `--schema-only` | Validate schema only, skip endpoint testing | +| `--test-live` | Test live agent endpoint | +| `--skip-signature` | Skip JWS signature verification | +| `--registry-ready` | Check registry deployment readiness | +| `--errors-only` | Show only errors and warnings | +| `--timeout ` | Request timeout (default: 10s) | + +**Examples:** + +```bash +# Validate local file +capiscio validate ./agent-card.json + +# Validate with JSON output +capiscio validate ./agent-card.json --json + +# Validate URL +capiscio validate https://example.com/.well-known/agent-card.json + +# Strict mode for production +capiscio validate ./agent-card.json --strict + +# Test live endpoint +capiscio validate ./agent-card.json --test-live +``` + +--- + +### badge + +Manage Trust Badges (RFC-002). + +#### badge issue + +Issue a new Trust Badge. + +```bash +capiscio badge issue [flags] +``` + +**Flags:** + +| Flag | Description | +|------|-------------| +| `--self-sign` | Issue self-signed badge (Level 0, did:key) | +| `--level <0-4>` | Trust level (0=SS, 1=DV, 2=OV, 3=EV, 4=CV) | +| `--sub ` | Subject DID | +| `--aud ` | Audience (comma-separated URLs) | +| `--exp ` | Expiration duration (default: 5m) | +| `--key ` | Path to private key file | + +**Examples:** + +```bash +# Self-signed badge for development +capiscio badge issue --self-sign + +# With custom expiration +capiscio badge issue --self-sign --exp 1h + +# With audience restriction +capiscio badge issue --self-sign --aud "https://api.example.com" +``` + +#### badge verify + +Verify a Trust Badge. + +```bash +capiscio badge verify [flags] +``` + +**Flags:** + +| Flag | Description | +|------|-------------| +| `--accept-self-signed` | Accept self-signed badges (Level 0) | +| `--audience ` | Verify audience claim | +| `--skip-revocation` | Skip revocation check | +| `--json` | Output as JSON | + +**Examples:** + +```bash +# Verify badge (rejects self-signed by default) +capiscio badge verify "eyJhbGciOiJFZERTQSJ9..." + +# Accept self-signed for development +capiscio badge verify "eyJhbGciOiJFZERTQSJ9..." --accept-self-signed + +# JSON output +capiscio badge verify "eyJhbGciOiJFZERTQSJ9..." --json +``` + +--- + +### key + +Manage cryptographic keys. + +```bash +capiscio key [command] +``` + +**Subcommands:** + +- `generate` - Generate a new key pair +- `list` - List stored keys + +--- + +### gateway + +Start the CapiscIO Gateway server. + +```bash +capiscio gateway [flags] +``` + +--- + +## Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | Validation/verification failed | +| `2` | Network error | +| `3` | Protocol violation | + +## Getting Help + +```bash +# General help +capiscio --help + +# Command-specific help +capiscio validate --help +capiscio badge --help +capiscio badge issue --help +``` diff --git a/mkdocs.yml b/mkdocs.yml index b43d980..af79dc8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -86,8 +86,5 @@ nav: - Home: index.md - Getting Started: - Installation: getting-started/installation.md - - Guides: - - Programmatic Usage: guides/programmatic-usage.md - - Scoring: guides/scoring.md - Reference: - - CLI Reference: reference/cli.md + - Commands: reference/commands.md diff --git a/package-lock.json b/package-lock.json index c595ff2..41c3cb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,34 +1,27 @@ { - "name": "capiscio-cli", - "version": "2.1.0", + "name": "capiscio", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "capiscio-cli", - "version": "2.1.0", + "name": "capiscio", + "version": "2.2.0", "license": "Apache-2.0", "dependencies": { "axios": "^1.13.2", "chalk": "^4.1.2", - "commander": "^11.1.0", "execa": "^5.1.1", - "glob": "^10.3.10", - "inquirer": "^9.2.12", - "jose": "^6.1.0", "ora": "^5.4.1" }, "bin": { "capiscio": "bin/capiscio.js" }, "devDependencies": { - "@types/inquirer": "^9.0.3", "@types/node": "^20.8.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "@vitest/coverage-v8": "^1.6.0", - "@yao-pkg/pkg": "^6.7.0", - "esbuild": "^0.25.10", "eslint": "^8.57.1", "tsup": "^8.5.0", "typescript": "^5.2.2", @@ -53,23 +46,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -718,40 +694,11 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", - "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", - "license": "MIT", - "dependencies": { - "chardet": "^2.1.0", - "iconv-lite": "^0.7.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", - "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -769,6 +716,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -781,6 +729,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -793,12 +742,14 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -816,6 +767,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -831,6 +783,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -844,19 +797,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -961,6 +901,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -1289,17 +1230,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/inquirer": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.9.tgz", - "integrity": "sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/through": "*", - "rxjs": "^7.2.0" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1311,7 +1241,7 @@ "version": "20.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1324,16 +1254,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/through": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", - "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -1684,95 +1604,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@yao-pkg/pkg": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-6.7.0.tgz", - "integrity": "sha512-Q06diprlqZrZ0SFefUUhvVj06QboHsBOLyml2CpzWvMUdV7fleSZ8wq5tBrVfmjWu2/ka4bDWHwlocHuYD7HOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "@yao-pkg/pkg-fetch": "3.5.25", - "into-stream": "^6.0.0", - "minimist": "^1.2.6", - "multistream": "^4.1.0", - "picocolors": "^1.1.0", - "picomatch": "^4.0.2", - "prebuild-install": "^7.1.1", - "resolve": "^1.22.10", - "stream-meter": "^1.0.4", - "tar": "^7.4.3", - "tinyglobby": "^0.2.11", - "unzipper": "^0.12.3" - }, - "bin": { - "pkg": "lib-es5/bin.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@yao-pkg/pkg-fetch": { - "version": "3.5.25", - "resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.25.tgz", - "integrity": "sha512-6deLQjwn5EJVCGRb9Rsy5c8TZixRgiBHMu3cIFVakwZR6ebidE16/Oc7WDBvhQg9N3B3ExgDi7QA19w7Z2GZkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.6", - "picocolors": "^1.1.0", - "progress": "^2.0.3", - "semver": "^7.3.5", - "tar-fs": "^2.1.1", - "yargs": "^16.2.0" - }, - "bin": { - "pkg-fetch": "lib-es5/bin.js" - } - }, - "node_modules/@yao-pkg/pkg/node_modules/into-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", - "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "from2": "^2.3.0", - "p-is-promise": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@yao-pkg/pkg/node_modules/p-is-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", - "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@yao-pkg/pkg/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1809,19 +1640,6 @@ "node": ">=0.4.0" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1839,33 +1657,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1945,6 +1736,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -1978,17 +1770,11 @@ "readable-stream": "^3.4.0" } }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true, - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -2115,12 +1901,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chardet": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", - "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", - "license": "MIT" - }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -2150,16 +1930,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -2184,45 +1954,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -2262,15 +1993,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2295,13 +2017,6 @@ "node": "^14.18.0 || >=16.10.0" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2334,35 +2049,6 @@ } } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -2376,16 +2062,6 @@ "node": ">=6" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2414,16 +2090,6 @@ "node": ">=0.4.0" } }, - "node_modules/detect-libc": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", - "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -2474,77 +2140,19 @@ "node": ">= 0.4" } }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/duplexer2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/duplexer2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/duplexer2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } + "license": "MIT" }, "node_modules/es-define-property": { "version": "1.0.1", @@ -2633,20 +2241,10 @@ "@esbuild/win32-x64": "0.25.10" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -2870,16 +2468,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3042,6 +2630,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -3070,79 +2659,6 @@ "node": ">= 6" } }, - "node_modules/from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "node_modules/from2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/from2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/from2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/from2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT" - }, - "node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3174,16 +2690,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -3243,17 +2749,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT" - }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -3287,6 +2787,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -3347,13 +2848,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3416,20 +2910,6 @@ "dev": true, "license": "MIT" }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3439,22 +2919,6 @@ "node": ">=10.17.0" } }, - "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -3530,52 +2994,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/inquirer": { - "version": "9.3.8", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz", - "integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==", - "license": "MIT", - "dependencies": { - "@inquirer/external-editor": "^1.0.2", - "@inquirer/figures": "^1.0.3", - "ansi-escapes": "^4.3.2", - "cli-width": "^4.1.0", - "mute-stream": "1.0.0", - "ora": "^5.4.1", - "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3590,6 +3008,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3725,6 +3144,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -3736,15 +3156,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jose": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", - "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -3775,19 +3186,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3809,19 +3207,6 @@ "dev": true, "license": "MIT" }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3953,6 +3338,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, "license": "ISC" }, "node_modules/magic-string": { @@ -4078,45 +3464,16 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT" - }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -4137,40 +3494,6 @@ "dev": true, "license": "MIT" }, - "node_modules/multistream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", - "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "once": "^1.4.0", - "readable-stream": "^3.6.0" - } - }, - "node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -4202,13 +3525,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4216,72 +3532,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-abi": { - "version": "3.77.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", - "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -4406,6 +3656,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -4450,17 +3701,11 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -4614,33 +3859,6 @@ } } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4679,40 +3897,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4744,32 +3934,6 @@ ], "license": "MIT" }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -4805,37 +3969,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4981,15 +4114,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5014,15 +4138,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5043,12 +4158,6 @@ ], "license": "MIT" }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -5094,6 +4203,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -5102,53 +4212,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5183,56 +4246,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stream-meter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", - "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.1.4" - } - }, - "node_modules/stream-meter/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/stream-meter/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/stream-meter/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/stream-meter/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5246,6 +4259,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -5261,6 +4275,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -5288,6 +4303,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -5376,73 +4392,6 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/tar-fs/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -5669,12 +4618,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/tsup": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.0.tgz", @@ -5752,19 +4695,6 @@ "node": ">= 8" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5826,32 +4756,8 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unzipper": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", - "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", "dev": true, - "license": "MIT", - "dependencies": { - "bluebird": "~3.7.2", - "duplexer2": "~0.1.4", - "fs-extra": "^11.2.0", - "graceful-fs": "^4.2.2", - "node-int64": "^0.4.0" - } + "license": "MIT" }, "node_modules/uri-js": { "version": "4.4.1", @@ -6663,25 +5569,12 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -6702,55 +5595,6 @@ "dev": true, "license": "ISC" }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6763,18 +5607,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/package.json b/package.json index f42c93b..01f9e58 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capiscio", - "version": "2.1.2", - "description": "The definitive CLI tool for validating A2A (Agent-to-Agent) protocol agent cards", + "version": "2.2.0", + "description": "The official CapiscIO CLI tool for validating A2A agents", "keywords": [ "a2a", "agent", @@ -38,12 +38,9 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", - "test": "npm run test:unit", - "test:unit": "vitest run src/__tests__", - "test:integration": "npm run build && vitest run tests/integration tests/e2e", + "test": "vitest run", "test:watch": "vitest --watch", - "test:coverage": "vitest run src/__tests__ --coverage", - "test:e2e": "npm run build && vitest run tests/e2e", + "test:coverage": "vitest run --coverage", "lint": "eslint src --ext .ts", "lint:fix": "eslint src --ext .ts --fix", "typecheck": "tsc --noEmit", @@ -54,15 +51,10 @@ "dependencies": { "axios": "^1.13.2", "chalk": "^4.1.2", - "commander": "^11.1.0", "execa": "^5.1.1", - "glob": "^10.3.10", - "inquirer": "^9.2.12", - "jose": "^6.1.0", "ora": "^5.4.1" }, "devDependencies": { - "@types/inquirer": "^9.0.3", "@types/node": "^20.8.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts new file mode 100644 index 0000000..f3b3802 --- /dev/null +++ b/src/__tests__/cli.test.ts @@ -0,0 +1,456 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import fs from 'fs'; +import os from 'os'; + +// Mock stream pipeline +vi.mock('stream', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: { + ...actual.default, + pipeline: (source: any, dest: any, cb: any) => { + if (cb) cb(null); + return { on: vi.fn() }; + }, + }, + pipeline: (source: any, dest: any, cb: any) => { + if (cb) cb(null); + return { on: vi.fn() }; + } + }; +}); + +// Mock modules before imports +vi.mock('fs'); +vi.mock('axios'); +vi.mock('ora', () => ({ + default: vi.fn(() => ({ + start: vi.fn().mockReturnThis(), + succeed: vi.fn().mockReturnThis(), + fail: vi.fn().mockReturnThis(), + })), +})); + +// Reset singleton between tests +function resetBinaryManager() { + // Clear the module cache to reset the singleton + vi.resetModules(); +} + +describe('BinaryManager', () => { + beforeEach(() => { + vi.clearAllMocks(); + resetBinaryManager(); + + // Default mocks + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.mkdirSync).mockReturnValue(undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('getInstance', () => { + it('should be a singleton', async () => { + const { BinaryManager } = await import('../utils/binary-manager'); + const instance1 = BinaryManager.getInstance(); + const instance2 = BinaryManager.getInstance(); + expect(instance1).toBe(instance2); + }); + + it('should create bin directory if it does not exist', async () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + + const { BinaryManager } = await import('../utils/binary-manager'); + BinaryManager.getInstance(); + + expect(fs.mkdirSync).toHaveBeenCalled(); + }); + + it('should fallback to home directory if bin creation fails', async () => { + vi.mocked(fs.existsSync).mockImplementation((p) => { + if (String(p).includes('.capiscio')) return false; + return false; + }); + vi.mocked(fs.mkdirSync).mockImplementation((p) => { + if (!String(p).includes('.capiscio')) { + throw new Error('Permission denied'); + } + return undefined; + }); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + expect(instance).toBeDefined(); + }); + }); + + describe('getPlatform (via constructor)', () => { + it('should map darwin correctly', async () => { + vi.spyOn(os, 'platform').mockReturnValue('darwin'); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + expect(instance).toBeDefined(); + }); + + it('should map linux correctly', async () => { + vi.spyOn(os, 'platform').mockReturnValue('linux'); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + expect(instance).toBeDefined(); + }); + + it('should map win32 to windows', async () => { + vi.spyOn(os, 'platform').mockReturnValue('win32'); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + expect(instance).toBeDefined(); + }); + + it('should throw for unsupported platform', async () => { + vi.spyOn(os, 'platform').mockReturnValue('freebsd' as NodeJS.Platform); + + const { BinaryManager } = await import('../utils/binary-manager'); + + expect(() => BinaryManager.getInstance()).toThrow('Unsupported platform: freebsd'); + }); + }); + + describe('getArch (via constructor)', () => { + it('should map x64 to amd64', async () => { + vi.spyOn(os, 'arch').mockReturnValue('x64'); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + expect(instance).toBeDefined(); + }); + + it('should handle arm64', async () => { + vi.spyOn(os, 'arch').mockReturnValue('arm64'); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + expect(instance).toBeDefined(); + }); + + // Note: getArch() is only called during install(), not in constructor + // The unsupported architecture error is tested implicitly in install() tests + }); + + describe('getBinaryPath', () => { + it('should use CAPISCIO_CORE_PATH env var if set and file exists', async () => { + const customPath = '/custom/path/to/capiscio'; + process.env.CAPISCIO_CORE_PATH = customPath; + vi.mocked(fs.existsSync).mockImplementation((p) => String(p) === customPath || true); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + const result = await instance.getBinaryPath(); + + expect(result).toBe(customPath); + + delete process.env.CAPISCIO_CORE_PATH; + }); + + it('should warn and fallback if CAPISCIO_CORE_PATH does not exist', async () => { + const customPath = '/nonexistent/path'; + process.env.CAPISCIO_CORE_PATH = customPath; + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + vi.mocked(fs.existsSync).mockImplementation((p) => { + if (String(p) === customPath) return false; + return true; // Binary exists in default location + }); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + await instance.getBinaryPath(); + + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('does not exist')); + + delete process.env.CAPISCIO_CORE_PATH; + warnSpy.mockRestore(); + }); + + it('should return existing binary path without downloading', async () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + const result = await instance.getBinaryPath(); + + expect(result).toContain('capiscio-core'); + }); + }); +}); + +describe('CLI Package', () => { + beforeEach(() => { + vi.clearAllMocks(); + resetBinaryManager(); + vi.mocked(fs.existsSync).mockReturnValue(true); + }); + + it('should export version', async () => { + const { version } = await import('../index'); + expect(version).toBe('2.2.0'); + }); + + it('should export BinaryManager', async () => { + const { BinaryManager: ExportedBinaryManager } = await import('../index'); + expect(ExportedBinaryManager).toBeDefined(); + expect(typeof ExportedBinaryManager.getInstance).toBe('function'); + }); +}); + +describe('Binary naming', () => { + beforeEach(() => { + vi.clearAllMocks(); + resetBinaryManager(); + vi.mocked(fs.existsSync).mockReturnValue(true); + }); + + it('should add .exe extension on Windows', async () => { + vi.spyOn(os, 'platform').mockReturnValue('win32'); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + const binaryPath = await instance.getBinaryPath(); + + expect(binaryPath).toMatch(/\.exe$/); + }); + + it('should not add .exe extension on Unix platforms', async () => { + vi.spyOn(os, 'platform').mockReturnValue('darwin'); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + const binaryPath = await instance.getBinaryPath(); + + expect(binaryPath).not.toMatch(/\.exe$/); + }); +}); + +describe('Version handling', () => { + beforeEach(() => { + vi.clearAllMocks(); + resetBinaryManager(); + vi.mocked(fs.existsSync).mockReturnValue(true); + }); + + afterEach(() => { + delete process.env.CAPISCIO_CORE_VERSION; + }); + + it('should use default version when env var not set', async () => { + delete process.env.CAPISCIO_CORE_VERSION; + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + expect(instance).toBeDefined(); + // Default version is v1.0.2 as defined in binary-manager.ts + }); + + it('should respect CAPISCIO_CORE_VERSION env var', async () => { + process.env.CAPISCIO_CORE_VERSION = 'v2.0.0'; + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + expect(instance).toBeDefined(); + }); +}); + +describe('Install functionality', () => { + beforeEach(() => { + vi.clearAllMocks(); + resetBinaryManager(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should trigger install when binary does not exist', async () => { + const axios = await import('axios'); + + // Binary does not exist - should trigger install + let callCount = 0; + vi.mocked(fs.existsSync).mockImplementation((p) => { + const pathStr = String(p); + // First call is for bin dir check during constructor + // Subsequent calls are for binary path check + if (pathStr.includes('capiscio-core')) { + callCount++; + return callCount > 1; // First check returns false (triggers install), then true + } + return pathStr.includes('package.json'); + }); + vi.mocked(fs.mkdirSync).mockReturnValue(undefined); + vi.mocked(fs.mkdtempSync).mockReturnValue('/tmp/capiscio-test'); + vi.mocked(fs.createWriteStream).mockReturnValue({ + on: vi.fn(), + once: vi.fn(), + emit: vi.fn(), + write: vi.fn(), + end: vi.fn(), + } as any); + vi.mocked(fs.copyFileSync).mockReturnValue(undefined); + vi.mocked(fs.chmodSync).mockReturnValue(undefined); + vi.mocked(fs.rmSync).mockReturnValue(undefined); + + // Mock axios response + const mockStream: any = { + pipe: vi.fn().mockReturnThis(), + on: vi.fn((event, cb) => { + if (event === 'end') cb(); + return mockStream; + }), + once: vi.fn(), + emit: vi.fn(), + }; + vi.mocked(axios.default.get).mockResolvedValue({ data: mockStream }); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + // This should trigger install since binary doesn't exist + try { + await instance.getBinaryPath(); + } catch { + // Install may fail due to mocking complexity, but we verify axios was called + } + + // Verify download was attempted + expect(axios.default.get).toHaveBeenCalled(); + }); + + it('should handle 404 error from download', async () => { + const axios = await import('axios'); + + vi.mocked(fs.existsSync).mockImplementation((p) => { + const pathStr = String(p); + if (pathStr.includes('capiscio-core')) return false; + return pathStr.includes('package.json'); + }); + vi.mocked(fs.mkdirSync).mockReturnValue(undefined); + + const mockError = { + response: { status: 404 }, + message: 'Not found', + }; + vi.mocked(axios.default.get).mockRejectedValue(mockError); + vi.mocked(axios.isAxiosError).mockReturnValue(true); + + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + await expect(instance.getBinaryPath()).rejects.toEqual(mockError); + + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Could not find binary')); + + errorSpy.mockRestore(); + }); + + it('should handle network error from download', async () => { + const axios = await import('axios'); + + vi.mocked(fs.existsSync).mockImplementation((p) => { + const pathStr = String(p); + if (pathStr.includes('capiscio-core')) return false; + return pathStr.includes('package.json'); + }); + vi.mocked(fs.mkdirSync).mockReturnValue(undefined); + + const mockError = { + response: { status: 500 }, + message: 'Server error', + }; + vi.mocked(axios.default.get).mockRejectedValue(mockError); + vi.mocked(axios.isAxiosError).mockReturnValue(true); + + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + await expect(instance.getBinaryPath()).rejects.toEqual(mockError); + + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Network error')); + + errorSpy.mockRestore(); + }); + + it('should handle non-axios error from download', async () => { + const axios = await import('axios'); + + vi.mocked(fs.existsSync).mockImplementation((p) => { + const pathStr = String(p); + if (pathStr.includes('capiscio-core')) return false; + return pathStr.includes('package.json'); + }); + vi.mocked(fs.mkdirSync).mockReturnValue(undefined); + + const mockError = new Error('Unknown error'); + vi.mocked(axios.default.get).mockRejectedValue(mockError); + vi.mocked(axios.isAxiosError).mockReturnValue(false); + + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + await expect(instance.getBinaryPath()).rejects.toThrow('Unknown error'); + + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown error')); + + errorSpy.mockRestore(); + }); +}); + +describe('findPackageRoot', () => { + beforeEach(() => { + vi.clearAllMocks(); + resetBinaryManager(); + }); + + it('should find package root when package.json exists', async () => { + vi.mocked(fs.existsSync).mockImplementation((p) => { + return String(p).includes('package.json'); + }); + vi.mocked(fs.mkdirSync).mockReturnValue(undefined); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + expect(instance).toBeDefined(); + }); + + it('should fallback when package.json not found', async () => { + vi.mocked(fs.existsSync).mockImplementation((p) => { + // Never find package.json, but pretend bin dirs exist + if (String(p).includes('package.json')) return false; + return true; + }); + vi.mocked(fs.mkdirSync).mockReturnValue(undefined); + + const { BinaryManager } = await import('../utils/binary-manager'); + const instance = BinaryManager.getInstance(); + + expect(instance).toBeDefined(); + }); +}); diff --git a/src/__tests__/http-client.test.ts b/src/__tests__/http-client.test.ts deleted file mode 100644 index 30b3a27..0000000 --- a/src/__tests__/http-client.test.ts +++ /dev/null @@ -1,515 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { FetchHttpClient } from '../validator/http-client'; -import { HttpError } from '../types'; -import { Logger } from '../utils/logger'; - -// Mock fetch globally -const mockFetch = vi.fn(); -global.fetch = mockFetch; - -// Mock AbortController -const mockAbort = vi.fn(); -const mockAbortController = { - abort: mockAbort, - signal: { aborted: false } -}; -global.AbortController = vi.fn(() => mockAbortController) as any; - -// Mock setTimeout and clearTimeout -const mockSetTimeout = vi.fn(); -const mockClearTimeout = vi.fn(); -global.setTimeout = mockSetTimeout as any; -global.clearTimeout = mockClearTimeout as any; - -describe('FetchHttpClient', () => { - let httpClient: FetchHttpClient; - let mockLogger: Logger; - - beforeEach(() => { - vi.clearAllMocks(); - mockLogger = { - debug: vi.fn(), - network: vi.fn(), - error: vi.fn(), - } as any; - - // Default timeout behavior - mockSetTimeout.mockReturnValue('timeout-id'); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('Constructor', () => { - it('should create instance without logger', () => { - const client = new FetchHttpClient(); - expect(client).toBeInstanceOf(FetchHttpClient); - }); - - it('should create instance with logger', () => { - const client = new FetchHttpClient(mockLogger); - expect(client).toBeInstanceOf(FetchHttpClient); - }); - }); - - describe('Successful GET requests', () => { - beforeEach(() => { - httpClient = new FetchHttpClient(mockLogger); - }); - - it('should make successful GET request and return data', async () => { - const mockResponseData = { name: 'Test Agent', version: '1.0.0' }; - const mockResponse = { - ok: true, - status: 200, - json: vi.fn().mockResolvedValue(mockResponseData), - headers: new Map([['content-type', 'application/json']]) - }; - - mockFetch.mockResolvedValue(mockResponse); - - const result = await httpClient.get('https://example.com/agent-card.json'); - - expect(mockFetch).toHaveBeenCalledWith('https://example.com/agent-card.json', { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'User-Agent': 'capiscio-cli/1.1.0' - }, - signal: mockAbortController.signal - }); - - expect(result.status).toBe(200); - expect(result.data).toEqual(mockResponseData); - expect(result.headers).toEqual({ 'content-type': 'application/json' }); - expect(mockClearTimeout).toHaveBeenCalledWith('timeout-id'); - }); - - it('should include custom headers in request', async () => { - const mockResponse = { - ok: true, - status: 200, - json: vi.fn().mockResolvedValue({}), - headers: new Map() - }; - - mockFetch.mockResolvedValue(mockResponse); - - await httpClient.get('https://example.com/test', { - headers: { 'Authorization': 'Bearer token123' } - }); - - expect(mockFetch).toHaveBeenCalledWith('https://example.com/test', { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'User-Agent': 'capiscio-cli/1.1.0', - 'Authorization': 'Bearer token123' - }, - signal: mockAbortController.signal - }); - }); - - it('should use custom timeout', async () => { - const mockResponse = { - ok: true, - status: 200, - json: vi.fn().mockResolvedValue({}), - headers: new Map() - }; - - mockFetch.mockResolvedValue(mockResponse); - - await httpClient.get('https://example.com/test', { timeout: 5000 }); - - expect(mockSetTimeout).toHaveBeenCalledWith(expect.any(Function), 5000); - }); - - it('should use default timeout when not specified', async () => { - const mockResponse = { - ok: true, - status: 200, - json: vi.fn().mockResolvedValue({}), - headers: new Map() - }; - - mockFetch.mockResolvedValue(mockResponse); - - await httpClient.get('https://example.com/test'); - - expect(mockSetTimeout).toHaveBeenCalledWith(expect.any(Function), 10000); - }); - - it('should use custom abort signal when provided', async () => { - const customController = new AbortController(); - const mockResponse = { - ok: true, - status: 200, - json: vi.fn().mockResolvedValue({}), - headers: new Map() - }; - - mockFetch.mockResolvedValue(mockResponse); - - await httpClient.get('https://example.com/test', { - signal: customController.signal - }); - - expect(mockFetch).toHaveBeenCalledWith('https://example.com/test', - expect.objectContaining({ - signal: customController.signal - }) - ); - }); - - it('should log debug and network information', async () => { - const mockResponse = { - ok: true, - status: 200, - json: vi.fn().mockResolvedValue({}), - headers: new Map() - }; - - mockFetch.mockResolvedValue(mockResponse); - - await httpClient.get('https://example.com/test'); - - expect(mockLogger.debug).toHaveBeenCalledWith( - 'Initiating HTTP GET request to https://example.com/test' - ); - expect(mockLogger.network).toHaveBeenCalledWith( - 'GET', - 'https://example.com/test', - 200, - expect.any(Number) - ); - }); - }); - - describe('HTTP Error responses', () => { - beforeEach(() => { - httpClient = new FetchHttpClient(mockLogger); - }); - - it('should handle 404 Not Found', async () => { - const mockResponse = { - ok: false, - status: 404, - statusText: 'Not Found' - }; - - mockFetch.mockResolvedValue(mockResponse); - - await expect(httpClient.get('https://example.com/missing')).rejects.toThrow( - expect.objectContaining({ - message: 'HTTP 404: Not Found', - status: 404, - code: 'NOT_FOUND' - }) - ); - - expect(mockLogger.error).toHaveBeenCalledWith('HTTP request failed: 404 Not Found'); - }); - - it('should handle 401 Unauthorized', async () => { - const mockResponse = { - ok: false, - status: 401, - statusText: 'Unauthorized' - }; - - mockFetch.mockResolvedValue(mockResponse); - - await expect(httpClient.get('https://example.com/protected')).rejects.toThrow( - expect.objectContaining({ - status: 401, - code: 'UNAUTHORIZED' - }) - ); - }); - - it('should handle 500 Internal Server Error', async () => { - const mockResponse = { - ok: false, - status: 500, - statusText: 'Internal Server Error' - }; - - mockFetch.mockResolvedValue(mockResponse); - - await expect(httpClient.get('https://example.com/error')).rejects.toThrow( - expect.objectContaining({ - status: 500, - code: 'INTERNAL_SERVER_ERROR' - }) - ); - }); - - it('should handle 429 Rate Limited', async () => { - const mockResponse = { - ok: false, - status: 429, - statusText: 'Too Many Requests' - }; - - mockFetch.mockResolvedValue(mockResponse); - - await expect(httpClient.get('https://example.com/rate-limited')).rejects.toThrow( - expect.objectContaining({ - status: 429, - code: 'RATE_LIMITED' - }) - ); - }); - - it('should handle unknown HTTP status codes', async () => { - const mockResponse = { - ok: false, - status: 418, - statusText: "I'm a teapot" - }; - - mockFetch.mockResolvedValue(mockResponse); - - await expect(httpClient.get('https://example.com/teapot')).rejects.toThrow( - expect.objectContaining({ - status: 418, - code: 'HTTP_ERROR' - }) - ); - }); - }); - - describe('Network and timeout errors', () => { - beforeEach(() => { - httpClient = new FetchHttpClient(mockLogger); - }); - - it('should handle timeout errors', async () => { - const abortError = new Error('AbortError'); - abortError.name = 'AbortError'; - mockFetch.mockRejectedValue(abortError); - - await expect(httpClient.get('https://example.com/slow')).rejects.toThrow( - expect.objectContaining({ - message: 'Request timeout', - status: 408, - code: 'TIMEOUT' - }) - ); - }); - - it('should handle DNS resolution errors', async () => { - const dnsError = new Error('getaddrinfo ENOTFOUND invalid-domain.com'); - mockFetch.mockRejectedValue(dnsError); - - await expect(httpClient.get('https://invalid-domain.com/test')).rejects.toThrow( - expect.objectContaining({ - message: 'Domain not found - check the URL', - status: 0, - code: 'ENOTFOUND' - }) - ); - }); - - it('should handle connection refused errors', async () => { - const connError = new Error('connect ECONNREFUSED 127.0.0.1:8080'); - mockFetch.mockRejectedValue(connError); - - await expect(httpClient.get('https://localhost:8080/test')).rejects.toThrow( - expect.objectContaining({ - message: 'Connection refused - agent endpoint not accessible', - status: 0, - code: 'ECONNREFUSED' - }) - ); - }); - - it('should handle generic fetch errors', async () => { - const fetchError = new Error('fetch failed'); - mockFetch.mockRejectedValue(fetchError); - - await expect(httpClient.get('https://example.com/fetch-error')).rejects.toThrow( - expect.objectContaining({ - message: 'Network error', - status: 0, - code: 'NETWORK_ERROR' - }) - ); - }); - - it('should handle unknown errors', async () => { - const unknownError = new Error('Something unexpected happened'); - mockFetch.mockRejectedValue(unknownError); - - await expect(httpClient.get('https://example.com/unknown')).rejects.toThrow( - expect.objectContaining({ - message: 'Something unexpected happened', - status: 0, - code: 'UNKNOWN' - }) - ); - }); - - it('should handle non-Error exceptions', async () => { - mockFetch.mockRejectedValue('string error'); - - await expect(httpClient.get('https://example.com/weird')).rejects.toThrow( - expect.objectContaining({ - message: 'Unknown error', - status: 0, - code: 'UNKNOWN' - }) - ); - }); - - it('should handle HttpError exceptions passthrough', async () => { - const httpError = new HttpError('Custom HTTP error', 403, 'CUSTOM_ERROR'); - mockFetch.mockRejectedValue(httpError); - - await expect(httpClient.get('https://example.com/custom')).rejects.toThrow(httpError); - }); - }); - - describe('Timeout handling', () => { - beforeEach(() => { - httpClient = new FetchHttpClient(); - }); - - it('should set up timeout and clear it on success', async () => { - const mockResponse = { - ok: true, - status: 200, - json: vi.fn().mockResolvedValue({}), - headers: new Map() - }; - - mockFetch.mockResolvedValue(mockResponse); - - await httpClient.get('https://example.com/test'); - - expect(mockSetTimeout).toHaveBeenCalledWith(expect.any(Function), 10000); - expect(mockClearTimeout).toHaveBeenCalledWith('timeout-id'); - }); - - it('should clear timeout on error', async () => { - const error = new Error('Network error'); - mockFetch.mockRejectedValue(error); - - try { - await httpClient.get('https://example.com/error'); - } catch (e) { - // Expected error - } - - expect(mockSetTimeout).toHaveBeenCalledWith(expect.any(Function), 10000); - expect(mockClearTimeout).toHaveBeenCalledWith('timeout-id'); - }); - - it('should call abort when timeout fires', async () => { - // Simulate timeout firing - let timeoutCallback: Function; - mockSetTimeout.mockImplementation((callback) => { - timeoutCallback = callback; - return 'timeout-id'; - }); - - const mockResponse = { - ok: true, - status: 200, - json: vi.fn().mockResolvedValue({}), - headers: new Map() - }; - - mockFetch.mockResolvedValue(mockResponse); - - const promise = httpClient.get('https://example.com/test'); - - // Fire the timeout - timeoutCallback!(); - - await promise; - - expect(mockAbort).toHaveBeenCalled(); - }); - }); - - describe('Client without logger', () => { - beforeEach(() => { - httpClient = new FetchHttpClient(); // No logger - }); - - it('should work without logger', async () => { - const mockResponse = { - ok: true, - status: 200, - json: vi.fn().mockResolvedValue({ test: 'data' }), - headers: new Map() - }; - - mockFetch.mockResolvedValue(mockResponse); - - const result = await httpClient.get('https://example.com/test'); - - expect(result.status).toBe(200); - expect(result.data).toEqual({ test: 'data' }); - // Should not throw when logger methods are called - }); - - it('should handle errors without logger', async () => { - const mockResponse = { - ok: false, - status: 404, - statusText: 'Not Found' - }; - - mockFetch.mockResolvedValue(mockResponse); - - await expect(httpClient.get('https://example.com/missing')).rejects.toThrow( - expect.objectContaining({ - status: 404, - code: 'NOT_FOUND' - }) - ); - }); - }); - - describe('Error code mapping', () => { - beforeEach(() => { - httpClient = new FetchHttpClient(); - }); - - const statusCodeTests = [ - { status: 400, code: 'BAD_REQUEST' }, - { status: 401, code: 'UNAUTHORIZED' }, - { status: 403, code: 'FORBIDDEN' }, - { status: 404, code: 'NOT_FOUND' }, - { status: 408, code: 'TIMEOUT' }, - { status: 429, code: 'RATE_LIMITED' }, - { status: 500, code: 'INTERNAL_SERVER_ERROR' }, - { status: 502, code: 'BAD_GATEWAY' }, - { status: 503, code: 'SERVICE_UNAVAILABLE' }, - { status: 504, code: 'GATEWAY_TIMEOUT' } - ]; - - statusCodeTests.forEach(({ status, code }) => { - it(`should map ${status} to ${code}`, async () => { - const mockResponse = { - ok: false, - status, - statusText: 'Error' - }; - - mockFetch.mockResolvedValue(mockResponse); - - await expect(httpClient.get('https://example.com/test')).rejects.toThrow( - expect.objectContaining({ - status, - code - }) - ); - }); - }); - }); -}); \ No newline at end of file diff --git a/src/__tests__/live-tester.test.ts b/src/__tests__/live-tester.test.ts deleted file mode 100644 index 7813a8c..0000000 --- a/src/__tests__/live-tester.test.ts +++ /dev/null @@ -1,569 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { LiveTester } from '../validator/live-tester'; -import { AgentCard } from '../types'; - -// Mock fetch globally -const mockFetch = vi.fn(); -global.fetch = mockFetch as any; - -describe('LiveTester', () => { - let liveTester: LiveTester; - const mockAgentCard: AgentCard = { - protocolVersion: '0.3.0', - name: 'Test Agent', - description: 'Test agent for live testing', - url: 'https://test-agent.com/rpc', - version: '1.0.0', - capabilities: {}, - defaultInputModes: ['text/plain'], - defaultOutputModes: ['text/plain'], - skills: [ - { - id: 'test-skill', - name: 'Test Skill', - description: 'A test skill', - tags: ['test'] - } - ] - }; - - beforeEach(() => { - liveTester = new LiveTester({ timeout: 5000 }); - mockFetch.mockClear(); - }); - - afterEach(() => { - vi.clearAllTimers(); - }); - - describe('Successful Live Tests', () => { - it('should successfully test agent with valid message response', async () => { - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123', - result: { - kind: 'message', - role: 'agent', - parts: [ - { - type: 'text', - text: 'Hello! I am available.' - } - ] - } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(true); - expect(result.endpoint).toBe('https://test-agent.com/rpc'); - expect(result.errors).toHaveLength(0); - expect(result.responseTime).toBeGreaterThanOrEqual(0); // Fixed: allow 0 for instant mocked response - expect(result.response).toEqual(mockResponse.result); - expect(mockFetch).toHaveBeenCalledTimes(1); - }); - - it('should successfully test agent with valid task response', async () => { - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123', - result: { - kind: 'task', - id: 'task-abc-123', - status: { - state: 'working' - } - } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(true); - expect(result.response.kind).toBe('task'); - }); - }); - - describe('Protocol Validation Failures', () => { - it('should fail when message has empty parts array', async () => { - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123', - result: { - kind: 'message', - role: 'agent', - parts: [] // Empty parts - invalid! - } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(false); - expect(result.errors).toContain("Message object must have a non-empty 'parts' array"); - }); - - it('should fail when message has wrong role', async () => { - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123', - result: { - kind: 'message', - role: 'user', // Wrong role! - parts: [{ type: 'text', text: 'test' }] - } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(false); - expect(result.errors).toContain("Message from agent must have 'role' set to 'agent'"); - }); - - it('should fail when task missing required id', async () => { - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123', - result: { - kind: 'task', - status: { - state: 'working' - } - // Missing 'id' field! - } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(false); - expect(result.errors).toContain("Task object missing required field: 'id'"); - }); - - it('should fail when task missing status.state', async () => { - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123', - result: { - kind: 'task', - id: 'task-123', - status: {} // Missing 'state'! - } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(false); - expect(result.errors).toContain("Task object missing required field: 'status.state'"); - }); - - it('should fail when response missing result', async () => { - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123' - // Missing 'result' field! - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(false); - expect(result.errors).toContain('JSON-RPC response missing result field'); - }); - }); - - describe('Network Errors', () => { - it('should handle timeout error', async () => { - // Mock abort behavior - const abortError = new Error('The operation was aborted'); - abortError.name = 'AbortError'; - - mockFetch.mockRejectedValueOnce(abortError); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(false); - expect(result.errors[0]).toContain('timed out'); - }); - - it('should handle connection refused error', async () => { - mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED')); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(false); - expect(result.errors[0]).toContain('Connection refused'); - }); - - it('should handle DNS resolution failure', async () => { - mockFetch.mockRejectedValueOnce(new Error('ENOTFOUND')); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(false); - expect(result.errors[0]).toContain('DNS resolution failed'); - }); - - it('should handle TLS certificate error', async () => { - mockFetch.mockRejectedValueOnce(new Error('certificate has expired')); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(false); - expect(result.errors[0]).toContain('TLS certificate error'); - }); - - it('should handle HTTP 500 error', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 500, - statusText: 'Internal Server Error', - headers: { - get: () => null - } - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(false); - expect(result.errors[0]).toContain('HTTP 500'); - }); - - it('should handle HTTP 404 error', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 404, - statusText: 'Not Found', - headers: { - get: () => null - } - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(false); - expect(result.errors[0]).toContain('HTTP 404'); - }); - - it('should handle non-JSON response', async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'text/html' : null - }, - json: async () => { throw new Error('Not JSON'); } - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.success).toBe(false); - expect(result.errors[0]).toContain('Expected JSON response, got text/html'); - }); - }); - - describe('Custom Options', () => { - it('should use custom test message', async () => { - const customMessage = 'Custom test message'; - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123', - result: { - kind: 'message', - role: 'agent', - parts: [{ type: 'text', text: 'Response' }] - } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - await liveTester.testAgent(mockAgentCard, { testMessage: customMessage }); - - const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body); - expect(requestBody.params.message.parts[0].text).toBe(customMessage); - }); - - it('should use custom timeout', async () => { - const customTester = new LiveTester({ timeout: 1000 }); - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123', - result: { - kind: 'message', - role: 'agent', - parts: [{ type: 'text', text: 'Response' }] - } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - const result = await customTester.testAgent(mockAgentCard); - expect(result.success).toBe(true); - }); - }); - - describe('Response Tracking', () => { - it('should track response time', async () => { - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123', - result: { - kind: 'message', - role: 'agent', - parts: [{ type: 'text', text: 'Response' }] - } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse // Remove artificial delay for test reliability - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.responseTime).toBeGreaterThanOrEqual(0); // Fixed: allow 0 for instant mocked response - }); - - it('should include timestamp', async () => { - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123', - result: { - kind: 'message', - role: 'agent', - parts: [{ type: 'text', text: 'Response' }] - } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.timestamp).toBeDefined(); - expect(new Date(result.timestamp).getTime()).toBeLessThanOrEqual(Date.now()); - }); - - it('should include request and response in result', async () => { - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123', - result: { - kind: 'message', - role: 'agent', - parts: [{ type: 'text', text: 'Response' }] - } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - const result = await liveTester.testAgent(mockAgentCard); - - expect(result.request).toBeDefined(); - expect(result.request.jsonrpc).toBe('2.0'); - expect(result.request.method).toBe('message/send'); - expect(result.response).toEqual(mockResponse.result); - }); - }); - - describe('Transport Protocol Support', () => { - it('should successfully test agent with HTTP+JSON transport', async () => { - const httpJsonAgent: AgentCard = { - ...mockAgentCard, - preferredTransport: 'HTTP+JSON' - }; - - const mockResponse = { - kind: 'message', - role: 'agent', - parts: [ - { - type: 'text', - text: 'Hello from HTTP+JSON!' - } - ] - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - const result = await liveTester.testAgent(httpJsonAgent); - - expect(result.success).toBe(true); - expect(result.errors).toHaveLength(0); - - // Verify the request format for HTTP+JSON (no jsonrpc wrapper) - const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body); - expect(requestBody.jsonrpc).toBeUndefined(); - expect(requestBody.message).toBeDefined(); - expect(requestBody.message.role).toBe('user'); - }); - - it('should default to JSONRPC when preferredTransport is not specified', async () => { - const agentWithoutTransport: AgentCard = { - protocolVersion: '0.3.0', - name: 'Test Agent', - description: 'Test agent without transport', - url: 'https://test-agent.com/rpc', - version: '1.0.0', - capabilities: {}, - defaultInputModes: ['text/plain'], - defaultOutputModes: ['text/plain'], - skills: [] - }; - - const mockResponse = { - jsonrpc: '2.0', - id: 'test-123', - result: { - kind: 'message', - role: 'agent', - parts: [{ type: 'text', text: 'Response' }] - } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - headers: { - get: (name: string) => name === 'content-type' ? 'application/json' : null - }, - json: async () => mockResponse - }); - - const result = await liveTester.testAgent(agentWithoutTransport); - - expect(result.success).toBe(true); - - // Verify JSONRPC format is used by default - const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body); - expect(requestBody.jsonrpc).toBe('2.0'); - expect(requestBody.method).toBe('message/send'); - }); - - it('should return error for unsupported GRPC transport', async () => { - const grpcAgent: AgentCard = { - ...mockAgentCard, - preferredTransport: 'GRPC' - }; - - const result = await liveTester.testAgent(grpcAgent); - - expect(result.success).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0]).toContain('GRPC transport is not yet supported'); - }); - - it('should handle missing endpoint URL', async () => { - const agentWithoutUrl: AgentCard = { - ...mockAgentCard, - url: undefined as any - }; - - const result = await liveTester.testAgent(agentWithoutUrl); - - expect(result.success).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0]).toContain('does not specify a valid endpoint URL'); - }); - }); -}); diff --git a/src/__tests__/output.test.ts b/src/__tests__/output.test.ts deleted file mode 100644 index 5f1bec9..0000000 --- a/src/__tests__/output.test.ts +++ /dev/null @@ -1,443 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { ConsoleOutput } from '../output/console'; -import { JsonOutput } from '../output/json'; -import type { ValidationResult, CLIOptions } from '../types'; - -// Mock console methods -const originalConsoleLog = console.log; -const originalConsoleError = console.error; - -describe('Output Formatters', () => { - let consoleOutput: ConsoleOutput; - let jsonOutput: JsonOutput; - let consoleLogs: string[]; - let consoleErrors: string[]; - - beforeEach(() => { - consoleOutput = new ConsoleOutput(); - jsonOutput = new JsonOutput(); - consoleLogs = []; - consoleErrors = []; - - // Mock console methods - console.log = vi.fn((...args) => { - consoleLogs.push(args.join(' ')); - }); - console.error = vi.fn((...args) => { - consoleErrors.push(args.join(' ')); - }); - }); - - afterEach(() => { - // Restore console methods - console.log = originalConsoleLog; - console.error = originalConsoleError; - vi.clearAllMocks(); - }); - - const successResult: ValidationResult = { - success: true, - score: 100, - errors: [], - warnings: [], - suggestions: [], - validations: [ - { - id: 'schema_validation', - name: 'Schema Validation', - status: 'passed', - message: 'Agent card conforms to A2A v0.3.0 schema', - duration: 12, - details: 'Agent card structure is valid' - }, - { - id: 'v030_features', - name: 'A2A v0.3.0 Features', - status: 'passed', - message: 'All v0.3.0 features are properly configured', - duration: 5, - details: 'Validation of v0.3.0 specific features and capabilities' - } - ], - versionInfo: { - detectedVersion: '0.3.0', - validatorVersion: '0.3.0', - strictness: 'progressive', - compatibility: { - detectedVersion: '0.3.0', - targetVersion: '0.3.0', - compatible: true, - mismatches: [], - suggestions: [] - }, - migrationPath: [] - } - }; - - const failureResult: ValidationResult = { - success: false, - score: 65, - errors: [ - { - code: 'SCHEMA_VALIDATION_ERROR', - message: 'url: Invalid URL format', - field: 'url', - severity: 'error', - fixable: true - }, - { - code: 'VERSION_MISMATCH_ERROR', - message: 'Version format is invalid', - field: 'version', - severity: 'error', - fixable: true - } - ], - warnings: [ - { - code: 'LEGACY_DISCOVERY_ENDPOINT', - message: 'Using legacy discovery endpoint', - field: 'discovery', - severity: 'warning', - fixable: true - } - ], - suggestions: [ - { - id: 'migrate_legacy_endpoint', - message: 'Consider migrating to new endpoint format', - severity: 'info', - impact: 'Future compatibility', - fixable: true - } - ], - validations: [ - { - id: 'schema_validation', - name: 'Schema Validation', - status: 'failed', - message: 'Schema validation failed with 2 errors', - duration: 8, - details: 'Agent card does not conform to A2A v0.3.0 schema' - }, - { - id: 'v030_features', - name: 'A2A v0.3.0 Features', - status: 'passed', - message: 'All v0.3.0 features are properly configured', - duration: 3, - details: 'Validation of v0.3.0 specific features and capabilities' - } - ], - versionInfo: { - detectedVersion: '0.3.0', - validatorVersion: '0.3.0', - strictness: 'progressive', - compatibility: { - detectedVersion: '0.3.0', - targetVersion: '0.3.0', - compatible: false, - mismatches: [], - suggestions: [] - }, - migrationPath: [] - } - }; - - describe('ConsoleOutput', () => { - describe('Successful validation', () => { - it('should display success header', () => { - const options: CLIOptions = {}; - consoleOutput.display(successResult, 'test-agent.json', options); - - const output = consoleLogs.join('\n'); - expect(output).toContain('✅ A2A AGENT VALIDATION PASSED'); - expect(output).toContain('Agent: test-agent.json'); - // Scoring is tested in integration tests, not needed here - expect(output).toContain('Version: 0.3.0 (Strictness: progressive)'); - }); - - it('should display validation summary', () => { - const options: CLIOptions = {}; - consoleOutput.display(successResult, 'test-agent.json', options); - - const output = consoleLogs.join('\n'); - expect(output).toContain('VALIDATION SUMMARY:'); - expect(output).toContain('2 checks performed'); - expect(output).toContain('passed'); - expect(output).toContain('failed'); - expect(output).toContain('warnings'); - expect(output).toContain('Completed in 17ms'); - }); - - it('should display validation details', () => { - const options: CLIOptions = {}; - consoleOutput.display(successResult, 'test-agent.json', options); - - const output = consoleLogs.join('\n'); - expect(output).toContain('VALIDATIONS PERFORMED:'); - expect(output).toContain('✅ Schema Validation'); - expect(output).toContain('Agent card structure is valid'); - expect(output).toContain('Duration: 12ms'); - expect(output).toContain('✅ A2A v0.3.0 Features'); - }); - - it('should display success message', () => { - const options: CLIOptions = {}; - consoleOutput.display(successResult, 'test-agent.json', options); - - const output = consoleLogs.join('\n'); - expect(output).toContain('🏆 Perfect! Your agent passes all validations.'); - expect(output).toContain('🚀 Your agent is ready for deployment!'); - }); - }); - - describe('Failed validation', () => { - it('should display failure header', () => { - const options: CLIOptions = {}; - consoleOutput.display(failureResult, 'test-agent.json', options); - - const output = consoleLogs.join('\n'); - expect(output).toContain('❌ A2A AGENT VALIDATION FAILED'); - // Scoring is tested in integration tests, not needed here - }); - - it('should display errors', () => { - const options: CLIOptions = {}; - consoleOutput.display(failureResult, 'test-agent.json', options); - - const output = consoleLogs.join('\n'); - expect(output).toContain('ERRORS FOUND (2):'); - expect(output).toContain('❌ SCHEMA_VALIDATION_ERROR: url: Invalid URL format'); - expect(output).toContain('Field: url'); - expect(output).toContain('❌ VERSION_MISMATCH_ERROR: Version format is invalid'); - expect(output).toContain('Field: version'); - }); - - it('should display warnings', () => { - const options: CLIOptions = {}; - consoleOutput.display(failureResult, 'test-agent.json', options); - - const output = consoleLogs.join('\n'); - expect(output).toContain('WARNINGS (1):'); - expect(output).toContain('⚠️ LEGACY_DISCOVERY_ENDPOINT: Using legacy discovery endpoint'); - }); - - it('should display suggestions', () => { - const options: CLIOptions = {}; - consoleOutput.display(failureResult, 'test-agent.json', options); - - const output = consoleLogs.join('\n'); - expect(output).toContain('SUGGESTIONS (1):'); - expect(output).toContain('💡 Consider migrating to new endpoint format'); - expect(output).toContain('Impact: Future compatibility'); - }); - - it('should display next steps', () => { - const options: CLIOptions = {}; - consoleOutput.display(failureResult, 'test-agent.json', options); - - const output = consoleLogs.join('\n'); - expect(output).toContain('NEXT STEPS:'); - expect(output).toContain('1. Fix the errors listed above'); - expect(output).toContain('2. Re-run validation to confirm fixes'); - }); - }); - - describe('Errors-only mode', () => { - it('should skip validation details in errors-only mode', () => { - const options: CLIOptions = { errorsOnly: true }; - consoleOutput.display(failureResult, 'test-agent.json', options); - - const output = consoleLogs.join('\n'); - expect(output).not.toContain('VALIDATIONS PERFORMED:'); - expect(output).toContain('ERRORS FOUND (2):'); - expect(output).toContain('WARNINGS (1):'); - }); - - it('should skip suggestions in errors-only mode', () => { - const options: CLIOptions = { errorsOnly: true }; - consoleOutput.display(failureResult, 'test-agent.json', options); - - const output = consoleLogs.join('\n'); - expect(output).not.toContain('SUGGESTIONS (1):'); - }); - }); - - describe('Version compatibility display', () => { - it('should show version compatibility issues', () => { - const incompatibleResult = { - ...failureResult, - versionInfo: { - ...failureResult.versionInfo!, - compatibility: { - detectedVersion: '0.2.0', - targetVersion: '0.3.0', - compatible: false, - mismatches: [{ - feature: 'capabilities.streaming', - requiredVersion: '0.3.0', - detectedVersion: '0.2.0', - severity: 'warning' as const, - description: 'Streaming requires v0.3.0+' - }], - suggestions: [] - } - } - }; - - const options: CLIOptions = {}; - consoleOutput.display(incompatibleResult, 'test-agent.json', options); - - const output = consoleLogs.join('\n'); - expect(output).toContain('Version Compatibility Issues: 1 detected'); - }); - }); - }); - - describe('JsonOutput', () => { - it('should output valid JSON for successful validation', () => { - const options: CLIOptions = {}; - jsonOutput.display(successResult, 'test-agent.json', options); - - expect(consoleLogs).toHaveLength(1); - const output = consoleLogs[0]; - expect(output).toBeDefined(); - - expect(() => JSON.parse(output!)).not.toThrow(); - const parsed = JSON.parse(output!); - - expect(parsed.success).toBe(true); - expect(parsed.score).toBe(100); - expect(parsed.errors).toHaveLength(0); - expect(parsed.warnings).toHaveLength(0); - expect(parsed.validations).toHaveLength(2); - expect(parsed.versionInfo).toBeDefined(); - }); - - it('should output valid JSON for failed validation', () => { - const options: CLIOptions = {}; - jsonOutput.display(failureResult, 'test-agent.json', options); - - expect(consoleLogs).toHaveLength(1); - const output = consoleLogs[0]; - expect(output).toBeDefined(); - - expect(() => JSON.parse(output!)).not.toThrow(); - const parsed = JSON.parse(output!); - - expect(parsed.success).toBe(false); - expect(parsed.score).toBe(65); - expect(parsed.errors).toHaveLength(2); - expect(parsed.warnings).toHaveLength(1); - expect(parsed.suggestions).toHaveLength(1); - expect(parsed.validations).toHaveLength(2); - }); - - it('should preserve all error details in JSON', () => { - const options: CLIOptions = {}; - jsonOutput.display(failureResult, 'test-agent.json', options); - - const output = consoleLogs[0]; - expect(output).toBeDefined(); - const parsed = JSON.parse(output!); - - expect(parsed.errors[0]).toEqual({ - code: 'SCHEMA_VALIDATION_ERROR', - message: 'url: Invalid URL format', - field: 'url', - severity: 'error', - fixable: true - }); - - expect(parsed.warnings[0]).toEqual({ - code: 'LEGACY_DISCOVERY_ENDPOINT', - message: 'Using legacy discovery endpoint', - field: 'discovery', - severity: 'warning', - fixable: true - }); - }); - - it('should include complete validation details', () => { - const options: CLIOptions = {}; - jsonOutput.display(successResult, 'test-agent.json', options); - - const output = consoleLogs[0]; - expect(output).toBeDefined(); - const parsed = JSON.parse(output!); - - expect(parsed.validations[0]).toEqual({ - id: 'schema_validation', - name: 'Schema Validation', - status: 'passed', - message: 'Agent card conforms to A2A v0.3.0 schema', - duration: 12, - details: 'Agent card structure is valid' - }); - }); - - it('should format JSON with proper indentation', () => { - const options: CLIOptions = {}; - jsonOutput.display(successResult, 'test-agent.json', options); - - const output = consoleLogs[0]; - expect(output).toContain('\n "success": true'); - expect(output).toContain('\n "score": 100'); - }); - }); - - describe('Edge cases', () => { - it('should handle empty validation results', () => { - const emptyResult: ValidationResult = { - success: false, - score: 0, - errors: [], - warnings: [], - suggestions: [], - validations: [] - }; - - const options: CLIOptions = {}; - - expect(() => { - consoleOutput.display(emptyResult, 'empty.json', options); - }).not.toThrow(); - - expect(() => { - jsonOutput.display(emptyResult, 'empty.json', options); - }).not.toThrow(); - }); - - it('should handle missing version info', () => { - const noVersionResult: ValidationResult = { - success: true, - score: 90, - errors: [], - warnings: [], - suggestions: [], - validations: [] - }; - - const options: CLIOptions = {}; - - expect(() => { - consoleOutput.display(noVersionResult, 'no-version.json', options); - }).not.toThrow(); - - const output = consoleLogs.join('\n'); - // Just verify it doesn't crash without version info - expect(output).toContain('✅ A2A AGENT VALIDATION PASSED'); - }); - - it('should handle long agent paths', () => { - const longPath = 'very/long/path/to/agent/card/file/that/might/be/truncated/agent-card.json'; - const options: CLIOptions = {}; - - consoleOutput.display(successResult, longPath, options); - - const output = consoleLogs.join('\n'); - expect(output).toContain(`Agent: ${longPath}`); - }); - }); -}); \ No newline at end of file diff --git a/src/__tests__/runtime-validators.test.ts b/src/__tests__/runtime-validators.test.ts deleted file mode 100644 index 7b5fc11..0000000 --- a/src/__tests__/runtime-validators.test.ts +++ /dev/null @@ -1,528 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - validateTask, - validateStatusUpdate, - validateArtifactUpdate, - validateMessage, - validateRuntimeMessage, - type RuntimeValidationResult -} from '../validator/runtime-validators'; - -describe('Runtime Message Validators', () => { - describe('validateTask', () => { - it('should validate a valid Task message', () => { - const validTask = { - id: 'task-123', - status: { - state: 'working' - } - }; - - const result = validateTask(validTask); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should fail when id is missing', () => { - const invalidTask = { - status: { - state: 'working' - } - }; - - const result = validateTask(invalidTask); - - expect(result.valid).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0]?.code).toBe('TASK_MISSING_ID'); - expect(result.errors[0]?.field).toBe('id'); - }); - - it('should fail when id is not a string', () => { - const invalidTask = { - id: 123, - status: { - state: 'working' - } - }; - - const result = validateTask(invalidTask); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'TASK_MISSING_ID')).toBe(true); - }); - - it('should fail when status is missing', () => { - const invalidTask = { - id: 'task-123' - }; - - const result = validateTask(invalidTask); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'TASK_MISSING_STATUS')).toBe(true); - }); - - it('should fail when status.state is missing', () => { - const invalidTask = { - id: 'task-123', - status: {} - }; - - const result = validateTask(invalidTask); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'TASK_MISSING_STATUS_STATE')).toBe(true); - expect(result.errors[0]?.field).toBe('status.state'); - }); - - it('should fail when status.state is not a string', () => { - const invalidTask = { - id: 'task-123', - status: { - state: 123 - } - }; - - const result = validateTask(invalidTask); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'TASK_MISSING_STATUS_STATE')).toBe(true); - }); - - it('should fail with multiple errors when multiple fields are missing', () => { - const invalidTask = {}; - - const result = validateTask(invalidTask); - - expect(result.valid).toBe(false); - expect(result.errors.length).toBeGreaterThan(1); - expect(result.errors.some(e => e.code === 'TASK_MISSING_ID')).toBe(true); - expect(result.errors.some(e => e.code === 'TASK_MISSING_STATUS')).toBe(true); - }); - }); - - describe('validateStatusUpdate', () => { - it('should validate a valid StatusUpdate message', () => { - const validStatusUpdate = { - status: { - state: 'completed' - } - }; - - const result = validateStatusUpdate(validStatusUpdate); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should fail when status is missing', () => { - const invalidStatusUpdate = {}; - - const result = validateStatusUpdate(invalidStatusUpdate); - - expect(result.valid).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0]?.code).toBe('STATUS_UPDATE_MISSING_STATUS'); - expect(result.errors[0]?.field).toBe('status'); - }); - - it('should fail when status.state is missing', () => { - const invalidStatusUpdate = { - status: {} - }; - - const result = validateStatusUpdate(invalidStatusUpdate); - - expect(result.valid).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0]?.code).toBe('STATUS_UPDATE_MISSING_STATE'); - expect(result.errors[0]?.field).toBe('status.state'); - }); - - it('should fail when status is not an object', () => { - const invalidStatusUpdate = { - status: 'completed' - }; - - const result = validateStatusUpdate(invalidStatusUpdate); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'STATUS_UPDATE_MISSING_STATUS')).toBe(true); - }); - - it('should fail when status.state is not a string', () => { - const invalidStatusUpdate = { - status: { - state: true - } - }; - - const result = validateStatusUpdate(invalidStatusUpdate); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'STATUS_UPDATE_MISSING_STATE')).toBe(true); - }); - }); - - describe('validateArtifactUpdate', () => { - it('should validate a valid ArtifactUpdate message', () => { - const validArtifactUpdate = { - artifact: { - parts: [ - { type: 'text', content: 'Hello' } - ] - } - }; - - const result = validateArtifactUpdate(validArtifactUpdate); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should fail when artifact is missing', () => { - const invalidArtifactUpdate = {}; - - const result = validateArtifactUpdate(invalidArtifactUpdate); - - expect(result.valid).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0]?.code).toBe('ARTIFACT_UPDATE_MISSING_ARTIFACT'); - expect(result.errors[0]?.field).toBe('artifact'); - }); - - it('should fail when artifact.parts is missing', () => { - const invalidArtifactUpdate = { - artifact: {} - }; - - const result = validateArtifactUpdate(invalidArtifactUpdate); - - expect(result.valid).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0]?.code).toBe('ARTIFACT_MISSING_PARTS_ARRAY'); - expect(result.errors[0]?.field).toBe('artifact.parts'); - }); - - it('should fail when artifact.parts is not an array', () => { - const invalidArtifactUpdate = { - artifact: { - parts: 'not-an-array' - } - }; - - const result = validateArtifactUpdate(invalidArtifactUpdate); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'ARTIFACT_MISSING_PARTS_ARRAY')).toBe(true); - }); - - it('should fail when artifact.parts is an empty array', () => { - const invalidArtifactUpdate = { - artifact: { - parts: [] - } - }; - - const result = validateArtifactUpdate(invalidArtifactUpdate); - - expect(result.valid).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0]?.code).toBe('ARTIFACT_EMPTY_PARTS'); - expect(result.errors[0]?.field).toBe('artifact.parts'); - }); - - it('should validate artifact with multiple parts', () => { - const validArtifactUpdate = { - artifact: { - parts: [ - { type: 'text', content: 'Part 1' }, - { type: 'text', content: 'Part 2' } - ] - } - }; - - const result = validateArtifactUpdate(validArtifactUpdate); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - }); - - describe('validateMessage', () => { - it('should validate a valid Message', () => { - const validMessage = { - role: 'agent', - parts: [ - { type: 'text', content: 'Hello' } - ] - }; - - const result = validateMessage(validMessage); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should fail when parts is missing', () => { - const invalidMessage = { - role: 'agent' - }; - - const result = validateMessage(invalidMessage); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'MESSAGE_MISSING_PARTS_ARRAY')).toBe(true); - }); - - it('should fail when parts is not an array', () => { - const invalidMessage = { - role: 'agent', - parts: 'not-an-array' - }; - - const result = validateMessage(invalidMessage); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'MESSAGE_MISSING_PARTS_ARRAY')).toBe(true); - }); - - it('should fail when parts is an empty array', () => { - const invalidMessage = { - role: 'agent', - parts: [] - }; - - const result = validateMessage(invalidMessage); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'MESSAGE_EMPTY_PARTS')).toBe(true); - expect(result.errors[0]?.field).toBe('parts'); - }); - - it('should fail when role is missing', () => { - const invalidMessage = { - parts: [ - { type: 'text', content: 'Hello' } - ] - }; - - const result = validateMessage(invalidMessage); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'MESSAGE_MISSING_ROLE')).toBe(true); - }); - - it('should fail when role is not "agent"', () => { - const invalidMessage = { - role: 'user', - parts: [ - { type: 'text', content: 'Hello' } - ] - }; - - const result = validateMessage(invalidMessage); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'MESSAGE_INVALID_ROLE')).toBe(true); - expect(result.errors[0]?.field).toBe('role'); - }); - - it('should fail with multiple errors when multiple fields are invalid', () => { - const invalidMessage = { - role: 'user', - parts: [] - }; - - const result = validateMessage(invalidMessage); - - expect(result.valid).toBe(false); - expect(result.errors.length).toBeGreaterThan(1); - expect(result.errors.some(e => e.code === 'MESSAGE_EMPTY_PARTS')).toBe(true); - expect(result.errors.some(e => e.code === 'MESSAGE_INVALID_ROLE')).toBe(true); - }); - - it('should validate message with multiple parts', () => { - const validMessage = { - role: 'agent', - parts: [ - { type: 'text', content: 'Part 1' }, - { type: 'text', content: 'Part 2' } - ] - }; - - const result = validateMessage(validMessage); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - }); - - describe('validateRuntimeMessage', () => { - it('should validate a Task message by kind', () => { - const message = { - kind: 'task', - id: 'task-123', - status: { - state: 'working' - } - }; - - const result = validateRuntimeMessage(message); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should validate a StatusUpdate message by kind', () => { - const message = { - kind: 'status-update', - status: { - state: 'completed' - } - }; - - const result = validateRuntimeMessage(message); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should validate an ArtifactUpdate message by kind', () => { - const message = { - kind: 'artifact-update', - artifact: { - parts: [{ type: 'text', content: 'Result' }] - } - }; - - const result = validateRuntimeMessage(message); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should validate a Message by kind', () => { - const message = { - kind: 'message', - role: 'agent', - parts: [{ type: 'text', content: 'Hello' }] - }; - - const result = validateRuntimeMessage(message); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should fail when kind is missing', () => { - const message = { - role: 'agent', - parts: [{ type: 'text', content: 'Hello' }] - }; - - const result = validateRuntimeMessage(message); - - expect(result.valid).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0]?.code).toBe('MESSAGE_MISSING_KIND'); - expect(result.errors[0]?.field).toBe('kind'); - }); - - it('should fail for unknown message kind', () => { - const message = { - kind: 'unknown-kind', - data: {} - }; - - const result = validateRuntimeMessage(message); - - expect(result.valid).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0]?.code).toBe('MESSAGE_UNKNOWN_KIND'); - expect(result.errors[0]?.message).toContain('unknown-kind'); - }); - - it('should handle case-insensitive kind matching', () => { - const message = { - kind: 'TASK', - id: 'task-123', - status: { - state: 'working' - } - }; - - const result = validateRuntimeMessage(message); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should propagate validation errors from specific validators', () => { - const message = { - kind: 'task', - id: 'task-123' - // missing status - }; - - const result = validateRuntimeMessage(message); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'TASK_MISSING_STATUS')).toBe(true); - }); - }); - - describe('Edge Cases', () => { - it('should handle null input gracefully', () => { - const result = validateRuntimeMessage(null); - - expect(result.valid).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should handle undefined input gracefully', () => { - const result = validateRuntimeMessage(undefined); - - expect(result.valid).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should handle empty object', () => { - const result = validateRuntimeMessage({}); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'MESSAGE_MISSING_KIND')).toBe(true); - }); - - it('should handle kind as non-string', () => { - const message = { - kind: 123, - data: {} - }; - - const result = validateRuntimeMessage(message); - - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.code === 'MESSAGE_MISSING_KIND')).toBe(true); - }); - - it('should validate all error fields have required properties', () => { - const invalidMessage = { - kind: 'task' - }; - - const result = validateRuntimeMessage(invalidMessage); - - result.errors.forEach(error => { - expect(error).toHaveProperty('code'); - expect(error).toHaveProperty('message'); - expect(error).toHaveProperty('severity'); - expect(error.severity).toBe('error'); - expect(typeof error.code).toBe('string'); - expect(typeof error.message).toBe('string'); - }); - }); - }); -}); diff --git a/src/__tests__/scoring/integration.test.ts b/src/__tests__/scoring/integration.test.ts deleted file mode 100644 index 0701b2b..0000000 --- a/src/__tests__/scoring/integration.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Integration tests for the scoring system - */ - -import { describe, it, expect } from 'vitest'; -import { calculateScores, createScoringContext } from '../../scoring/index.js'; -import type { AgentCard } from '../../types/index.js'; - -describe('Scoring System Integration', () => { - const perfectAgentCard: AgentCard = { - protocolVersion: '0.3.0', - name: 'Perfect Agent', - description: 'A fully compliant agent', - url: 'https://example.com/agent', - version: '1.0.0', - capabilities: { - streaming: true, - pushNotifications: false, - }, - defaultInputModes: ['text/plain'], - defaultOutputModes: ['text/plain', 'application/json'], - skills: [ - { - id: 'skill-1', - name: 'Test Skill', - description: 'A test skill', - tags: ['test', 'demo'], - }, - ], - provider: { - organization: 'Test Org', - url: 'https://test.org', - }, - securitySchemes: { - oauth2: { - type: 'oauth2', - flows: { - authorizationCode: { - authorizationUrl: 'https://auth.example.com/authorize', - tokenUrl: 'https://auth.example.com/token', - scopes: { read: 'Read access' }, - }, - }, - }, - }, - documentationUrl: 'https://docs.example.com', - }; - - it('should calculate perfect compliance score (100/100)', () => { - const context = createScoringContext({ schemaOnly: true }); - const result = calculateScores( - { - agentCard: perfectAgentCard, - validationErrors: [], - }, - context - ); - - expect(result.compliance.total).toBe(100); - expect(result.compliance.rating).toBe('Perfect'); - expect(result.compliance.issues).toHaveLength(0); - }); - - it('should apply trust confidence multiplier without signatures', () => { - const context = createScoringContext({ schemaOnly: true }); - const result = calculateScores( - { - agentCard: perfectAgentCard, - validationErrors: [], - }, - context - ); - - // Without signatures, confidence multiplier should be 0.6 - expect(result.trust.confidenceMultiplier).toBe(0.6); - // Raw score should be decent (provider + security + docs) - expect(result.trust.rawScore).toBeGreaterThanOrEqual(40); - // But final score should be reduced by multiplier - expect(result.trust.total).toBeLessThan(result.trust.rawScore); - expect(result.trust.issues).toContain( - 'Trust confidence reduced (0.6x) - no cryptographic verification' - ); - }); - - it('should handle missing required fields', () => { - const incompleteCard: any = { - name: 'Incomplete Agent', - version: '1.0.0', - // Missing many required fields - }; - - const context = createScoringContext({ schemaOnly: true }); - const result = calculateScores( - { - agentCard: incompleteCard, - validationErrors: [], - }, - context - ); - - expect(result.compliance.total).toBeLessThan(50); - expect(result.compliance.issues.length).toBeGreaterThan(0); - expect(result.compliance.issues[0]).toContain('Missing required fields'); - }); - - it('should not test availability in schema-only mode', () => { - const context = createScoringContext({ schemaOnly: true }); - const result = calculateScores( - { - agentCard: perfectAgentCard, - validationErrors: [], - }, - context - ); - - expect(result.availability.tested).toBe(false); - expect(result.availability.total).toBeNull(); - expect(result.availability.rating).toBeNull(); - expect(result.availability.notTestedReason).toContain('Schema-only validation'); - }); - - it('should calculate legacy score for backward compatibility', () => { - const context = createScoringContext({ schemaOnly: true }); - const result = calculateScores( - { - agentCard: perfectAgentCard, - validationErrors: [], - }, - context - ); - - // Verify all three scores are present - expect(result.compliance).toBeDefined(); - expect(result.trust).toBeDefined(); - expect(result.availability).toBeDefined(); - expect(result.recommendation).toBeDefined(); - }); - - it('should generate appropriate recommendations', () => { - const context = createScoringContext({ schemaOnly: true }); - const result = calculateScores( - { - agentCard: perfectAgentCard, - validationErrors: [], - }, - context - ); - - // Should mention compliance (either "Fully compliant" or "Excellent") - expect( - result.recommendation.includes('Fully A2A v0.3.0 compliant') || - result.recommendation.includes('Excellent A2A compliance') - ).toBe(true); - expect(result.recommendation).toContain('signatures'); - }); - - it('should penalize invalid MIME types', () => { - const badCard: AgentCard = { - ...perfectAgentCard, - defaultInputModes: ['invalid-mime-type'], - }; - - const context = createScoringContext({ schemaOnly: true }); - const result = calculateScores( - { - agentCard: badCard, - validationErrors: [], - }, - context - ); - - expect(result.compliance.total).toBeLessThan(100); - expect(result.compliance.breakdown.formatCompliance.details.validMimeTypes).toBe( - false - ); - }); - - it('should penalize missing skill tags', () => { - const noTagsCard: AgentCard = { - ...perfectAgentCard, - skills: [ - { - id: 'skill-1', - name: 'Test Skill', - description: 'A test skill', - tags: [], // Empty tags - }, - ], - }; - - const context = createScoringContext({ schemaOnly: true }); - const result = calculateScores( - { - agentCard: noTagsCard, - validationErrors: [], - }, - context - ); - - expect(result.compliance.breakdown.skillsQuality.details.allSkillsHaveTags).toBe( - false - ); - expect(result.compliance.total).toBeLessThan(100); - }); - - it('should penalize HTTP URLs in security score', () => { - const httpCard: AgentCard = { - ...perfectAgentCard, - url: 'http://insecure.example.com', - }; - - const context = createScoringContext({ schemaOnly: true }); - const result = calculateScores( - { - agentCard: httpCard, - validationErrors: [], - }, - context - ); - - expect(result.trust.breakdown.security.details.httpsOnly).toBe(false); - expect(result.trust.breakdown.security.details.hasHttpUrls).toBe(true); - }); -}); diff --git a/src/__tests__/signature-verification-integration.test.ts b/src/__tests__/signature-verification-integration.test.ts deleted file mode 100644 index b7e1b62..0000000 --- a/src/__tests__/signature-verification-integration.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { A2AValidator } from '../validator/a2a-validator'; -import type { AgentCard, HttpClient } from '../types'; - -describe('Signature Verification Integration', () => { - let validator: A2AValidator; - let mockHttpClient: HttpClient; - - beforeEach(() => { - // Create mock HTTP client - mockHttpClient = { - get: vi.fn() - }; - - validator = new A2AValidator(mockHttpClient); - }); - - const createTestAgentCard = (signatures?: any[]): AgentCard => ({ - protocolVersion: '0.3.0', - name: 'Test Agent', - description: 'A test agent for signature verification', - url: 'https://example.com/agent', - preferredTransport: 'HTTP+JSON', - provider: { - organization: 'Test Corp', - url: 'https://testcorp.com' - }, - version: '1.0.0', - capabilities: { - streaming: false, - pushNotifications: false - }, - defaultInputModes: ['text/plain'], - defaultOutputModes: ['text/plain'], - skills: [ - { - id: 'test-skill', - name: 'Test Skill', - description: 'A test skill', - tags: ['test'] - } - ], - ...(signatures && { signatures }) - }); - - describe('Signature Verification in Validation Pipeline', () => { - it('should include signature verification check by default', async () => { - const agentCard = createTestAgentCard(); - - const result = await validator.validate(agentCard, { skipDynamic: true }); - - // Should always include signature verification check - expect(result.validations).toBeDefined(); - const sigCheck = result.validations.find(v => v.id === 'signature_verification'); - expect(sigCheck).toBeDefined(); - expect(sigCheck?.name).toBe('JWS Signature Verification'); - }); - - it('should skip signature verification when no signatures present', async () => { - const agentCard = createTestAgentCard(); - - const result = await validator.validate(agentCard, { skipDynamic: true }); - - const sigCheck = result.validations.find(v => v.id === 'signature_verification'); - expect(sigCheck?.status).toBe('skipped'); - expect(sigCheck?.message).toBe('No signatures found to verify'); - - // Should include warning about missing signatures - expect(result.warnings.some(w => w.code === 'NO_SIGNATURES_FOUND')).toBe(true); - }); - - it('should skip signature verification when explicitly disabled', async () => { - const agentCard = createTestAgentCard([{ signature: 'test.signature' }]); - - const result = await validator.validate(agentCard, { - skipDynamic: true, - skipSignatureVerification: true - }); - - const sigCheck = result.validations.find(v => v.id === 'signature_verification'); - expect(sigCheck?.status).toBe('skipped'); - expect(sigCheck?.message).toBe('Signature verification was explicitly skipped'); - - // Should include warning about skipped verification - expect(result.warnings.some(w => w.code === 'SIGNATURE_VERIFICATION_SKIPPED')).toBe(true); - }); - - it('should attempt verification when signatures are present', async () => { - const agentCard = createTestAgentCard([ - { signature: 'invalid.test.signature' } - ]); - - const result = await validator.validate(agentCard, { skipDynamic: true }); - - const sigCheck = result.validations.find(v => v.id === 'signature_verification'); - expect(sigCheck?.status).toBe('failed'); // Should fail due to invalid signature - expect(sigCheck?.message).toContain('0 of 1 signatures verified'); - - // Should include specific signature verification errors - expect(result.errors.some(e => e.code === 'SIGNATURE_VERIFICATION_FAILED')).toBe(true); - }); - - it('should not affect other validations when signature verification fails', async () => { - const agentCard = createTestAgentCard([ - { signature: 'invalid.signature' } - ]); - - const result = await validator.validate(agentCard, { skipDynamic: true }); - - // Schema validation should still pass - const schemaCheck = result.validations.find(v => v.id === 'schema_validation'); - expect(schemaCheck?.status).toBe('passed'); - - // Version features should still pass - const versionCheck = result.validations.find(v => v.id === 'v030_features'); - expect(versionCheck?.status).toBe('passed'); - - // Only signature verification should fail - const sigCheck = result.validations.find(v => v.id === 'signature_verification'); - expect(sigCheck?.status).toBe('failed'); - }); - - it('should include signature verification timing information', async () => { - const agentCard = createTestAgentCard(); - - const result = await validator.validate(agentCard, { skipDynamic: true }); - - const sigCheck = result.validations.find(v => v.id === 'signature_verification'); - expect(sigCheck?.duration).toBeDefined(); - expect(typeof sigCheck?.duration).toBe('number'); - expect(sigCheck?.duration).toBeGreaterThanOrEqual(0); - }); - - it('should affect validation score when signatures missing', async () => { - const agentCardWithoutSigs = createTestAgentCard(); - const agentCardSkippingAllChecks = createTestAgentCard(); - - const resultWithoutSigs = await validator.validate(agentCardWithoutSigs, { - skipDynamic: true - }); - - const resultSkippingAllChecks = await validator.validate(agentCardSkippingAllChecks, { - skipDynamic: true, - skipSignatureVerification: true - }); - - // Missing signatures should result in warnings that affect the score - expect(resultWithoutSigs.score).toBeLessThan(100); - expect(resultWithoutSigs.warnings.some(w => w.code === 'NO_SIGNATURES_FOUND')).toBe(true); - - // Skipping verification should have no signature-related warnings - expect(resultSkippingAllChecks.warnings.some(w => w.code === 'NO_SIGNATURES_FOUND')).toBe(false); - expect(resultSkippingAllChecks.warnings.some(w => w.code === 'SIGNATURE_VERIFICATION_SKIPPED')).toBe(false); - }); - }); - - describe('Security Warnings and Messages', () => { - it('should provide helpful warning when no signatures present', async () => { - const agentCard = createTestAgentCard(); - - const result = await validator.validate(agentCard, { skipDynamic: true }); - - const warning = result.warnings.find(w => w.code === 'NO_SIGNATURES_FOUND'); - expect(warning).toBeDefined(); - expect(warning?.message).toContain('Consider adding signatures to improve trust'); - expect(warning?.severity).toBe('warning'); - expect(warning?.fixable).toBe(true); - }); - - it('should warn when signature verification is skipped', async () => { - const agentCard = createTestAgentCard([{ signature: 'test.sig' }]); - - const result = await validator.validate(agentCard, { - skipDynamic: true, - skipSignatureVerification: true - }); - - const warning = result.warnings.find(w => w.code === 'SIGNATURE_VERIFICATION_SKIPPED'); - expect(warning).toBeDefined(); - expect(warning?.message).toContain('reduces trust verification'); - expect(warning?.severity).toBe('warning'); - }); - - it('should provide specific error details for failed signatures', async () => { - const agentCard = createTestAgentCard([ - { signature: 'malformed.signature.here' } - ]); - - const result = await validator.validate(agentCard, { skipDynamic: true }); - - const error = result.errors.find(e => e.code === 'SIGNATURE_VERIFICATION_FAILED'); - expect(error).toBeDefined(); - expect(error?.message).toContain('Signature 1 verification failed'); - expect(error?.field).toBe('signatures[0]'); - expect(error?.severity).toBe('error'); - }); - }); -}); \ No newline at end of file diff --git a/src/__tests__/validate-wrapper.test.ts b/src/__tests__/validate-wrapper.test.ts deleted file mode 100644 index 4ee61bb..0000000 --- a/src/__tests__/validate-wrapper.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { ValidateCommand } from '../commands/validate'; -import { BinaryManager } from '../utils/binary-manager'; -import execa from 'execa'; - -// Mock dependencies -vi.mock('../utils/binary-manager'); -vi.mock('execa'); - -describe('ValidateCommand Wrapper', () => { - const mockBinaryPath = '/path/to/capiscio-core'; - const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); - const mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); - - beforeEach(() => { - vi.clearAllMocks(); - - // Setup BinaryManager mock - (BinaryManager.getInstance as any).mockReturnValue({ - getBinaryPath: vi.fn().mockResolvedValue(mockBinaryPath) - }); - - // Setup execa mock - (execa as any).mockResolvedValue({ exitCode: 0 }); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should execute binary with basic arguments', async () => { - await ValidateCommand.execute('agent.json', {}); - - expect(BinaryManager.getInstance).toHaveBeenCalled(); - expect(execa).toHaveBeenCalledWith( - mockBinaryPath, - ['validate', 'agent.json'], - expect.objectContaining({ stdio: 'inherit' }) - ); - expect(mockExit).toHaveBeenCalledWith(0); - }); - - it('should pass --strict flag', async () => { - await ValidateCommand.execute('agent.json', { strict: true }); - - expect(execa).toHaveBeenCalledWith( - mockBinaryPath, - ['validate', 'agent.json', '--strict'], - expect.any(Object) - ); - }); - - it('should pass --progressive flag', async () => { - await ValidateCommand.execute('agent.json', { progressive: true }); - - expect(execa).toHaveBeenCalledWith( - mockBinaryPath, - ['validate', 'agent.json', '--progressive'], - expect.any(Object) - ); - }); - - // it('should pass --conservative flag', async () => { - // await ValidateCommand.execute('agent.json', { conservative: true }); - - // expect(execa).toHaveBeenCalledWith( - // mockBinaryPath, - // ['validate', 'agent.json', '--conservative'], - // expect.any(Object) - // ); - // }); - - it('should pass multiple flags', async () => { - await ValidateCommand.execute('agent.json', { - strict: true, - json: true, - verbose: true - }); - - expect(execa).toHaveBeenCalledWith( - mockBinaryPath, - expect.arrayContaining(['--strict', '--json', '--verbose']), - expect.any(Object) - ); - }); - - it('should handle timeout option', async () => { - await ValidateCommand.execute('agent.json', { timeout: '5000' }); - - expect(execa).toHaveBeenCalledWith( - mockBinaryPath, - expect.arrayContaining(['--timeout', '5000ms']), - expect.any(Object) - ); - }); - - it('should handle execution errors', async () => { - const error = new Error('Execution failed'); - (execa as any).mockRejectedValue(error); - - await ValidateCommand.execute('agent.json', {}); - - expect(mockConsoleError).toHaveBeenCalled(); - expect(mockExit).toHaveBeenCalledWith(1); - }); - - it('should propagate non-zero exit code', async () => { - (execa as any).mockResolvedValue({ exitCode: 123 }); - - await ValidateCommand.execute('agent.json', {}); - - expect(mockExit).toHaveBeenCalledWith(123); - }); -}); diff --git a/src/__tests__/validator.test.ts b/src/__tests__/validator.test.ts deleted file mode 100644 index d462f19..0000000 --- a/src/__tests__/validator.test.ts +++ /dev/null @@ -1,469 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { A2AValidator } from '../validator/a2a-validator'; -import { FetchHttpClient } from '../validator/http-client'; -import type { AgentCard, ValidationOptions, HttpClient, HttpResponse } from '../types'; - -// Mock fetch for HTTP client tests -const mockFetch = vi.fn(); -global.fetch = mockFetch; - -describe('A2AValidator - Comprehensive Tests', () => { - let validator: A2AValidator; - let mockHttpClient: HttpClient; - - beforeEach(() => { - vi.clearAllMocks(); - - // Create mock HTTP client - mockHttpClient = { - get: vi.fn() - }; - - validator = new A2AValidator(mockHttpClient); - }); - - const validAgentCard: AgentCard = { - protocolVersion: '0.3.0', - name: 'Test Agent', - description: 'A comprehensive test agent for validation', - url: 'https://example.com/agent', - preferredTransport: 'HTTP+JSON', - provider: { - organization: 'Test Corp', - url: 'https://testcorp.com' - }, - version: '1.0.0', - capabilities: { - streaming: true, - pushNotifications: false - }, - defaultInputModes: ['text/plain', 'application/json'], - defaultOutputModes: ['text/plain', 'application/json'], - skills: [ - { - id: 'test-skill', - name: 'Test Skill', - description: 'A test skill', - tags: ['test', 'example'], - examples: ['Example 1', 'Example 2'] - } - ] - }; - - describe('Constructor and Instance Creation', () => { - it('should create an instance with default HTTP client', () => { - const defaultValidator = new A2AValidator(); - expect(defaultValidator).toBeDefined(); - expect(defaultValidator).toBeInstanceOf(A2AValidator); - }); - - it('should create an instance with custom HTTP client', () => { - expect(validator).toBeDefined(); - expect(validator).toBeInstanceOf(A2AValidator); - }); - }); - - describe('Schema Validation', () => { - it('should validate a complete valid agent card', async () => { - const result = await validator.validate(validAgentCard, { skipDynamic: true, skipSignatureVerification: true }); - - expect(result.success).toBe(true); - expect(result.errors).toHaveLength(0); - expect(result.score).toBe(100); - expect(result.validations).toBeDefined(); - expect(result.validations.length).toBeGreaterThan(0); - expect(result.validations.some(v => v.id === 'schema_validation')).toBe(true); - expect(result.versionInfo?.detectedVersion).toBe('0.3.0'); - }); - - it('should fail for missing required fields', async () => { - const incompleteCard = { - name: 'Incomplete Agent', - description: 'Missing required fields' - }; - - const result = await validator.validate(incompleteCard as any, { skipDynamic: true }); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - expect(result.score).toBeLessThan(100); - - const errorFields = result.errors.map(e => e.field); - // Check for required fields per official A2A spec - expect(errorFields).toContain('protocolVersion'); // Required - expect(errorFields).toContain('url'); // Required - expect(errorFields).toContain('version'); // Required - expect(errorFields).toContain('capabilities'); // Required - expect(errorFields).toContain('defaultInputModes'); // Required - expect(errorFields).toContain('defaultOutputModes'); // Required - expect(errorFields).toContain('skills'); // Required - // preferredTransport and provider are OPTIONAL per official A2A spec - expect(errorFields).not.toContain('preferredTransport'); // Optional (defaults to JSONRPC) - expect(errorFields).not.toContain('provider'); // Optional - }); - - it('should validate transport protocols', async () => { - const invalidTransport = { - ...validAgentCard, - preferredTransport: 'INVALID_TRANSPORT' as any - }; - - const result = await validator.validate(invalidTransport, { skipDynamic: true }); - - expect(result.success).toBe(false); - expect(result.errors.some(e => - e.field === 'preferredTransport' || - e.message.includes('preferredTransport') - )).toBe(true); - }); - - it('should validate URL formats', async () => { - const invalidUrlCard = { - ...validAgentCard, - url: 'not-a-valid-url' - }; - - const result = await validator.validate(invalidUrlCard, { skipDynamic: true }); - - expect(result.success).toBe(false); - expect(result.errors.some(e => e.field === 'url')).toBe(true); - }); - - it('should validate version format (semver)', async () => { - const invalidVersionCard = { - ...validAgentCard, - version: 'not-semver' - }; - - const result = await validator.validate(invalidVersionCard, { skipDynamic: true }); - - expect(result.success).toBe(false); - expect(result.errors.some(e => - e.field === 'version' && - e.message.includes('semver') - )).toBe(true); - }); - - it('should validate protocol version format', async () => { - const invalidProtocolVersion = { - ...validAgentCard, - protocolVersion: 'invalid-version' - }; - - const result = await validator.validate(invalidProtocolVersion, { skipDynamic: true }); - - expect(result.success).toBe(false); - expect(result.errors.some(e => - e.field === 'protocolVersion' - )).toBe(true); - }); - - it('should validate optional fields when present', async () => { - const extendedCard = { - ...validAgentCard, - iconUrl: 'https://example.com/icon.png', - documentationUrl: 'https://docs.example.com', - additionalInterfaces: [ - { - url: 'https://grpc.example.com', - transport: 'GRPC' as const - } - ] - }; - - const result = await validator.validate(extendedCard, { skipDynamic: true }); - - expect(result.success).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should fail for invalid optional URL fields', async () => { - const invalidOptionalUrls = { - ...validAgentCard, - iconUrl: 'not-a-url', - documentationUrl: 'also-not-a-url' - }; - - const result = await validator.validate(invalidOptionalUrls, { skipDynamic: true }); - - expect(result.success).toBe(false); - expect(result.errors.some(e => e.field === 'iconUrl')).toBe(true); - expect(result.errors.some(e => e.field === 'documentationUrl')).toBe(true); - }); - }); - - describe('Version Compatibility Analysis', () => { - it('should detect version compatibility issues', async () => { - const versionMismatchCard = { - ...validAgentCard, - protocolVersion: '0.2.0', - capabilities: { - streaming: true, // Requires 0.3.0+ - pushNotifications: true // Requires 0.3.0+ - } - }; - - const result = await validator.validate(versionMismatchCard, { skipDynamic: true }); - - expect(result.versionInfo?.compatibility?.compatible).toBe(false); - expect(result.versionInfo?.compatibility?.mismatches?.length).toBeGreaterThan(0); - }); - - it('should suggest migration path for incompatible versions', async () => { - const oldVersionCard = { - ...validAgentCard, - protocolVersion: '0.1.0' - }; - - const result = await validator.validate(oldVersionCard, { skipDynamic: true }); - - expect(result.versionInfo?.migrationPath?.length).toBeGreaterThan(0); - }); - - it('should handle missing protocol version', async () => { - const noVersionCard = { - ...validAgentCard - }; - delete (noVersionCard as any).protocolVersion; - - const result = await validator.validate(noVersionCard, { skipDynamic: true }); - - expect(result.versionInfo?.detectedVersion).toBe('undefined'); - expect(result.errors.some(e => e.field === 'protocolVersion')).toBe(true); - }); - }); - - describe('Network Validation', () => { - it('should fetch agent card from URL', async () => { - const mockResponse: HttpResponse = { - status: 200, - data: validAgentCard, - headers: { 'content-type': 'application/json' } - }; - - // Mock both the agent card fetch AND the endpoint connectivity test - (mockHttpClient.get as any) - .mockResolvedValueOnce(mockResponse) // For agent card fetch - .mockResolvedValueOnce(mockResponse); // For endpoint connectivity test - - // Mock fetch for transport endpoint testing - mockFetch.mockResolvedValue({ - status: 200, - headers: { get: () => 'application/json' }, - json: () => Promise.resolve({}) - }); - - const result = await validator.validate('https://example.com/agent.json'); - - expect(result.success).toBe(true); - expect(mockHttpClient.get).toHaveBeenCalledWith('https://example.com/agent.json'); - }); - - it('should try well-known endpoint when direct URL fails', async () => { - const mockError = new Error('Not found'); - const mockResponse: HttpResponse = { - status: 200, - data: validAgentCard, - headers: { 'content-type': 'application/json' } - }; - - (mockHttpClient.get as any) - .mockRejectedValueOnce(mockError) // Direct URL fails - .mockResolvedValueOnce(mockResponse) // Well-known endpoint succeeds - .mockResolvedValueOnce(mockResponse); // Endpoint connectivity test - - // Mock fetch for transport endpoint testing - mockFetch.mockResolvedValue({ - status: 200, - headers: { get: () => 'application/json' }, - json: () => Promise.resolve({}) - }); - - const result = await validator.validate('https://example.com'); - - expect(result.success).toBe(true); - expect(mockHttpClient.get).toHaveBeenCalledWith('https://example.com/.well-known/agent-card.json'); - }); - - it('should try legacy well-known endpoint as fallback', async () => { - const mockError = new Error('Not found'); - const mockResponse: HttpResponse = { - status: 200, - data: validAgentCard, - headers: { 'content-type': 'application/json' } - }; - - (mockHttpClient.get as any) - .mockRejectedValueOnce(mockError) // Direct URL fails - .mockRejectedValueOnce(mockError) // Well-known endpoint fails - .mockResolvedValueOnce(mockResponse) // Legacy endpoint succeeds - .mockResolvedValueOnce(mockResponse); // Endpoint connectivity test - - // Mock fetch for transport endpoint testing - mockFetch.mockResolvedValue({ - status: 200, - headers: { get: () => 'application/json' }, - json: () => Promise.resolve({}) - }); - - const result = await validator.validate('https://example.com'); - - expect(result.success).toBe(true); - expect(result.warnings.some(w => w.code === 'LEGACY_DISCOVERY_ENDPOINT')).toBe(true); - expect(mockHttpClient.get).toHaveBeenCalledWith('https://example.com/.well-known/agent.json'); - }); - - it('should handle network errors gracefully', async () => { - const networkError = new Error('Network Error'); - (mockHttpClient.get as any).mockRejectedValue(networkError); - - const result = await validator.validate('https://invalid.com'); - - expect(result.success).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors[0]?.message).toContain('Network Error'); - }); - }); - - describe('Validation Modes', () => { - it('should use progressive mode by default', async () => { - const result = await validator.validateProgressive(validAgentCard); - - expect(result.versionInfo?.strictness).toBe('progressive'); - expect(result.success).toBe(true); - }); - - it('should use strict mode', async () => { - const result = await validator.validateStrict(validAgentCard); - - expect(result.versionInfo?.strictness).toBe('strict'); - }); - - it('should use conservative mode', async () => { - const result = await validator.validateConservative(validAgentCard); - - expect(result.versionInfo?.strictness).toBe('conservative'); - }); - - it('should apply stricter validation in strict mode', async () => { - const cardWithWarnings = { - ...validAgentCard, - additionalInterfaces: [{ - url: 'https://grpc.example.com', - transport: 'GRPC' as const - }], - capabilities: { - streaming: false // GRPC without streaming should be error in strict mode - } - }; - - const progressiveResult = await validator.validateProgressive(cardWithWarnings, { skipDynamic: true }); - const strictResult = await validator.validateStrict(cardWithWarnings, { skipDynamic: true }); - - expect(progressiveResult.warnings.some(w => w.code === 'GRPC_WITHOUT_STREAMING')).toBe(true); - expect(strictResult.errors.length).toBeGreaterThan(progressiveResult.errors.length); - }); - }); - - describe('Schema-Only Validation', () => { - it('should skip network calls in schema-only mode', async () => { - const result = await validator.validateSchemaOnly(validAgentCard); - - expect(result.success).toBe(true); - expect(mockHttpClient.get).not.toHaveBeenCalled(); - }); - }); - - describe('Edge Cases and Error Handling', () => { - it('should handle null input', async () => { - const result = await validator.validate(null as any); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should handle undefined input', async () => { - const result = await validator.validate(undefined as any); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should handle empty object', async () => { - const result = await validator.validate({} as any, { skipDynamic: true }); - - expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); - - it('should handle malformed JSON from URL', async () => { - const mockResponse: HttpResponse = { - status: 200, - data: 'not-json', - headers: { 'content-type': 'text/plain' } - }; - - (mockHttpClient.get as any).mockResolvedValueOnce(mockResponse); - - const result = await validator.validate('https://example.com/agent.json'); - - expect(result.success).toBe(false); - }); - - it('should handle circular references in agent card', async () => { - const circularCard: any = { ...validAgentCard }; - circularCard.self = circularCard; - - const result = await validator.validate(circularCard, { skipDynamic: true }); - - // Should not crash, might succeed or fail gracefully - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - }); - }); - - describe('Score Calculation', () => { - it('should calculate score based on errors and warnings', async () => { - const cardWithIssues = { - ...validAgentCard, - url: 'not-a-valid-url', - version: 'not-semver' - }; - - const result = await validator.validate(cardWithIssues, { skipDynamic: true }); - - expect(result.score).toBeGreaterThan(0); - expect(result.score).toBeLessThan(100); - expect(result.score).toBeGreaterThanOrEqual(0); - expect(result.score).toBeLessThanOrEqual(100); - }); - - it('should return perfect score for valid agent card', async () => { - const result = await validator.validate(validAgentCard, { skipDynamic: true, skipSignatureVerification: true }); - - expect(result.score).toBe(100); - }); - - it('should return 0 score for completely invalid card', async () => { - const result = await validator.validate(null as any); - - expect(result.score).toBe(0); - }); - }); - - describe('Validation Timing', () => { - it('should include timing information in results', async () => { - const result = await validator.validate(validAgentCard, { skipDynamic: true }); - - expect(result.validations.some(v => v.duration !== undefined)).toBe(true); - }); - - it('should complete validation within reasonable time', async () => { - const start = Date.now(); - await validator.validate(validAgentCard, { skipDynamic: true }); - const duration = Date.now() - start; - - expect(duration).toBeLessThan(1000); // Should complete within 1 second - }); - }); -}); \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 815ff04..11ac3c6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,18 +1,66 @@ -import { Command } from 'commander'; import chalk from 'chalk'; -import { ValidateCommand } from './commands/validate'; +import execa from 'execa'; +import { BinaryManager } from './utils/binary-manager'; // Import version directly from package.json at build time import { version } from '../package.json'; -const program = new Command(); +/** + * Passthrough CLI - delegates all commands to capiscio-core binary. + * + * This wrapper manages the download and execution of the platform-specific + * capiscio-core binary, passing all arguments through transparently. + */ +async function main(): Promise { + const args = process.argv.slice(2); -program - .name('capiscio') - .description('The definitive CLI tool for validating A2A (Agent-to-Agent) protocol agent cards') - .version(version); + // Handle wrapper-specific maintenance commands + if (args.length > 0) { + if (args[0] === '--wrapper-version') { + console.log(`capiscio-node wrapper v${version}`); + process.exit(0); + } -// Register commands -ValidateCommand.register(program); + if (args[0] === '--wrapper-clean') { + const fs = await import('fs'); + const os = await import('os'); + const path = await import('path'); + + const cacheDir = path.join(os.homedir(), '.capiscio', 'bin'); + try { + if (fs.existsSync(cacheDir)) { + fs.rmSync(cacheDir, { recursive: true, force: true }); + console.log(chalk.green(`Cleaned cache directory: ${cacheDir}`)); + } else { + console.log(chalk.yellow('Cache directory does not exist.')); + } + process.exit(0); + } catch (error) { + console.error(chalk.red(`Failed to clean cache: ${error instanceof Error ? error.message : 'Unknown error'}`)); + process.exit(1); + } + } + } + + // Delegate everything to the core binary + try { + const binaryManager = BinaryManager.getInstance(); + const binaryPath = await binaryManager.getBinaryPath(); + + // Execute binary with all args passed through + // We inherit stdio so the binary's output goes directly to the user's terminal + const subprocess = execa(binaryPath, args, { + stdio: 'inherit', + reject: false // Don't throw on non-zero exit code, we handle it manually + }); + + const result = await subprocess; + process.exit(result.exitCode); + + } catch (error) { + console.error(chalk.red(`❌ Error executing CapiscIO Core: ${error instanceof Error ? error.message : 'Unknown error'}`)); + process.exit(1); + } +} // Global error handling process.on('uncaughtException', (error) => { @@ -25,4 +73,4 @@ process.on('unhandledRejection', (reason) => { process.exit(1); }); -program.parse(); \ No newline at end of file +main(); \ No newline at end of file diff --git a/src/commands/validate.ts b/src/commands/validate.ts deleted file mode 100644 index c35d941..0000000 --- a/src/commands/validate.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Command } from 'commander'; -import chalk from 'chalk'; -import execa from 'execa'; -import { BinaryManager } from '../utils/binary-manager'; -import { CLIOptions } from '../types'; - -export class ValidateCommand { - static register(program: Command): void { - program - .command('validate') - .description('Validate an A2A agent card') - .argument('[input]', 'Agent URL, file path, or auto-detect') - .option('--strict', 'Enable strict validation mode') - .option('--progressive', 'Enable progressive validation mode (default)') - // .option('--conservative', 'Enable conservative validation mode') - .option('--skip-signature', 'Skip JWS signature verification (not recommended)') - .option('--registry-ready', 'Check registry deployment readiness') - .option('--schema-only', 'Validate schema only, skip endpoint testing') - .option('--test-live', 'Test live agent endpoint by sending a message') - .option('--json', 'Output results in JSON format') - .option('--errors-only', 'Show only errors and warnings') - .option('--verbose', 'Show detailed validation steps and timing') - .option('--timeout ', 'Request timeout in milliseconds', '10000') - .option('--show-version', 'Display detailed version compatibility analysis') - .action(async (input, options) => { - await this.execute(input, options); - }); - } - - static async execute(input: string | undefined, options: CLIOptions): Promise { - try { - const binaryManager = BinaryManager.getInstance(); - const binaryPath = await binaryManager.getBinaryPath(); - - const args = ['validate']; - - if (input) { - args.push(input); - } else { - // Default to agent-card.json if no input provided - // This matches the behavior of the previous Node CLI - args.push('agent-card.json'); - } - - // Map flags - if (options.strict) args.push('--strict'); - if (options.progressive) args.push('--progressive'); - // Conservative mode is not supported in the core binary yet - // if (options.conservative) args.push('--conservative'); - if (options.skipSignature) args.push('--skip-signature'); - if (options.registryReady) args.push('--registry-ready'); - if (options.schemaOnly) args.push('--schema-only'); - if (options.testLive) args.push('--test-live'); - if (options.json) args.push('--json'); - if (options.errorsOnly) args.push('--errors-only'); - if (options.verbose) args.push('--verbose'); - - if (options.timeout) { - // Convert ms string to Go duration string (e.g. "10000" -> "10s") - const ms = parseInt(options.timeout); - if (!isNaN(ms)) { - args.push('--timeout', `${ms}ms`); - } - } - - // Execute binary - // We inherit stdio so the binary's output goes directly to the user's terminal - const subprocess = execa(binaryPath, args, { - stdio: 'inherit', - reject: false // Don't throw on non-zero exit code, we handle it manually - }); - - const result = await subprocess; - process.exit(result.exitCode); - - } catch (error) { - console.error(chalk.red(`❌ Error executing CapiscIO Core: ${error instanceof Error ? error.message : 'Unknown error'}`)); - process.exit(1); - } - } -} diff --git a/src/index.ts b/src/index.ts index e27bb09..513ec4c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,23 @@ -// Main exports for programmatic usage -export * from './types'; -export { A2AValidator } from './validator/a2a-validator'; -export { FetchHttpClient } from './validator/http-client'; -export { ValidateCommand } from './commands/validate'; -export { ConsoleOutput } from './output/console'; -export { JsonOutput } from './output/json'; \ No newline at end of file +/** + * CapiscIO CLI Package + * + * This package provides the CLI wrapper only. + * For programmatic SDK usage, use @capiscio/sdk (coming soon). + * + * @example CLI Usage + * ```bash + * # Install globally + * npm install -g capiscio + * + * # Validate an agent card + * capiscio validate ./agent-card.json + * + * # Issue a self-signed badge + * capiscio badge issue --self-sign + * ``` + */ + +export const version = '2.2.0'; + +// Re-export binary manager for advanced users who want to manage the binary +export { BinaryManager } from './utils/binary-manager'; \ No newline at end of file diff --git a/src/output/console.ts b/src/output/console.ts deleted file mode 100644 index 7cd7e13..0000000 --- a/src/output/console.ts +++ /dev/null @@ -1,248 +0,0 @@ -import chalk from 'chalk'; -import { ValidationResult, ValidationCheck, CLIOptions, LiveTestResult } from '../types'; - -export class ConsoleOutput { - display(result: ValidationResult, input: string, options: CLIOptions): void { - console.log(); // Empty line - - // Header with status - this.displayHeader(result, input); - - // Summary metrics - this.displaySummary(result); - - // Live test results (if performed) - if (result.liveTest) { - this.displayLiveTest(result.liveTest); - } - - // Detailed results unless errors-only - if (!options.errorsOnly) { - this.displayValidations(result); - } - - // Issues (errors, warnings) - this.displayIssues(result); - - // Suggestions - if (!options.errorsOnly && result.suggestions.length > 0) { - this.displaySuggestions(result); - } - - // What's next guidance - this.displayNextSteps(result); - } - - private displayHeader(result: ValidationResult, input: string): void { - const status = result.success - ? chalk.green('✅ A2A AGENT VALIDATION PASSED') - : chalk.red('❌ A2A AGENT VALIDATION FAILED'); - - console.log(status); - console.log(chalk.gray(`Agent: ${input}`)); - - // Always display detailed scoring breakdown - if (result.scoringResult) { - this.displayDetailedScores(result.scoringResult); - } - - if (result.versionInfo) { - const version = result.versionInfo.detectedVersion || 'undefined'; - const strictness = result.versionInfo.strictness; - console.log(chalk.gray(`Version: ${version} (Strictness: ${strictness})`)); - - if (result.versionInfo.compatibility && !result.versionInfo.compatibility.compatible) { - const mismatchCount = result.versionInfo.compatibility.mismatches.length; - console.log(chalk.yellow(`⚠️ Version Compatibility Issues: ${mismatchCount} detected`)); - } - } - - console.log(); - } - - private displaySummary(result: ValidationResult): void { - if (result.validations.length === 0) return; - - const total = result.validations.length; - const passed = result.validations.filter(v => v.status === 'passed').length; - const failed = result.validations.filter(v => v.status === 'failed').length; - const warnings = result.warnings.length; - - console.log(chalk.cyan('🔍 VALIDATION SUMMARY:')); - console.log(` 📊 ${total} checks performed: ${chalk.green(passed + ' passed')}, ${chalk.red(failed + ' failed')}, ${chalk.yellow(warnings + ' warnings')}`); - - // Calculate total duration if available - const totalDuration = result.validations.reduce((sum, v) => sum + (v.duration || 0), 0); - if (totalDuration > 0) { - console.log(` ⏱️ Completed in ${totalDuration}ms`); - } - - console.log(); - } - - private displayValidations(result: ValidationResult): void { - if (result.validations.length === 0) return; - - console.log(chalk.cyan.bold('🔍 VALIDATIONS PERFORMED:')); - - result.validations.forEach((validation: ValidationCheck) => { - const statusIcon = validation.status === 'passed' ? '✅' : - validation.status === 'failed' ? '❌' : '⏭️'; - const statusColor = validation.status === 'passed' ? chalk.green : - validation.status === 'failed' ? chalk.red : chalk.gray; - - console.log(statusColor(`${statusIcon} ${validation.name}`)); - - if (validation.details) { - console.log(chalk.gray(` ${validation.details}`)); - } - - if (validation.duration) { - console.log(chalk.gray(` Duration: ${validation.duration}ms`)); - } - }); - - console.log(); - } - - private displayLiveTest(liveTest: LiveTestResult): void { - console.log(chalk.cyan.bold('🔗 LIVE ENDPOINT TESTING:')); - - if (liveTest.success) { - console.log(chalk.green(`✅ Live test passed`)); - console.log(chalk.gray(` Endpoint: ${liveTest.endpoint}`)); - console.log(chalk.gray(` Response Time: ${liveTest.responseTime}ms`)); - - if (liveTest.response) { - const responseKind = liveTest.response.kind || 'unknown'; - console.log(chalk.gray(` Response Type: ${responseKind}`)); - } - } else { - console.log(chalk.red(`❌ Live test failed`)); - console.log(chalk.gray(` Endpoint: ${liveTest.endpoint}`)); - - liveTest.errors.forEach(error => { - console.log(chalk.red(` • ${error}`)); - }); - } - - console.log(); - } - - private displayIssues(result: ValidationResult): void { - // Errors - if (result.errors.length > 0) { - console.log(chalk.red.bold(`🔍 ERRORS FOUND (${result.errors.length}):`)); - result.errors.forEach(error => { - console.log(chalk.red(`❌ ${error.code}: ${error.message}`)); - if (error.field) { - console.log(chalk.gray(` Field: ${error.field}`)); - } - }); - console.log(); - } - - // Warnings - if (result.warnings.length > 0) { - console.log(chalk.yellow.bold(`⚠️ WARNINGS (${result.warnings.length}):`)); - result.warnings.forEach(warning => { - console.log(chalk.yellow(`⚠️ ${warning.code}: ${warning.message}`)); - if (warning.field) { - console.log(chalk.gray(` Field: ${warning.field}`)); - } - }); - console.log(); - } - } - - private displaySuggestions(result: ValidationResult): void { - console.log(chalk.blue.bold(`💡 SUGGESTIONS (${result.suggestions.length}):`)); - result.suggestions.forEach(suggestion => { - console.log(chalk.blue(`💡 ${suggestion.message}`)); - if (suggestion.impact) { - console.log(chalk.gray(` Impact: ${suggestion.impact}`)); - } - }); - console.log(); - } - - private displayDetailedScores(scoringResult: any): void { - console.log(); - console.log(chalk.cyan.bold('📊 SCORING BREAKDOWN:')); - console.log(); - - // Compliance Score - const complianceColor = this.getScoreColor(scoringResult.compliance.total); - console.log(complianceColor(` ✓ Spec Compliance: ${scoringResult.compliance.total}/100 ${scoringResult.compliance.rating}`)); - console.log(chalk.gray(` └─ Core Fields: ${scoringResult.compliance.breakdown.coreFields.score}/60`)); - console.log(chalk.gray(` └─ Skills Quality: ${scoringResult.compliance.breakdown.skillsQuality.score}/20`)); - console.log(chalk.gray(` └─ Format: ${scoringResult.compliance.breakdown.formatCompliance.score}/15`)); - console.log(chalk.gray(` └─ Data Quality: ${scoringResult.compliance.breakdown.dataQuality.score}/5`)); - - // Trust Score - const trustColor = this.getScoreColor(scoringResult.trust.total); - console.log(); - console.log(trustColor(` ✓ Trust: ${scoringResult.trust.total}/100 ${scoringResult.trust.rating}`)); - if (scoringResult.trust.confidenceMultiplier < 1.0) { - console.log(chalk.yellow(` ⚠️ Confidence: ${scoringResult.trust.confidenceMultiplier}x (Raw: ${scoringResult.trust.rawScore})`)); - } - console.log(chalk.gray(` └─ Signatures: ${scoringResult.trust.breakdown.signatures.score}/40 ${scoringResult.trust.breakdown.signatures.tested ? '' : '(Not Tested)'}`)); - console.log(chalk.gray(` └─ Provider: ${scoringResult.trust.breakdown.provider.score}/25`)); - console.log(chalk.gray(` └─ Security: ${scoringResult.trust.breakdown.security.score}/20`)); - console.log(chalk.gray(` └─ Documentation: ${scoringResult.trust.breakdown.documentation.score}/15`)); - - // Availability Score - console.log(); - if (scoringResult.availability.tested && scoringResult.availability.total !== null) { - const availColor = this.getScoreColor(scoringResult.availability.total); - console.log(availColor(` ✓ Availability: ${scoringResult.availability.total}/100 ${scoringResult.availability.rating}`)); - console.log(chalk.gray(` └─ Primary Endpoint: ${scoringResult.availability.breakdown.primaryEndpoint.score}/50`)); - console.log(chalk.gray(` └─ Transport Support: ${scoringResult.availability.breakdown.transportSupport.score}/30`)); - console.log(chalk.gray(` └─ Response Quality: ${scoringResult.availability.breakdown.responseQuality.score}/20`)); - } else { - console.log(chalk.gray(` ⏭️ Availability: Not Tested`)); - if (scoringResult.availability.notTestedReason) { - console.log(chalk.gray(` └─ ${scoringResult.availability.notTestedReason}`)); - } - } - - // Recommendation - if (scoringResult.recommendation) { - console.log(); - console.log(chalk.blue.bold('💡 RECOMMENDATION:')); - const recommendations = scoringResult.recommendation.split('\n'); - recommendations.forEach((rec: string) => { - console.log(` ${rec}`); - }); - } - - console.log(); - } - - private getScoreColor(score: number): typeof chalk.green { - if (score >= 90) return chalk.green; - if (score >= 75) return chalk.yellow; - if (score >= 60) return chalk.magenta; - return chalk.red; - } - - private displayNextSteps(result: ValidationResult): void { - if (result.errors.length > 0) { - console.log(chalk.blue.bold('💻 NEXT STEPS:')); - console.log(chalk.blue('1. Fix the errors listed above')); - console.log(chalk.blue('2. Re-run validation to confirm fixes')); - - if (result.suggestions.length > 0) { - console.log(chalk.blue('3. Consider applying the suggestions for improvements')); - } - } else if (result.warnings.length > 0) { - console.log(chalk.green.bold('🎉 Great! Your agent is valid with minor improvements available.')); - console.log(chalk.blue('💡 Consider addressing the warnings above for optimal compliance.')); - } else { - console.log(chalk.green.bold('🏆 Perfect! Your agent passes all validations.')); - console.log(chalk.blue('🚀 Your agent is ready for deployment!')); - } - - console.log(); - } -} \ No newline at end of file diff --git a/src/output/json.ts b/src/output/json.ts deleted file mode 100644 index 1d87be4..0000000 --- a/src/output/json.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ValidationResult, CLIOptions } from '../types'; - -export class JsonOutput { - display(result: ValidationResult, _input: string, _options: CLIOptions): void { - console.log(JSON.stringify(result, null, 2)); - } -} \ No newline at end of file diff --git a/src/scoring/availability-scorer.ts b/src/scoring/availability-scorer.ts deleted file mode 100644 index f2bb525..0000000 --- a/src/scoring/availability-scorer.ts +++ /dev/null @@ -1,294 +0,0 @@ -/** - * Availability Scorer - Measures operational readiness - * - * Weighting: - * - Primary Endpoint: 50 points (response, timing, TLS, CORS) - * - Transport Support: 30 points (declared protocols work) - * - Response Quality: 20 points (valid structure, headers, error handling) - * - * Only calculated when --test-live is used (not --schema-only) - * - * Total: 100 points - */ - -import { AgentCard } from '../types/index.js'; -import { - AvailabilityScore, - AvailabilityBreakdown, - getAvailabilityRating, - ScoringContext, -} from './types.js'; - -/** - * Live testing result from LiveTester - */ -export interface LiveTestResult { - success: boolean; - responseTime?: number; - errors: string[]; - response?: any; - hasCors?: boolean; - validTls?: boolean; - transportTested?: string; - protocolValid?: boolean; -} - -/** - * Calculate availability score for an agent card - */ -export function calculateAvailabilityScore( - agentCard: AgentCard, - context: ScoringContext, - liveTestResult?: LiveTestResult -): AvailabilityScore { - // If live testing wasn't performed, return null score - if (!context.testLive || !liveTestResult) { - return { - total: null, - rating: null, - breakdown: null, - issues: [], - tested: false, - notTestedReason: context.schemaOnly - ? 'Schema-only validation (use --test-live to test availability)' - : 'Live testing not performed', - }; - } - - const breakdown: AvailabilityBreakdown = { - primaryEndpoint: evaluatePrimaryEndpoint(liveTestResult), - transportSupport: evaluateTransportSupport(agentCard, liveTestResult), - responseQuality: evaluateResponseQuality(liveTestResult), - }; - - const total = - breakdown.primaryEndpoint.score + - breakdown.transportSupport.score + - breakdown.responseQuality.score; - - const issues = collectIssues(breakdown, liveTestResult); - - return { - total: Math.round(total * 100) / 100, - rating: getAvailabilityRating(total), - breakdown, - issues, - tested: true, - }; -} - -/** - * Evaluate primary endpoint (50 points max) - */ -function evaluatePrimaryEndpoint( - liveTestResult: LiveTestResult -): AvailabilityBreakdown['primaryEndpoint'] { - let score = 0; - const errors: string[] = []; - - // Endpoint responds (30 points) - const responds = liveTestResult.success; - if (responds) { - score += 30; - } else { - errors.push(...liveTestResult.errors); - } - - // Response time < 3 seconds (10 points) - const responseTime = liveTestResult.responseTime; - if (responseTime !== undefined && responseTime < 3000) { - score += 10; - } else if (responseTime !== undefined && responseTime >= 3000 && responseTime < 10000) { - // Partial credit for slow but working responses - score += 5; - } - - // Proper CORS headers (5 points) - const hasCors = liveTestResult.hasCors || false; - if (hasCors) { - score += 5; - } - - // Valid TLS certificate (5 points) - const validTls = liveTestResult.validTls !== false; // Assume valid if not explicitly false - if (validTls) { - score += 5; - } - - const details: AvailabilityBreakdown['primaryEndpoint']['details'] = { - responds, - validTls, - }; - - if (responseTime !== undefined) { - details.responseTime = responseTime; - } - - if (hasCors) { - details.hasCors = hasCors; - } - - if (errors.length > 0) { - details.errors = errors; - } - - return { - score, - maxScore: 50, - details, - }; -} - -/** - * Evaluate transport protocol support (30 points max) - */ -function evaluateTransportSupport( - agentCard: AgentCard, - liveTestResult: LiveTestResult -): AvailabilityBreakdown['transportSupport'] { - let score = 0; - - // Preferred transport works (20 points) - const preferredTransportWorks = liveTestResult.success; - if (preferredTransportWorks) { - score += 20; - } - - // Additional interfaces (up to 10 points) - const additionalInterfaces = agentCard.additionalInterfaces || []; - const additionalInterfacesWorking = 0; // Would need to test each interface - const additionalInterfacesFailed = 0; - - // Give credit if no additional interfaces declared (they're optional) - if (additionalInterfaces.length === 0) { - score += 10; // No additional interfaces to fail - } else { - // In a real implementation, we'd test each additional interface - // For now, give partial credit - score += 5; - } - - return { - score, - maxScore: 30, - details: { - preferredTransportWorks, - additionalInterfacesWorking, - additionalInterfacesFailed, - }, - }; -} - -/** - * Evaluate response quality (20 points max) - */ -function evaluateResponseQuality( - liveTestResult: LiveTestResult -): AvailabilityBreakdown['responseQuality'] { - let score = 0; - - if (!liveTestResult.success) { - // Can't evaluate quality if endpoint didn't respond - return { - score: 0, - maxScore: 20, - details: { - validStructure: false, - properContentType: false, - properErrorHandling: false, - }, - }; - } - - // Valid protocol structure (10 points) - const validStructure = liveTestResult.protocolValid !== false; - if (validStructure) { - score += 10; - } - - // Proper content-type headers (5 points) - // In a real implementation, we'd check the actual headers - const properContentType = true; // Assume true for now - if (properContentType) { - score += 5; - } - - // Proper error handling (5 points) - // In a real implementation, we'd test error scenarios - const properErrorHandling = liveTestResult.errors.length === 0; - if (properErrorHandling) { - score += 5; - } - - return { - score, - maxScore: 20, - details: { - validStructure, - properContentType, - properErrorHandling, - }, - }; -} - -/** - * Collect all issues from breakdown - */ -function collectIssues( - breakdown: AvailabilityBreakdown, - _liveTestResult: LiveTestResult -): string[] { - const issues: string[] = []; - - // Primary endpoint issues - if (!breakdown.primaryEndpoint.details.responds) { - issues.push('Primary endpoint not responding'); - if (breakdown.primaryEndpoint.details.errors) { - issues.push(...breakdown.primaryEndpoint.details.errors); - } - } else { - // Only check these if endpoint is responding - const responseTime = breakdown.primaryEndpoint.details.responseTime; - if (responseTime !== undefined) { - if (responseTime >= 10000) { - issues.push(`Slow response time: ${responseTime}ms (timeout)`); - } else if (responseTime >= 3000) { - issues.push(`Slow response time: ${responseTime}ms`); - } - } - - if (!breakdown.primaryEndpoint.details.hasCors) { - issues.push('Missing CORS headers - may not work in browsers'); - } - - if (!breakdown.primaryEndpoint.details.validTls) { - issues.push('Invalid or expired TLS certificate'); - } - } - - // Transport support issues - if (!breakdown.transportSupport.details.preferredTransportWorks) { - issues.push('Preferred transport protocol not working'); - } - - if (breakdown.transportSupport.details.additionalInterfacesFailed > 0) { - issues.push( - `${breakdown.transportSupport.details.additionalInterfacesFailed} additional interface(s) not working` - ); - } - - // Response quality issues - if (!breakdown.responseQuality.details.validStructure) { - issues.push('Response does not follow A2A protocol structure'); - } - - if (!breakdown.responseQuality.details.properContentType) { - issues.push('Incorrect content-type headers'); - } - - if (!breakdown.responseQuality.details.properErrorHandling) { - issues.push('Protocol errors detected in response'); - } - - return issues; -} diff --git a/src/scoring/compliance-scorer.ts b/src/scoring/compliance-scorer.ts deleted file mode 100644 index a72a71d..0000000 --- a/src/scoring/compliance-scorer.ts +++ /dev/null @@ -1,423 +0,0 @@ -/** - * Compliance Scorer - Measures A2A v0.3.0 specification adherence - * - * Weighting: - * - Core Required Fields: 60 points (9 fields @ 6.67 each) - * - Skills Quality: 20 points - * - Format Compliance: 15 points - * - Data Quality: 5 points - * - * Total: 100 points - */ - -import { AgentCard } from '../types/index.js'; -import { - ComplianceScore, - ComplianceBreakdown, - getComplianceRating, -} from './types.js'; - -const REQUIRED_FIELDS = [ - 'protocolVersion', - 'name', - 'description', - 'url', - 'version', - 'capabilities', - 'defaultInputModes', - 'defaultOutputModes', - 'skills', -]; - -/** - * Calculate compliance score for an agent card - */ -export function calculateComplianceScore( - agentCard: AgentCard, - validationErrors: string[] -): ComplianceScore { - const breakdown: ComplianceBreakdown = { - coreFields: evaluateCoreFields(agentCard), - skillsQuality: evaluateSkillsQuality(agentCard), - formatCompliance: evaluateFormatCompliance(agentCard, validationErrors), - dataQuality: evaluateDataQuality(agentCard), - }; - - const total = - breakdown.coreFields.score + - breakdown.skillsQuality.score + - breakdown.formatCompliance.score + - breakdown.dataQuality.score; - - // Ensure total doesn't exceed 100 due to rounding - const finalTotal = Math.min(100, Math.round(total * 100) / 100); - - const issues = collectIssues(breakdown); - - return { - total: finalTotal, - rating: getComplianceRating(finalTotal), - breakdown, - issues, - }; -} - -/** - * Evaluate core required fields (60 points max) - */ -function evaluateCoreFields(agentCard: any): ComplianceBreakdown['coreFields'] { - const present: string[] = []; - const missing: string[] = []; - - for (const field of REQUIRED_FIELDS) { - if (agentCard[field] !== undefined && agentCard[field] !== null) { - // Additional check for arrays - must not be empty - if (Array.isArray(agentCard[field])) { - if (agentCard[field].length > 0) { - present.push(field); - } else { - missing.push(field); - } - } else { - present.push(field); - } - } else { - missing.push(field); - } - } - - // Calculate score: 60 points distributed evenly across fields - // Use exact division to avoid rounding issues - const score = (present.length / REQUIRED_FIELDS.length) * 60; - - return { - score: Math.round(score * 100) / 100, - maxScore: 60, - details: { - present, - missing, - }, - }; -} - -/** - * Evaluate skills quality (20 points max) - */ -function evaluateSkillsQuality(agentCard: AgentCard): ComplianceBreakdown['skillsQuality'] { - let score = 0; - let issueCount = 0; - - const skills = agentCard.skills || []; - - // Base requirement: at least one skill present (5 points) - const skillsPresent = skills.length > 0; - if (skillsPresent) { - score += 5; - } else { - issueCount++; - } - - // All skills have required fields (10 points) - let allSkillsHaveRequiredFields = true; - if (skills.length > 0) { - for (const skill of skills) { - if (!skill.id || !skill.name || !skill.description) { - allSkillsHaveRequiredFields = false; - issueCount++; - score -= 2; // Deduct 2 points per skill (max -10) - } - } - if (allSkillsHaveRequiredFields) { - score += 10; - } else { - // Ensure we don't go negative - score = Math.max(5, score); - } - } else { - allSkillsHaveRequiredFields = false; - } - - // All skills have tags (5 points) - let allSkillsHaveTags = true; - if (skills.length > 0) { - for (const skill of skills) { - if (!skill.tags || skill.tags.length === 0) { - allSkillsHaveTags = false; - issueCount++; - score -= 1; // Deduct 1 point per skill (max -5) - } - } - if (allSkillsHaveTags) { - score += 5; - } else { - // Ensure we don't go below 5 (base requirement) - score = Math.max(skillsPresent ? 5 : 0, score); - } - } else { - allSkillsHaveTags = false; - } - - return { - score: Math.max(0, Math.round(score * 100) / 100), - maxScore: 20, - details: { - skillsPresent, - allSkillsHaveRequiredFields, - allSkillsHaveTags, - issueCount, - }, - }; -} - -/** - * Evaluate format compliance (15 points max) - */ -function evaluateFormatCompliance( - agentCard: AgentCard, - _validationErrors: string[] -): ComplianceBreakdown['formatCompliance'] { - let score = 15; // Start with max, deduct for violations - - // Valid semver version format (3 points) - const validSemver = /^\d+\.\d+\.\d+/.test(agentCard.version || ''); - if (!validSemver) { - score -= 3; - } - - // Valid protocolVersion (3 points) - const validProtocolVersion = ['0.1.0', '0.2.0', '0.3.0'].includes( - agentCard.protocolVersion || '' - ); - if (!validProtocolVersion) { - score -= 3; - } - - // Valid URL format (3 points) - const validUrl = isValidUrl(agentCard.url); - if (!validUrl) { - score -= 3; - } - - // Valid transport protocols (3 points) - const validTransports = validateTransports(agentCard); - if (!validTransports) { - score -= 3; - } - - // Valid MIME types (3 points) - const validMimeTypes = validateMimeTypes(agentCard); - if (!validMimeTypes) { - score -= 3; - } - - return { - score: Math.max(0, score), - maxScore: 15, - details: { - validSemver, - validProtocolVersion, - validUrl, - validTransports, - validMimeTypes, - }, - }; -} - -/** - * Evaluate data quality (5 points max) - */ -function evaluateDataQuality(agentCard: AgentCard): ComplianceBreakdown['dataQuality'] { - let score = 5; // Start with max, deduct for violations - - // No duplicate skill IDs (2 points) - const noDuplicateSkillIds = checkNoDuplicateSkillIds(agentCard.skills || []); - if (!noDuplicateSkillIds) { - score -= 2; - } - - // Field lengths valid (2 points) - const fieldLengthsValid = checkFieldLengths(agentCard); - if (!fieldLengthsValid) { - score -= 2; - } - - // No SSRF risks (1 point) - const noSsrfRisks = checkNoSsrfRisks(agentCard); - if (!noSsrfRisks) { - score -= 1; - } - - return { - score: Math.max(0, score), - maxScore: 5, - details: { - noDuplicateSkillIds, - fieldLengthsValid, - noSsrfRisks, - }, - }; -} - -/** - * Helper: Check if URL is valid - */ -function isValidUrl(url: string | undefined): boolean { - if (!url) return false; - try { - const parsed = new URL(url); - return parsed.protocol === 'https:' || parsed.protocol === 'http:'; - } catch { - return false; - } -} - -/** - * Helper: Validate transport protocols - */ -function validateTransports(agentCard: AgentCard): boolean { - const validTransports = ['JSONRPC', 'HTTP+JSON', 'GRPC']; - - if (agentCard.preferredTransport) { - if (!validTransports.includes(agentCard.preferredTransport)) { - return false; - } - } - - if (agentCard.additionalInterfaces) { - for (const iface of agentCard.additionalInterfaces) { - if (iface.transport && !validTransports.includes(iface.transport)) { - return false; - } - } - } - - return true; -} - -/** - * Helper: Validate MIME types - */ -function validateMimeTypes(agentCard: AgentCard): boolean { - const mimeTypeRegex = /^[a-z]+\/[a-z0-9+\-.]+$/i; - - const inputModes = agentCard.defaultInputModes || []; - const outputModes = agentCard.defaultOutputModes || []; - - for (const mode of [...inputModes, ...outputModes]) { - if (!mimeTypeRegex.test(mode)) { - return false; - } - } - - return true; -} - -/** - * Helper: Check for duplicate skill IDs - */ -function checkNoDuplicateSkillIds(skills: any[]): boolean { - const ids = skills.map((s) => s.id).filter(Boolean); - const uniqueIds = new Set(ids); - return ids.length === uniqueIds.size; -} - -/** - * Helper: Check field lengths are reasonable - */ -function checkFieldLengths(agentCard: AgentCard): boolean { - // Name should be reasonable length - if (agentCard.name && agentCard.name.length > 100) return false; - - // Description should not be excessively long - if (agentCard.description && agentCard.description.length > 1000) return false; - - // URLs should be reasonable - if (agentCard.url && agentCard.url.length > 500) return false; - - return true; -} - -/** - * Helper: Check for SSRF risks (localhost, private IPs) - */ -function checkNoSsrfRisks(agentCard: AgentCard): boolean { - const dangerousPatterns = [ - /localhost/i, - /127\.0\.0\.1/, - /0\.0\.0\.0/, - /192\.168\./, - /10\./, - /172\.(1[6-9]|2[0-9]|3[01])\./, - ]; - - const urls = [ - agentCard.url, - agentCard.documentationUrl, - agentCard.provider?.url, - ].filter(Boolean); - - for (const url of urls) { - for (const pattern of dangerousPatterns) { - if (pattern.test(url as string)) { - return false; - } - } - } - - return true; -} - -/** - * Collect all issues from breakdown - */ -function collectIssues(breakdown: ComplianceBreakdown): string[] { - const issues: string[] = []; - - // Core fields issues - if (breakdown.coreFields.details.missing.length > 0) { - issues.push( - `Missing required fields: ${breakdown.coreFields.details.missing.join(', ')}` - ); - } - - // Skills quality issues - if (!breakdown.skillsQuality.details.skillsPresent) { - issues.push('No skills defined'); - } - if (!breakdown.skillsQuality.details.allSkillsHaveRequiredFields) { - issues.push('Some skills missing required fields (id, name, description)'); - } - if (!breakdown.skillsQuality.details.allSkillsHaveTags) { - issues.push('Some skills missing tags'); - } - - // Format compliance issues - if (!breakdown.formatCompliance.details.validSemver) { - issues.push('Invalid semver version format'); - } - if (!breakdown.formatCompliance.details.validProtocolVersion) { - issues.push('Invalid or unsupported protocolVersion'); - } - if (!breakdown.formatCompliance.details.validUrl) { - issues.push('Invalid URL format'); - } - if (!breakdown.formatCompliance.details.validTransports) { - issues.push('Invalid transport protocol specified'); - } - if (!breakdown.formatCompliance.details.validMimeTypes) { - issues.push('Invalid MIME types in input/output modes'); - } - - // Data quality issues - if (!breakdown.dataQuality.details.noDuplicateSkillIds) { - issues.push('Duplicate skill IDs detected'); - } - if (!breakdown.dataQuality.details.fieldLengthsValid) { - issues.push('Some fields exceed reasonable length limits'); - } - if (!breakdown.dataQuality.details.noSsrfRisks) { - issues.push('URLs contain localhost or private IP addresses'); - } - - return issues; -} diff --git a/src/scoring/index.ts b/src/scoring/index.ts deleted file mode 100644 index 371fdbd..0000000 --- a/src/scoring/index.ts +++ /dev/null @@ -1,203 +0,0 @@ -/** - * Scoring Orchestrator - Coordinates all three scoring dimensions - * - * Calculates: - * 1. Compliance Score - A2A protocol adherence (always calculated) - * 2. Trust Score - Security and authenticity (with confidence multiplier) - * 3. Availability Score - Operational readiness (when --test-live used) - */ - -import { AgentCard } from '../types/index.js'; -import { ScoringResult, ScoringContext } from './types.js'; -import { calculateComplianceScore } from './compliance-scorer.js'; -import { calculateTrustScore } from './trust-scorer.js'; -import { - calculateAvailabilityScore, - type LiveTestResult, -} from './availability-scorer.js'; - -// Re-export types for convenience -export * from './types.js'; -export type { LiveTestResult } from './availability-scorer.js'; - -/** - * Input data for scoring calculation - */ -export interface ScoringInput { - agentCard: AgentCard; - validationErrors: string[]; - signatureVerificationResult?: { - valid: boolean; - invalid: boolean; - details?: any; - }; - liveTestResult?: LiveTestResult; -} - -/** - * Calculate all three scoring dimensions - */ -export function calculateScores( - input: ScoringInput, - context: ScoringContext -): ScoringResult { - // Calculate compliance score (always calculated) - const compliance = calculateComplianceScore( - input.agentCard, - input.validationErrors - ); - - // Calculate trust score (always calculated, but may have partial validation) - const trust = calculateTrustScore( - input.agentCard, - context, - input.signatureVerificationResult - ); - - // Calculate availability score (only if live testing was performed) - const availability = calculateAvailabilityScore( - input.agentCard, - context, - input.liveTestResult - ); - - // Generate overall recommendation - const recommendation = generateRecommendation(compliance, trust, availability); - - return { - compliance, - trust, - availability, - recommendation, - }; -} - -/** - * Generate overall recommendation based on all scores - */ -function generateRecommendation( - compliance: ScoringResult['compliance'], - trust: ScoringResult['trust'], - availability: ScoringResult['availability'] -): string { - const recommendations: string[] = []; - - // Compliance recommendations - if (compliance.total === 100) { - recommendations.push('✅ Fully A2A v0.3.0 compliant'); - } else if (compliance.total >= 90) { - recommendations.push('✅ Excellent A2A compliance'); - } else if (compliance.total >= 75) { - recommendations.push('⚠️ Good compliance with minor issues'); - } else if (compliance.total >= 60) { - recommendations.push('⚠️ Fair compliance - improvements recommended'); - } else { - recommendations.push('❌ Poor compliance - significant improvements needed'); - } - - // Trust recommendations (considering confidence multiplier) - if (trust.confidenceMultiplier < 1.0) { - if (trust.confidenceMultiplier === 0.4) { - recommendations.push( - '🚨 Invalid signatures detected - do not use in production' - ); - } else if (trust.confidenceMultiplier === 0.6) { - recommendations.push( - '⚠️ No cryptographic signatures - consider adding JWS signatures to improve trust' - ); - } - } else if (trust.total >= 80) { - recommendations.push('✅ Highly trusted with strong security signals'); - } else if (trust.total >= 60) { - recommendations.push('✅ Trusted with good security configuration'); - } else if (trust.total >= 40) { - recommendations.push('⚠️ Moderate trust - consider improving security'); - } else { - recommendations.push('⚠️ Low trust - security improvements strongly recommended'); - } - - // Availability recommendations - if (availability.tested && availability.total !== null) { - if (availability.total >= 95) { - recommendations.push('✅ Fully operational and performant'); - } else if (availability.total >= 80) { - recommendations.push('✅ Operational with minor issues'); - } else if (availability.total >= 60) { - recommendations.push('⚠️ Degraded performance or reliability issues'); - } else if (availability.total >= 40) { - recommendations.push('⚠️ Unstable - significant operational issues'); - } else { - recommendations.push('❌ Unavailable or severely degraded'); - } - } - - // Overall production readiness - const isProductionReady = - compliance.total >= 95 && - trust.total >= 60 && - trust.confidenceMultiplier >= 0.6 && - (!availability.tested || (availability.total !== null && availability.total >= 80)); - - if (isProductionReady) { - recommendations.push('🎉 Production ready!'); - } else { - const blockers: string[] = []; - if (compliance.total < 95) blockers.push('compliance'); - if (trust.total < 60 || trust.confidenceMultiplier < 0.6) blockers.push('trust'); - if (availability.tested && availability.total !== null && availability.total < 80) { - blockers.push('availability'); - } - recommendations.push( - `⚠️ Not yet production ready - improve: ${blockers.join(', ')}` - ); - } - - return recommendations.join('\n'); -} - -/** - * Helper: Create scoring context from CLI options - */ -export function createScoringContext(options: { - schemaOnly?: boolean; - skipSignatureVerification?: boolean; - testLive?: boolean; - strictMode?: boolean; -}): ScoringContext { - return { - schemaOnly: options.schemaOnly || false, - skipSignatureVerification: options.skipSignatureVerification || false, - testLive: options.testLive || false, - strictMode: options.strictMode || false, - }; -} - -/** - * Helper: Get production readiness threshold - */ -export function getProductionReadinessThreshold() { - return { - compliance: 95, - trust: 60, - trustConfidence: 0.6, - availability: 80, - }; -} - -/** - * Helper: Check if agent meets production readiness criteria - */ -export function isProductionReady(result: ScoringResult): boolean { - const threshold = getProductionReadinessThreshold(); - - const complianceReady = result.compliance.total >= threshold.compliance; - const trustReady = - result.trust.total >= threshold.trust && - result.trust.confidenceMultiplier >= threshold.trustConfidence; - const availabilityReady = - !result.availability.tested || - (result.availability.total !== null && - result.availability.total >= threshold.availability); - - return complianceReady && trustReady && availabilityReady; -} diff --git a/src/scoring/trust-scorer.ts b/src/scoring/trust-scorer.ts deleted file mode 100644 index 841cd31..0000000 --- a/src/scoring/trust-scorer.ts +++ /dev/null @@ -1,394 +0,0 @@ -/** - * Trust Scorer - Measures security and authenticity signals - * - * Weighting (before multiplier): - * - Cryptographic Signatures: 40 points (most pivotal) - * - Provider Information: 25 points - * - Security Configuration: 20 points - * - Documentation: 15 points - * - * Trust Confidence Multiplier: - * - With valid signatures: 1.0x (100% confidence) - * - Without signatures: 0.6x (60% confidence - unverified claims) - * - With invalid signatures: 0.4x (40% confidence - active distrust) - * - * Total: 100 points (after multiplier) - */ - -import { AgentCard } from '../types/index.js'; -import { - TrustScore, - TrustBreakdown, - getTrustRating, - getTrustConfidenceMultiplier, - ScoringContext, -} from './types.js'; - -/** - * Calculate trust score for an agent card - */ -export function calculateTrustScore( - agentCard: AgentCard, - context: ScoringContext, - signatureVerificationResult?: { - valid: boolean; - invalid: boolean; - details?: any; - } -): TrustScore { - const breakdown: TrustBreakdown = { - signatures: evaluateSignatures(agentCard, context, signatureVerificationResult), - provider: evaluateProvider(agentCard, context), - security: evaluateSecurity(agentCard), - documentation: evaluateDocumentation(agentCard), - }; - - // Calculate raw score (before confidence multiplier) - const rawScore = - breakdown.signatures.score + - breakdown.provider.score + - breakdown.security.score + - breakdown.documentation.score; - - // Apply trust confidence multiplier - const hasValidSignature = breakdown.signatures.details.hasValidSignature; - const hasInvalidSignature = breakdown.signatures.details.hasInvalidSignature || false; - - const confidenceMultiplier = getTrustConfidenceMultiplier( - hasValidSignature, - hasInvalidSignature - ); - - const total = rawScore * confidenceMultiplier; - - const issues = collectIssues(breakdown, confidenceMultiplier); - - return { - total: Math.round(total * 100) / 100, - rawScore: Math.round(rawScore * 100) / 100, - confidenceMultiplier, - rating: getTrustRating(total), - breakdown, - issues, - partialValidation: context.skipSignatureVerification || context.schemaOnly, - }; -} - -/** - * Evaluate cryptographic signatures (40 points max + confidence multiplier) - */ -function evaluateSignatures( - agentCard: AgentCard, - context: ScoringContext, - verificationResult?: { valid: boolean; invalid: boolean; details?: any } -): TrustBreakdown['signatures'] { - // If signature verification was skipped, we can't evaluate - if (context.skipSignatureVerification) { - return { - score: 0, - maxScore: 40, - tested: false, - details: { - hasValidSignature: false, - multipleSignatures: false, - coversAllFields: false, - isRecent: false, - }, - }; - } - - let score = 0; - const signatures = agentCard.signatures || []; - const hasSignatures = signatures.length > 0; - - // At least one valid signature (30 points) - const hasValidSignature = verificationResult?.valid || false; - if (hasValidSignature) { - score += 30; - } - - // Multiple signatures for redundancy (3 points) - const multipleSignatures = signatures.length > 1; - if (multipleSignatures && hasValidSignature) { - score += 3; - } - - // Signature covers all critical fields (4 points) - // This is a simplification - in practice you'd check the JWS payload - const coversAllFields = hasValidSignature && hasSignatures; - if (coversAllFields) { - score += 4; - } - - // Recent signature < 90 days (3 points) - // This would require parsing the signature timestamp - const isRecent = hasValidSignature && hasSignatures; - if (isRecent) { - score += 3; - } - - // Penalties (worse than missing) - const hasInvalidSignature = verificationResult?.invalid || false; - if (hasInvalidSignature) { - score -= 15; // Active deception penalty - } - - // Expired signature penalty (> 1 year) would go here if we tracked it - const hasExpiredSignature = false; // Placeholder for future implementation - - return { - score: Math.max(0, score), - maxScore: 40, - tested: !context.skipSignatureVerification, - details: { - hasValidSignature, - multipleSignatures, - coversAllFields, - isRecent, - hasInvalidSignature, - hasExpiredSignature, - }, - }; -} - -/** - * Evaluate provider information (25 points max) - */ -function evaluateProvider( - agentCard: AgentCard, - context: ScoringContext -): TrustBreakdown['provider'] { - let score = 0; - - const provider = agentCard.provider; - - // Provider organization present (10 points) - const hasOrganization = !!(provider?.organization); - if (hasOrganization) { - score += 10; - } - - // Provider URL present and HTTPS (10 points) - const hasUrl = !!(provider?.url); - const isHttps = provider?.url?.startsWith('https://'); - if (hasUrl && isHttps) { - score += 10; - } - - // Provider URL reachable (5 bonus points) - only if we tested it - // This would require a HEAD request in a real implementation - let urlReachable: boolean | undefined; - if (!context.schemaOnly && hasUrl && isHttps) { - // In a real implementation, we'd make a HEAD request here - // For now, we give the benefit of the doubt - urlReachable = true; - score += 5; - } - - const details: TrustBreakdown['provider']['details'] = { - hasOrganization, - hasUrl: !!(hasUrl && isHttps), - }; - - if (urlReachable !== undefined) { - details.urlReachable = urlReachable; - } - - return { - score, - maxScore: 25, - tested: !context.schemaOnly, - details, - }; -} - -/** - * Evaluate security configuration (20 points max) - */ -function evaluateSecurity(agentCard: AgentCard): TrustBreakdown['security'] { - let score = 0; - - // HTTPS-only URLs (10 points) - const httpsOnly = checkHttpsOnly(agentCard); - if (httpsOnly) { - score += 10; - } - - // Security schemes declared (5 points) - const hasSecuritySchemes = !!( - agentCard.securitySchemes && - Object.keys(agentCard.securitySchemes).length > 0 - ); - if (hasSecuritySchemes) { - score += 5; - } - - // Strong auth (mTLS or OAuth2) (5 bonus points) - const hasStrongAuth = checkStrongAuth(agentCard); - if (hasStrongAuth) { - score += 5; - } - - // Penalty for HTTP URLs (-10 points) - const hasHttpUrls = checkHasHttpUrls(agentCard); - if (hasHttpUrls) { - score -= 10; - } - - return { - score: Math.max(0, score), - maxScore: 20, - details: { - httpsOnly, - hasSecuritySchemes, - hasStrongAuth, - hasHttpUrls, - }, - }; -} - -/** - * Evaluate documentation and transparency (15 points max) - */ -function evaluateDocumentation(agentCard: AgentCard): TrustBreakdown['documentation'] { - let score = 0; - - // Documentation URL present (5 points) - const hasDocumentationUrl = !!(agentCard.documentationUrl); - if (hasDocumentationUrl) { - score += 5; - } - - // Terms of Service URL (5 points) - // This would be in an extension in a real implementation - const hasTermsOfService = false; // Placeholder - if (hasTermsOfService) { - score += 5; - } - - // Privacy Policy URL (5 points) - // This would be in an extension in a real implementation - const hasPrivacyPolicy = false; // Placeholder - if (hasPrivacyPolicy) { - score += 5; - } - - return { - score, - maxScore: 15, - details: { - hasDocumentationUrl, - hasTermsOfService, - hasPrivacyPolicy, - }, - }; -} - -/** - * Helper: Check if all URLs are HTTPS - */ -function checkHttpsOnly(agentCard: AgentCard): boolean { - const urls = [ - agentCard.url, - agentCard.documentationUrl, - agentCard.provider?.url, - ].filter(Boolean); - - for (const url of urls) { - if (url && !url.startsWith('https://')) { - return false; - } - } - - // Check additional interfaces - if (agentCard.additionalInterfaces) { - for (const iface of agentCard.additionalInterfaces) { - if (iface.url && !iface.url.startsWith('https://')) { - return false; - } - } - } - - return true; -} - -/** - * Helper: Check for HTTP URLs (security issue) - */ -function checkHasHttpUrls(agentCard: AgentCard): boolean { - return !checkHttpsOnly(agentCard); -} - -/** - * Helper: Check for strong authentication schemes - */ -function checkStrongAuth(agentCard: AgentCard): boolean { - if (!agentCard.securitySchemes) return false; - - const schemes = Object.values(agentCard.securitySchemes); - - for (const scheme of schemes) { - // Check for mTLS - if ('mtlsSecurityScheme' in scheme) { - return true; - } - // Check for OAuth2 - if ('oauth2SecurityScheme' in scheme) { - return true; - } - } - - return false; -} - -/** - * Collect all issues from breakdown - */ -function collectIssues(breakdown: TrustBreakdown, confidenceMultiplier: number): string[] { - const issues: string[] = []; - - // Signature issues - if (!breakdown.signatures.tested) { - issues.push('Signature verification skipped (--skip-signature-verification)'); - } else if (breakdown.signatures.details.hasInvalidSignature) { - issues.push('Invalid signature detected - possible tampering'); - } else if (!breakdown.signatures.details.hasValidSignature) { - issues.push('No valid cryptographic signatures - trust claims unverified'); - } - - // Confidence multiplier warning - if (confidenceMultiplier < 1.0) { - if (confidenceMultiplier === 0.4) { - issues.push( - `Trust confidence severely reduced (${confidenceMultiplier}x) due to invalid signatures` - ); - } else if (confidenceMultiplier === 0.6) { - issues.push( - `Trust confidence reduced (${confidenceMultiplier}x) - no cryptographic verification` - ); - } - } - - // Provider issues - if (!breakdown.provider.details.hasOrganization) { - issues.push('No provider organization specified'); - } - if (!breakdown.provider.details.hasUrl) { - issues.push('No provider URL specified or not using HTTPS'); - } - - // Security issues - if (breakdown.security.details.hasHttpUrls) { - issues.push('Some URLs use insecure HTTP instead of HTTPS'); - } - if (!breakdown.security.details.hasSecuritySchemes) { - issues.push('No security schemes declared'); - } - - // Documentation issues - if (!breakdown.documentation.details.hasDocumentationUrl) { - issues.push('No documentation URL provided'); - } - - return issues; -} diff --git a/src/scoring/types.ts b/src/scoring/types.ts deleted file mode 100644 index 92541ac..0000000 --- a/src/scoring/types.ts +++ /dev/null @@ -1,322 +0,0 @@ -/** - * Multi-dimensional scoring system types - * - * Three independent scores that measure different aspects of agent quality: - * 1. Compliance Score - A2A protocol adherence - * 2. Trust Score - Security and authenticity signals - * 3. Availability Score - Operational readiness - */ - -// ============================================================================ -// Core Score Interfaces -// ============================================================================ - -/** - * Compliance Score (0-100): Measures A2A v0.3.0 specification adherence - * Always calculated consistently regardless of CLI flags - */ -export interface ComplianceScore { - /** Total score (0-100) */ - total: number; - /** Rating level based on score range */ - rating: ComplianceRating; - /** Detailed breakdown by category */ - breakdown: ComplianceBreakdown; - /** Issues found during validation */ - issues: string[]; -} - -/** - * Trust Score (0-100): Measures security and authenticity signals - * Includes Trust Confidence Multiplier based on signature presence - */ -export interface TrustScore { - /** Total score (0-100) after applying confidence multiplier */ - total: number; - /** Raw score before confidence multiplier */ - rawScore: number; - /** Confidence multiplier applied (1.0x, 0.6x, or 0.4x) */ - confidenceMultiplier: number; - /** Rating level based on final score */ - rating: TrustRating; - /** Detailed breakdown by category */ - breakdown: TrustBreakdown; - /** Issues found during validation */ - issues: string[]; - /** Whether any validation was skipped */ - partialValidation: boolean; -} - -/** - * Availability Score (0-100): Measures operational readiness - * Only calculated when network tests are enabled (not --schema-only) - */ -export interface AvailabilityScore { - /** Total score (0-100), or null if not tested */ - total: number | null; - /** Rating level, or null if not tested */ - rating: AvailabilityRating | null; - /** Detailed breakdown by category, or null if not tested */ - breakdown: AvailabilityBreakdown | null; - /** Issues found during validation */ - issues: string[]; - /** Whether availability was tested */ - tested: boolean; - /** Reason if not tested */ - notTestedReason?: string; -} - -// ============================================================================ -// Breakdown Structures -// ============================================================================ - -/** - * Compliance Score Breakdown (total: 100 points) - */ -export interface ComplianceBreakdown { - /** Core required fields (60 points - 9 fields @ 6.67 each) */ - coreFields: { - score: number; - maxScore: 60; - details: { - present: string[]; - missing: string[]; - }; - }; - /** Skills quality (20 points) */ - skillsQuality: { - score: number; - maxScore: 20; - details: { - skillsPresent: boolean; - allSkillsHaveRequiredFields: boolean; - allSkillsHaveTags: boolean; - issueCount: number; - }; - }; - /** Format and protocol compliance (15 points) */ - formatCompliance: { - score: number; - maxScore: 15; - details: { - validSemver: boolean; - validProtocolVersion: boolean; - validUrl: boolean; - validTransports: boolean; - validMimeTypes: boolean; - }; - }; - /** Data quality (5 points) */ - dataQuality: { - score: number; - maxScore: 5; - details: { - noDuplicateSkillIds: boolean; - fieldLengthsValid: boolean; - noSsrfRisks: boolean; - }; - }; -} - -/** - * Trust Score Breakdown (total: 100 points before multiplier) - */ -export interface TrustBreakdown { - /** Cryptographic signatures (40 points + confidence multiplier) */ - signatures: { - score: number; - maxScore: 40; - tested: boolean; - details: { - hasValidSignature: boolean; - multipleSignatures: boolean; - coversAllFields: boolean; - isRecent: boolean; - hasInvalidSignature?: boolean; - hasExpiredSignature?: boolean; - }; - }; - /** Provider information (25 points) */ - provider: { - score: number; - maxScore: 25; - tested: boolean; - details: { - hasOrganization: boolean; - hasUrl: boolean; - urlReachable?: boolean; - }; - }; - /** Security configuration (20 points) */ - security: { - score: number; - maxScore: 20; - details: { - httpsOnly: boolean; - hasSecuritySchemes: boolean; - hasStrongAuth: boolean; - hasHttpUrls?: boolean; - }; - }; - /** Documentation and transparency (15 points) */ - documentation: { - score: number; - maxScore: 15; - details: { - hasDocumentationUrl: boolean; - hasTermsOfService: boolean; - hasPrivacyPolicy: boolean; - }; - }; -} - -/** - * Availability Score Breakdown (total: 100 points) - */ -export interface AvailabilityBreakdown { - /** Primary endpoint (50 points) */ - primaryEndpoint: { - score: number; - maxScore: 50; - details: { - responds: boolean; - responseTime?: number; - hasCors?: boolean; - validTls?: boolean; - errors?: string[]; - }; - }; - /** Transport protocol support (30 points) */ - transportSupport: { - score: number; - maxScore: 30; - details: { - preferredTransportWorks: boolean; - additionalInterfacesWorking: number; - additionalInterfacesFailed: number; - }; - }; - /** Response quality (20 points) */ - responseQuality: { - score: number; - maxScore: 20; - details: { - validStructure: boolean; - properContentType: boolean; - properErrorHandling: boolean; - }; - }; -} - -// ============================================================================ -// Rating Enums -// ============================================================================ - -export enum ComplianceRating { - PERFECT = 'Perfect', - EXCELLENT = 'Excellent', - GOOD = 'Good', - FAIR = 'Fair', - POOR = 'Poor', -} - -export enum TrustRating { - HIGHLY_TRUSTED = 'Highly Trusted', - TRUSTED = 'Trusted', - MODERATE_TRUST = 'Moderate Trust', - LOW_TRUST = 'Low Trust', - UNTRUSTED = 'Untrusted', -} - -export enum AvailabilityRating { - FULLY_AVAILABLE = 'Fully Available', - AVAILABLE = 'Available', - DEGRADED = 'Degraded', - UNSTABLE = 'Unstable', - UNAVAILABLE = 'Unavailable', -} - -// ============================================================================ -// Combined Scoring Result -// ============================================================================ - -/** - * Complete scoring result with all three dimensions - */ -export interface ScoringResult { - /** Compliance score (always calculated) */ - compliance: ComplianceScore; - /** Trust score (always calculated, but may have partial validation) */ - trust: TrustScore; - /** Availability score (null if not tested) */ - availability: AvailabilityScore; - /** Overall recommendation based on all scores */ - recommendation: string; -} - -// ============================================================================ -// Validation Context -// ============================================================================ - -/** - * Context provided to scorers about what validation was performed - */ -export interface ScoringContext { - /** Whether schema-only validation was performed */ - schemaOnly: boolean; - /** Whether signature verification was skipped */ - skipSignatureVerification: boolean; - /** Whether live endpoint testing was performed */ - testLive: boolean; - /** Whether strict mode is enabled */ - strictMode: boolean; -} - -// ============================================================================ -// Helper Functions -// ============================================================================ - -/** - * Get compliance rating based on score - */ -export function getComplianceRating(score: number): ComplianceRating { - if (score === 100) return ComplianceRating.PERFECT; - if (score >= 90) return ComplianceRating.EXCELLENT; - if (score >= 75) return ComplianceRating.GOOD; - if (score >= 60) return ComplianceRating.FAIR; - return ComplianceRating.POOR; -} - -/** - * Get trust rating based on score - */ -export function getTrustRating(score: number): TrustRating { - if (score >= 80) return TrustRating.HIGHLY_TRUSTED; - if (score >= 60) return TrustRating.TRUSTED; - if (score >= 40) return TrustRating.MODERATE_TRUST; - if (score >= 20) return TrustRating.LOW_TRUST; - return TrustRating.UNTRUSTED; -} - -/** - * Get availability rating based on score - */ -export function getAvailabilityRating(score: number): AvailabilityRating { - if (score >= 95) return AvailabilityRating.FULLY_AVAILABLE; - if (score >= 80) return AvailabilityRating.AVAILABLE; - if (score >= 60) return AvailabilityRating.DEGRADED; - if (score >= 40) return AvailabilityRating.UNSTABLE; - return AvailabilityRating.UNAVAILABLE; -} - -/** - * Get trust confidence multiplier based on signature state - */ -export function getTrustConfidenceMultiplier( - hasValidSignature: boolean, - hasInvalidSignature: boolean -): number { - if (hasInvalidSignature) return 0.4; // Active distrust - if (hasValidSignature) return 1.0; // Full confidence - return 0.6; // Unverified claims -} diff --git a/src/signature-verification.ts b/src/signature-verification.ts deleted file mode 100644 index e38ebf5..0000000 --- a/src/signature-verification.ts +++ /dev/null @@ -1,369 +0,0 @@ -/** - * Agent Card JWS Signature Verification - * Implements A2A Protocol §5.5.6 - AgentCardSignature verification - */ - -import type { AgentCard, AgentCardSignature } from './types'; - -// Dynamic import for ESM-only jose library -let joseModule: any = null; -async function getJose() { - if (!joseModule) { - joseModule = await import('jose'); - } - return joseModule; -} - -/** - * Result of signature verification - */ -export interface SignatureVerificationResult { - valid: boolean; - signatures: SignatureResult[]; - summary: { - total: number; - valid: number; - failed: number; - errors: string[]; - }; -} - -/** - * Result for individual signature - */ -export interface SignatureResult { - index: number; - valid: boolean; - algorithm?: string; - keyId?: string; - issuer?: string; - jwksUri?: string; - error?: string; - details?: string; -} - -/** - * JWS Protected Header structure - */ -interface JWSHeader { - alg: string; - typ?: string; - kid?: string; - jku?: string; - jwks_uri?: string; -} - -/** - * Verify all signatures in an Agent Card - */ -export async function verifyAgentCardSignatures( - agentCard: AgentCard, - options: { timeout?: number; allowInsecure?: boolean } = {} -): Promise { - const { timeout = 10000, allowInsecure = false } = options; - - if (!agentCard.signatures || agentCard.signatures.length === 0) { - return { - valid: false, - signatures: [], - summary: { - total: 0, - valid: 0, - failed: 0, - errors: ['No signatures present in Agent Card'] - } - }; - } - - const results: SignatureResult[] = []; - const errors: string[] = []; - - // Verify each signature - for (let i = 0; i < agentCard.signatures.length; i++) { - const signature = agentCard.signatures[i]; - if (!signature) { - results.push({ - index: i, - valid: false, - error: 'Signature is undefined' - }); - continue; - } - - try { - const result = await verifySingleSignature(agentCard, signature, i, { - timeout, - allowInsecure - }); - results.push(result); - - if (!result.valid && result.error) { - errors.push(`Signature ${i + 1}: ${result.error}`); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - results.push({ - index: i, - valid: false, - error: errorMessage - }); - errors.push(`Signature ${i + 1}: ${errorMessage}`); - } - } - - const validCount = results.filter(r => r.valid).length; - const totalCount = results.length; - - return { - valid: validCount > 0 && validCount === totalCount, - signatures: results, - summary: { - total: totalCount, - valid: validCount, - failed: totalCount - validCount, - errors - } - }; -} - -/** - * Verify a single signature - */ -async function verifySingleSignature( - agentCard: AgentCard, - signature: AgentCardSignature, - index: number, - options: { timeout?: number; allowInsecure?: boolean } -): Promise { - try { - // Parse the protected header - const header = parseProtectedHeader(signature.protected); - - // Validate header - const headerValidation = validateJWSHeader(header, options.allowInsecure || false); - if (headerValidation.error) { - return { - index, - valid: false, - algorithm: header.alg, - ...(header.kid && { keyId: header.kid }), - error: headerValidation.error - }; - } - - // Get JWKS URI - const jwksUri = header.jku || header.jwks_uri; - if (!jwksUri) { - return { - index, - valid: false, - algorithm: header.alg, - ...(header.kid && { keyId: header.kid }), - error: 'No JWKS URI found in signature header (jku or jwks_uri required)' - }; - } - - // Verify the signature - const isValid = await verifyDetachedJWS( - agentCard, - signature, - header, - jwksUri, - options.timeout - ); - - return { - index, - valid: isValid, - algorithm: header.alg, - ...(header.kid && { keyId: header.kid }), - jwksUri, - details: isValid ? 'Signature verified successfully' : 'Signature verification failed' - }; - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown verification error'; - return { - index, - valid: false, - error: errorMessage - }; - } -} - -/** - * Parse JWS protected header - */ -function parseProtectedHeader(protectedHeader: string): JWSHeader { - try { - const decoded = Buffer.from(protectedHeader, 'base64url').toString('utf-8'); - const header: JWSHeader = JSON.parse(decoded); - return header; - } catch (error) { - throw new Error(`Invalid protected header format: ${error instanceof Error ? error.message : 'Unknown error'}`); - } -} - -/** - * Validate JWS header for security requirements - */ -function validateJWSHeader( - header: JWSHeader, - allowInsecure: boolean -): { valid: boolean; error?: string } { - // Check required fields - if (!header.alg) { - return { valid: false, error: 'Missing algorithm (alg) in signature header' }; - } - - // Reject dangerous algorithms - if (header.alg === 'none') { - return { valid: false, error: 'Algorithm "none" is not allowed for security reasons' }; - } - - // Check for supported algorithms - const supportedAlgorithms = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'PS256', 'PS384', 'PS512', 'EdDSA']; - if (!supportedAlgorithms.includes(header.alg)) { - return { valid: false, error: `Unsupported algorithm: ${header.alg}` }; - } - - // Validate JWKS URI security - const jwksUri = header.jku || header.jwks_uri; - if (jwksUri && !allowInsecure) { - try { - const url = new URL(jwksUri); - if (url.protocol !== 'https:') { - return { valid: false, error: 'JWKS URI must use HTTPS for security' }; - } - } catch { - return { valid: false, error: 'Invalid JWKS URI format' }; - } - } - - return { valid: true }; -} - -/** - * Verify a detached JWS signature against an Agent Card - */ -async function verifyDetachedJWS( - agentCard: AgentCard, - signature: AgentCardSignature, - header: JWSHeader, - jwksUri: string, - timeout = 10000 -): Promise { - try { - // Get jose functions dynamically - const { jwtVerify, createRemoteJWKSet } = await getJose(); - - // Create canonical Agent Card payload (exclude signatures) - const { signatures, ...agentCardWithoutSignatures } = agentCard; - const canonicalPayload = createCanonicalJSON(agentCardWithoutSignatures); - - // Create the detached JWS format for verification - // For detached JWS: {protected}.{payload}.{signature} - const payloadEncoded = Buffer.from(canonicalPayload).toString('base64url'); - const detachedJWS = `${signature.protected}.${payloadEncoded}.${signature.signature}`; - - // Create remote JWKS with timeout - const JWKS = createRemoteJWKSet(new URL(jwksUri), { - timeoutDuration: timeout, - cooldownDuration: 30000, // 30 seconds cooldown - cacheMaxAge: 300000 // 5 minutes cache - }); - - // Verify the JWT - await jwtVerify(detachedJWS, JWKS, { - algorithms: [header.alg as any], - ...(header.kid && { keyid: header.kid }) - }); - - return true; - } catch (error) { - // Log the specific error for debugging but return false for failed verification - console.warn(`JWS verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - return false; - } -} - -/** - * Create canonical JSON representation for consistent signing/verification - * Removes signatures array and sorts keys for deterministic output - */ -function createCanonicalJSON(obj: Record): string { - // Recursive function to sort all object keys - function sortObjectKeys(item: any): any { - if (Array.isArray(item)) { - return item.map(sortObjectKeys); - } else if (item !== null && typeof item === 'object') { - const sorted: Record = {}; - const keys = Object.keys(item).sort(); - for (const key of keys) { - sorted[key] = sortObjectKeys(item[key]); - } - return sorted; - } - return item; - } - - const sortedObj = sortObjectKeys(obj); - return JSON.stringify(sortedObj); -} - -/** - * Format verification results for display - */ -export function formatVerificationResults(result: SignatureVerificationResult): string[] { - const output: string[] = []; - - if (result.summary.total === 0) { - output.push('⚠️ No signatures present in Agent Card'); - return output; - } - - // Summary line - const status = result.valid ? '✅' : '❌'; - output.push(`${status} Signature verification: ${result.summary.valid}/${result.summary.total} signatures valid`); - - // Individual signature results - result.signatures.forEach((sig, idx) => { - const sigStatus = sig.valid ? '✅' : '❌'; - const sigNum = idx + 1; - - let line = `${sigStatus} Signature ${sigNum}/${result.summary.total}`; - - if (sig.algorithm) line += `: ${sig.algorithm}`; - if (sig.keyId) line += ` (key: ${sig.keyId})`; - if (sig.jwksUri) { - try { - const domain = new URL(sig.jwksUri).hostname; - line += ` from ${domain}`; - } catch { - // Ignore URL parsing errors in display - } - } - - output.push(line); - - if (sig.error) { - output.push(` Error: ${sig.error}`); - } - if (sig.details && sig.valid) { - output.push(` ${sig.details}`); - } - }); - - return output; -} - -/** - * Helper function to decode JWS header for inspection without verification - */ -export function inspectSignatureHeader(signature: AgentCardSignature): JWSHeader | null { - try { - return parseProtectedHeader(signature.protected); - } catch { - return null; - } -} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 2627b6d..0000000 --- a/src/types/index.ts +++ /dev/null @@ -1,227 +0,0 @@ -// A2A Protocol Types - Based on v0.3.0 specification (aligned with official TypeScript types) -export interface AgentCard { - protocolVersion: string; // REQUIRED per official A2A spec - name: string; - description: string; - url: string; - preferredTransport?: TransportProtocol; // OPTIONAL per official A2A spec (default: "JSONRPC") - additionalInterfaces?: AgentInterface[]; - provider?: AgentProvider; // Optional per official spec, but organization required if present - iconUrl?: string; - version: string; - documentationUrl?: string; - capabilities: AgentCapabilities; // REQUIRED per official A2A specification - securitySchemes?: Record; - security?: Array>; - defaultInputModes: string[]; // REQUIRED per official A2A specification - defaultOutputModes: string[]; // REQUIRED per official A2A specification - skills: AgentSkill[]; // REQUIRED per official A2A specification - supportsAuthenticatedExtendedCard?: boolean; - signatures?: AgentCardSignature[]; - extensions?: AgentExtension[]; -} - -export interface AgentProvider { - organization: string; - url: string; // REQUIRED per official A2A TypeScript types -} - -export interface AgentCapabilities { - streaming?: boolean; - pushNotifications?: boolean; - stateTransitionHistory?: boolean; -} - -export interface AgentInterface { - url: string; - transport: TransportProtocol; -} - -export type TransportProtocol = 'JSONRPC' | 'GRPC' | 'HTTP+JSON'; - -export interface SecurityScheme { - type: string; - scheme?: string; - bearerFormat?: string; - openIdConnectUrl?: string; - flows?: any; -} - -export interface AgentSkill { - id: string; - name: string; - description: string; - tags: string[]; // REQUIRED per official A2A specification - examples?: string[]; - inputModes?: string[]; - outputModes?: string[]; -} - -export interface AgentCardSignature { - protected: string; - signature: string; -} - -export interface AgentExtension { - name: string; - version: string; - description?: string; -} - -// Validation Types -export type ValidationStrictness = 'conservative' | 'progressive' | 'strict'; - -export interface ValidationOptions { - transport?: TransportProtocol | 'all'; - strictness?: ValidationStrictness; - a2aVersion?: string; - timeout?: number; - compliance?: boolean; - registryReady?: boolean; - testMessage?: string; - skipDynamic?: boolean; - suggestions?: boolean; - showVersionCompat?: boolean; - verbose?: boolean; - skipSignatureVerification?: boolean; -} - -export interface VersionCompatibility { - detectedVersion: string; - targetVersion: string; - compatible: boolean; - mismatches: VersionMismatch[]; - suggestions: string[]; -} - -export interface VersionMismatch { - feature: string; - requiredVersion: string; - detectedVersion: string; - severity: 'error' | 'warning'; - description: string; -} - -export interface ValidationResult { - success: boolean; - score: number; - errors: ValidationError[]; - warnings: ValidationWarning[]; - suggestions: ValidationSuggestion[]; - validations: ValidationCheck[]; - versionInfo?: VersionValidationInfo; - liveTest?: LiveTestResult; - scoringResult?: any; // Will be typed as ScoringResult from scoring module -} - -export interface LiveTestResult { - success: boolean; - endpoint: string; - responseTime: number; - errors: string[]; - request?: any; - response?: any; - timestamp: string; -} - -export interface VersionValidationInfo { - detectedVersion: string; - validatorVersion: string; - strictness: ValidationStrictness; - compatibility: VersionCompatibility; - migrationPath?: string[]; -} - -export interface ValidationCheck { - id: string; - name: string; - status: 'passed' | 'failed' | 'skipped'; - message: string; - duration?: number; - details?: string; -} - -export interface ValidationError { - code: string; - message: string; - field?: string; - line?: number; - severity: 'error'; - fixable?: boolean; - docsUrl?: string; -} - -export interface ValidationWarning { - code: string; - message: string; - field?: string; - line?: number; - severity: 'warning'; - fixable?: boolean; - docsUrl?: string; -} - -export interface ValidationSuggestion { - id: string; - message: string; - severity: 'info'; - impact?: string; - fixable?: boolean; -} - -// HTTP Client Types -export interface HttpClient { - get(url: string, options?: RequestOptions): Promise; -} - -export interface RequestOptions { - timeout?: number; - headers?: Record; - signal?: AbortSignal; -} - -export interface HttpResponse { - status: number; - data: any; - headers: Record; -} - -export class HttpError extends Error { - public status?: number; - public code?: string; - public override cause?: unknown; - - constructor( - message: string, - status?: number, - code?: string, - cause?: unknown - ) { - super(message); - this.name = 'HttpError'; - if (status !== undefined) this.status = status; - if (code !== undefined) this.code = code; - if (cause !== undefined) this.cause = cause; - } -} - -// CLI-specific types -export interface CLIOptions { - strict?: boolean; - progressive?: boolean; - conservative?: boolean; - registryReady?: boolean; - schemaOnly?: boolean; - skipSignature?: boolean; - testLive?: boolean; - json?: boolean; - junit?: boolean; - sarif?: boolean; - errorsOnly?: boolean; - watch?: boolean; - timeout?: string; - fix?: boolean; - verbose?: boolean; - quiet?: boolean; - showVersion?: boolean; -} \ No newline at end of file diff --git a/src/utils/binary-manager.ts b/src/utils/binary-manager.ts index da122e5..8fcab5d 100644 --- a/src/utils/binary-manager.ts +++ b/src/utils/binary-manager.ts @@ -14,8 +14,7 @@ const REPO_OWNER = 'capiscio'; const REPO_NAME = 'capiscio-core'; // Allow version override via env var or package.json -// TODO: Update this to the actual release version you want to pin -const DEFAULT_VERSION = 'v1.0.2'; +const DEFAULT_VERSION = 'v2.2.0'; const VERSION = process.env.CAPISCIO_CORE_VERSION || DEFAULT_VERSION; export class BinaryManager { diff --git a/src/utils/file-utils.ts b/src/utils/file-utils.ts deleted file mode 100644 index 6c7ece0..0000000 --- a/src/utils/file-utils.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { existsSync } from 'fs'; -import { readFile } from 'fs/promises'; -import { AgentCard } from '../types'; - -/** - * Auto-detect agent card files in common locations - */ -export async function detectAgentCard(): Promise { - const candidates = [ - './agent-card.json', - './.well-known/agent-card.json', - './src/agent-card.json', - './public/.well-known/agent-card.json', - './dist/.well-known/agent-card.json', - // Legacy support - './agent.json', - './.well-known/agent.json' - ]; - - for (const candidate of candidates) { - if (existsSync(candidate)) { - return candidate; - } - } - - return null; -} - -/** - * Read and parse an agent card from a file - */ -export async function readAgentCard(filePath: string): Promise { - try { - const content = await readFile(filePath, 'utf-8'); - const parsed = JSON.parse(content); - - if (!parsed || typeof parsed !== 'object') { - throw new Error('Agent card file does not contain a valid JSON object'); - } - - return parsed as AgentCard; - } catch (error) { - if (error instanceof SyntaxError) { - throw new Error(`Invalid JSON in agent card file: ${error.message}`); - } - throw new Error(`Failed to read agent card: ${error instanceof Error ? error.message : 'Unknown error'}`); - } -} - -/** - * Check if a string is a URL - */ -export function isUrl(input: string): boolean { - try { - const url = new URL(input); - // Only consider http and https protocols as valid URLs - return url.protocol === 'http:' || url.protocol === 'https:'; - } catch { - return false; - } -} - -/** - * Resolve input to either a file path or URL - */ -export async function resolveInput(input: string | undefined): Promise<{ type: 'file' | 'url'; value: string } | null> { - // If no input provided, try to auto-detect - if (!input) { - const detected = await detectAgentCard(); - if (detected) { - return { type: 'file', value: detected }; - } - return null; - } - - // Check if input is a URL (starts with http/https or is a valid URL) - if (isUrl(input)) { - return { type: 'url', value: input }; - } - - // Check if input is an existing file - if (existsSync(input)) { - return { type: 'file', value: input }; - } - - // If it looks like a domain (has dots but no slashes/backslashes and no file extension-like ending) - if (input.includes('.') && !input.includes('/') && !input.includes('\\') && - !input.endsWith('.json') && !input.endsWith('.js') && !input.endsWith('.txt') && - !input.includes(' ')) { - return { type: 'url', value: `https://${input}` }; - } - - // Otherwise, it's a missing file - throw new Error(`File not found: ${input}`); -} \ No newline at end of file diff --git a/src/utils/logger.ts b/src/utils/logger.ts deleted file mode 100644 index d88bcae..0000000 --- a/src/utils/logger.ts +++ /dev/null @@ -1,92 +0,0 @@ -import chalk from 'chalk'; - -export class Logger { - private verbose: boolean; - private startTime: number; - - constructor(verbose: boolean = false) { - this.verbose = verbose; - this.startTime = Date.now(); - } - - /** - * Log verbose information - only shown when --verbose is enabled - */ - debug(message: string, metadata?: any): void { - if (!this.verbose) return; - - const timestamp = this.getRelativeTime(); - console.log(chalk.gray(`[${timestamp}] 🔍 ${message}`)); - - if (metadata) { - console.log(chalk.gray(` ${JSON.stringify(metadata, null, 2)}`)); - } - } - - /** - * Log step information - only shown when --verbose is enabled - */ - step(step: string, duration?: number): void { - if (!this.verbose) return; - - const timestamp = this.getRelativeTime(); - const durationText = duration ? chalk.cyan(`(${duration}ms)`) : ''; - console.log(chalk.blue(`[${timestamp}] ⚡ ${step} ${durationText}`)); - } - - /** - * Log timing information - only shown when --verbose is enabled - */ - timing(operation: string, duration: number): void { - if (!this.verbose) return; - - const timestamp = this.getRelativeTime(); - console.log(chalk.yellow(`[${timestamp}] ⏱️ ${operation}: ${duration}ms`)); - } - - /** - * Log error details - only shown when --verbose is enabled - */ - error(message: string, error?: any): void { - if (!this.verbose) return; - - const timestamp = this.getRelativeTime(); - console.log(chalk.red(`[${timestamp}] ❌ ${message}`)); - - if (error) { - if (error.stack) { - console.log(chalk.red(` Stack: ${error.stack}`)); - } else { - console.log(chalk.red(` Error: ${JSON.stringify(error, null, 2)}`)); - } - } - } - - /** - * Log network request details - only shown when --verbose is enabled - */ - network(method: string, url: string, status?: number, duration?: number): void { - if (!this.verbose) return; - - const timestamp = this.getRelativeTime(); - const statusText = status ? chalk.green(`${status}`) : ''; - const durationText = duration ? chalk.cyan(`${duration}ms`) : ''; - console.log(chalk.magenta(`[${timestamp}] 🌐 ${method} ${url} ${statusText} ${durationText}`)); - } - - /** - * Get time elapsed since logger creation - */ - private getRelativeTime(): string { - const elapsed = Date.now() - this.startTime; - return `+${elapsed}ms`; - } - - /** - * Create a timer function for measuring operation duration - */ - timer(): () => number { - const start = Date.now(); - return () => Date.now() - start; - } -} \ No newline at end of file diff --git a/src/utils/semver.ts b/src/utils/semver.ts deleted file mode 100644 index ed3b134..0000000 --- a/src/utils/semver.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Lightweight semver utilities to replace the semver package - * Only implements the functionality we actually need - */ - -export function isValidSemver(version: string): boolean { - const semverRegex = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; - return semverRegex.test(version); -} - -export function parseSemver(version: string): { major: number; minor: number; patch: number; prerelease?: string; build?: string } | null { - const semverRegex = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; - const match = version.match(semverRegex); - - if (!match || !match[1] || !match[2] || !match[3]) { - return null; - } - - const result: { major: number; minor: number; patch: number; prerelease?: string; build?: string } = { - major: parseInt(match[1], 10), - minor: parseInt(match[2], 10), - patch: parseInt(match[3], 10) - }; - - if (match[4]) { - result.prerelease = match[4]; - } - - if (match[5]) { - result.build = match[5]; - } - - return result; -} - -export function semverCompare(a: string, b: string): number { - const parsedA = parseSemver(a); - const parsedB = parseSemver(b); - - if (!parsedA || !parsedB) { - throw new Error('Invalid semver format'); - } - - // Compare major version - if (parsedA.major !== parsedB.major) { - return parsedA.major - parsedB.major; - } - - // Compare minor version - if (parsedA.minor !== parsedB.minor) { - return parsedA.minor - parsedB.minor; - } - - // Compare patch version - if (parsedA.patch !== parsedB.patch) { - return parsedA.patch - parsedB.patch; - } - - // Handle prerelease versions - if (parsedA.prerelease && !parsedB.prerelease) { - return -1; // a is prerelease, b is not, so a < b - } - - if (!parsedA.prerelease && parsedB.prerelease) { - return 1; // a is not prerelease, b is, so a > b - } - - if (parsedA.prerelease && parsedB.prerelease) { - return parsedA.prerelease.localeCompare(parsedB.prerelease); - } - - return 0; // versions are equal -} - -export function semverGte(a: string, b: string): boolean { - return semverCompare(a, b) >= 0; -} - -export function semverSatisfies(version: string, range: string): boolean { - // Simple range matching - supports ^x.y.z and exact matches - if (range.startsWith('^')) { - const targetVersion = range.slice(1); - const target = parseSemver(targetVersion); - const current = parseSemver(version); - - if (!target || !current) { - return false; - } - - // Major version must match, minor and patch can be greater - return current.major === target.major && semverCompare(version, targetVersion) >= 0; - } - - // Exact match - return version === range; -} \ No newline at end of file diff --git a/src/validator/a2a-validator.ts b/src/validator/a2a-validator.ts deleted file mode 100644 index 34d6737..0000000 --- a/src/validator/a2a-validator.ts +++ /dev/null @@ -1,1478 +0,0 @@ -import { AgentCard, ValidationResult, ValidationOptions, HttpClient, VersionMismatch, TransportProtocol } from '../types'; -import { FetchHttpClient } from './http-client'; -import { semverCompare, isValidSemver } from '../utils/semver'; -import { Logger } from '../utils/logger'; - -/** - * A2A Validator with URL support and embedded validation logic - * - * This validator combines schema validation, version checking, and HTTP endpoint testing - * into a single self-contained validator optimized for CLI usage. - */ -export class A2AValidator { - private httpClient: HttpClient; - private logger: Logger; - - constructor(httpClient?: HttpClient) { - this.httpClient = httpClient || new FetchHttpClient(); - this.logger = new Logger(false); // Will be enabled per validation - } - - async validate( - input: AgentCard | string, - options: ValidationOptions = {} - ): Promise { - // Initialize logger with verbose setting - this.logger = new Logger(options.verbose || false); - - // Update HTTP client with logger if it's our default client - if (this.httpClient instanceof FetchHttpClient && options.verbose) { - this.httpClient = new FetchHttpClient(this.logger); - } - - this.logger.debug('Starting A2A validation', { - inputType: typeof input, - options: { ...options, verbose: undefined } // Don't log verbose flag itself - }); - - try { - let agentCard: AgentCard; - let usedLegacyEndpoint = false; - let discoveryUrl = ''; - - // Determine if input is a URL or local file path - if (typeof input === 'string') { - const isUrl = this.isValidUrl(input) || (!input.includes('\\') && !input.includes('/') && !input.endsWith('.json')); - this.logger.debug(`Input type detected: ${isUrl ? 'URL' : 'local file'}`, { input, isUrl }); - - if (isUrl && !options.skipDynamic) { - // Remote URL - fetch the agent card - this.logger.step('Fetching agent card from URL'); - const fetchTimer = this.logger.timer(); - const fetchResult = await this.fetchAgentCard(input); - const fetchDuration = fetchTimer(); - this.logger.timing('Agent card fetch', fetchDuration); - - agentCard = fetchResult.card; - usedLegacyEndpoint = fetchResult.usedLegacyEndpoint; - discoveryUrl = fetchResult.discoveryUrl; - - this.logger.debug('Agent card fetched successfully', { - usedLegacyEndpoint, - discoveryUrl, - cardSize: JSON.stringify(agentCard).length - }); - } else if (isUrl && options.skipDynamic) { - // URL provided but schema-only mode requested - return { - success: false, - score: 0, - errors: [{ - code: 'SCHEMA_ONLY_NO_URL', - message: 'Schema-only validation requires a local agent card file, not a URL', - severity: 'error' as const, - fixable: true - }], - warnings: [], - suggestions: [{ - id: 'use_local_file', - message: 'To use schema-only validation, provide a local agent card file instead of a URL', - severity: 'info' as const, - impact: 'Schema validation cannot be performed on remote URLs', - fixable: true - }], - validations: [] - }; - } else { - // Local file path - read and parse the file - try { - const fs = await import('fs/promises'); - const fileContent = await fs.readFile(input, 'utf-8'); - agentCard = JSON.parse(fileContent); - } catch (error) { - return { - success: false, - score: 0, - errors: [{ - code: 'FILE_READ_ERROR', - message: `Failed to read agent card file: ${error instanceof Error ? error.message : 'Unknown error'}`, - severity: 'error' as const, - fixable: false - }], - warnings: [], - suggestions: [], - validations: [] - }; - } - } - } else { - agentCard = input; - } - - // Perform validation based on strictness mode - const result = await this.performValidation(agentCard, options, input); - - // Add legacy endpoint warning if detected - if (usedLegacyEndpoint) { - result.warnings.push({ - code: 'LEGACY_DISCOVERY_ENDPOINT', - message: `Agent discovered via legacy endpoint (${discoveryUrl}). The A2A v0.3.0 specification recommends using /.well-known/agent-card.json`, - field: 'discovery', - severity: 'warning', - fixable: true - }); - - result.suggestions.push({ - id: 'migrate_legacy_endpoint', - message: 'Consider migrating from legacy /.well-known/agent.json to /.well-known/agent-card.json for future compatibility', - severity: 'info', - impact: 'Future A2A specification versions may not support the legacy agent.json endpoint', - fixable: true - }); - } - - return result; - - } catch (error) { - return { - success: false, - score: 0, - errors: [{ - code: 'VALIDATION_FAILED', - message: `Validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - severity: 'error' - }], - warnings: [], - suggestions: [], - validations: [] - }; - } - } - - private async performValidation(card: AgentCard, options: ValidationOptions, originalInput?: string | AgentCard): Promise { - const validations: any[] = []; - const errors: any[] = []; - const warnings: any[] = []; - const suggestions: any[] = []; - - // Schema validation timing is handled within validateSchema - - // 1. Schema Validation - const schemaResult = await this.validateSchema(card); - validations.push({ - id: 'schema_validation', - name: 'Schema Validation', - status: schemaResult.success ? 'passed' : 'failed', - message: schemaResult.success - ? 'Agent card conforms to A2A v0.3.0 schema' - : `Schema validation failed with ${schemaResult.errors.length} error(s)`, - duration: schemaResult.duration || 0, - details: schemaResult.success - ? 'Agent card structure is valid' - : 'Agent card does not conform to A2A v0.3.0 schema' - }); - - errors.push(...schemaResult.errors); - warnings.push(...schemaResult.warnings); - - // 2. Version Compatibility Check - const versionResult = this.validateVersionCompatibility(card, options.strictness || 'progressive'); - validations.push({ - id: 'v030_features', - name: 'A2A v0.3.0 Features', - status: versionResult.compatible ? 'passed' : 'failed', - message: versionResult.compatible - ? 'All v0.3.0 features are properly configured' - : 'Version compatibility issues detected', - duration: 0, - details: 'Validation of v0.3.0 specific features and capabilities' - }); - - // Add version-related errors/warnings based on strictness - if (options.strictness === 'strict') { - versionResult.mismatches.forEach(mismatch => { - if (mismatch.severity === 'warning') { - errors.push({ - code: 'STRICT_VERSION_MISMATCH', - message: `Strict mode: ${mismatch.description}`, - field: mismatch.feature, - severity: 'error' - }); - } else { - errors.push({ - code: 'VERSION_MISMATCH_ERROR', - message: mismatch.description, - field: mismatch.feature, - severity: 'error' - }); - } - }); - } else { - versionResult.mismatches.forEach(mismatch => { - if (mismatch.severity === 'error') { - errors.push({ - code: 'VERSION_MISMATCH_ERROR', - message: mismatch.description, - field: mismatch.feature, - severity: 'error' - }); - } else { - warnings.push({ - code: 'VERSION_FEATURE_MISMATCH', - message: `${mismatch.description}: Update protocolVersion to "${mismatch.requiredVersion}" or remove feature`, - field: mismatch.feature, - severity: 'warning', - fixable: true - }); - } - }); - } - - // 3. Signature Verification (enabled by default, can be skipped) - if (!options.skipSignatureVerification && card.signatures && card.signatures.length > 0) { - const { verifyAgentCardSignatures } = await import('../signature-verification'); - const signatureTimer = this.logger.timer(); - this.logger.step('Verifying JWS signatures'); - - try { - const signatureResult = await verifyAgentCardSignatures(card, { timeout: 10000 }); - const signatureDuration = signatureTimer(); - this.logger.timing('Signature verification', signatureDuration); - - const verifiedCount = signatureResult.summary.valid; - const failedCount = signatureResult.summary.failed; - const totalCount = signatureResult.summary.total; - - validations.push({ - id: 'signature_verification', - name: 'JWS Signature Verification', - status: failedCount === 0 ? 'passed' : 'failed', - message: `${verifiedCount} of ${totalCount} signatures verified successfully`, - duration: signatureDuration, - details: `Verified ${verifiedCount} valid signatures, ${failedCount} failed signatures` - }); - - // Add errors for failed signatures - signatureResult.signatures.forEach((result, index) => { - if (!result.valid) { - errors.push({ - code: 'SIGNATURE_VERIFICATION_FAILED', - message: `Signature ${index + 1} verification failed: ${result.error || 'Unknown error'}`, - field: `signatures[${index}]`, - severity: 'error' - }); - } - }); - - // Add warnings for signature issues - if (verifiedCount > 0 && failedCount > 0) { - warnings.push({ - code: 'PARTIAL_SIGNATURE_VERIFICATION', - message: `Only ${verifiedCount} of ${totalCount} signatures could be verified`, - field: 'signatures', - severity: 'warning', - fixable: true - }); - } - - // Add summary errors if any exist - if (signatureResult.summary.errors.length > 0) { - signatureResult.summary.errors.forEach(error => { - warnings.push({ - code: 'SIGNATURE_VERIFICATION_WARNING', - message: error, - field: 'signatures', - severity: 'warning', - fixable: true - }); - }); - } - - } catch (error) { - const signatureDuration = signatureTimer(); - this.logger.timing('Signature verification (failed)', signatureDuration); - - validations.push({ - id: 'signature_verification', - name: 'JWS Signature Verification', - status: 'failed', - message: 'Signature verification failed due to system error', - duration: signatureDuration, - details: `Error: ${error instanceof Error ? error.message : 'Unknown error'}` - }); - - errors.push({ - code: 'SIGNATURE_VERIFICATION_ERROR', - message: `Signature verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - field: 'signatures', - severity: 'error' - }); - } - } else if (!options.skipSignatureVerification && (!card.signatures || card.signatures.length === 0)) { - // Signature verification is enabled by default but no signatures present - validations.push({ - id: 'signature_verification', - name: 'JWS Signature Verification', - status: 'skipped', - message: 'No signatures found to verify', - duration: 0, - details: 'Agent card does not contain any signatures' - }); - - warnings.push({ - code: 'NO_SIGNATURES_FOUND', - message: 'No signatures are present in the agent card. Consider adding signatures to improve trust.', - field: 'signatures', - severity: 'warning', - fixable: true - }); - } else if (options.skipSignatureVerification && card.signatures && card.signatures.length > 0) { - // Signature verification was explicitly skipped but signatures are present - validations.push({ - id: 'signature_verification', - name: 'JWS Signature Verification', - status: 'skipped', - message: 'Signature verification was explicitly skipped', - duration: 0, - details: 'Agent card contains signatures but verification was skipped by user request' - }); - - warnings.push({ - code: 'SIGNATURE_VERIFICATION_SKIPPED', - message: 'Signature verification was skipped despite signatures being present. This reduces trust verification.', - field: 'signatures', - severity: 'warning', - fixable: true - }); - } - - // 4. Transport Endpoint Testing (if not skipped) - if (!options.skipDynamic && typeof originalInput === 'string' && this.isValidUrl(originalInput)) { - const transportResult = await this.validateTransportEndpoints(card, options); - validations.push(...transportResult.validations); - errors.push(...transportResult.errors); - warnings.push(...transportResult.warnings); - } - - // 5. Additional validations based on detected issues - this.addAdditionalWarnings(card, warnings); - - // 6. Strictness-specific validations - this.applyStrictnessValidations(card, options, errors, warnings); - - // Calculate score - const totalChecks = validations.length; - const passedChecks = validations.filter(v => v.status === 'passed').length; - const score = totalChecks > 0 ? Math.round((passedChecks / totalChecks) * 100) : 0; - - const success = errors.length === 0; - - return { - success, - score, - errors, - warnings, - suggestions, - validations, - versionInfo: { - detectedVersion: card.protocolVersion || 'undefined', - validatorVersion: '0.3.0', - strictness: options.strictness || 'progressive', - compatibility: versionResult, - migrationPath: versionResult.suggestions - } - }; - } - - private async validateSchema(card: AgentCard): Promise<{ success: boolean; errors: any[]; warnings: any[]; duration: number }> { - const startTime = Date.now(); - const errors: any[] = []; - const warnings: any[] = []; - - this.logger.step('Validating schema structure'); - - // Required fields per official A2A specification (matching A2A TypeScript types) - // See: A2A/types/src/types.ts - AgentCard interface - const requiredFields = [ - 'name', - 'description', - 'url', - 'version', - 'protocolVersion', - 'capabilities', - 'defaultInputModes', - 'defaultOutputModes', - 'skills' - ]; - - this.logger.debug('Checking required fields', { - requiredFields - }); - - requiredFields.forEach(field => { - if (!this.getNestedValue(card, field)) { - this.logger.debug(`Missing required field: ${field}`); - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `${field}: Required`, - field: field, - severity: 'error', - fixable: true - }); - } - }); - - // Version format validation - if (card.version && !isValidSemver(card.version)) { - this.logger.debug(`Invalid version format: ${card.version}`); - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: 'version: Version must follow semver format', - field: 'version', - severity: 'error', - fixable: true - }); - } - - // Provider validation: provider itself is optional per A2A spec, - // but if present, both organization and url are required (per AgentProvider interface) - if (card.provider && !card.provider.organization) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: 'provider.organization: Required', - field: 'provider.organization', - severity: 'error', - fixable: true - }); - } - - // Provider URL validation (required per official A2A TypeScript specification) - if (card.provider && !card.provider.url) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: 'provider.url: Required when provider is specified', - field: 'provider.url', - severity: 'error', - fixable: true - }); - } else if (card.provider && card.provider.url && !this.isValidUrl(card.provider.url)) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: 'provider.url: Must be a valid URL', - field: 'provider.url', - severity: 'error', - fixable: true - }); - } - - // Transport protocol validation (A2A v0.3.0 Section 3.2) - if (card.preferredTransport) { - const validTransports = ['JSONRPC', 'GRPC', 'HTTP+JSON']; - if (!validTransports.includes(card.preferredTransport)) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `preferredTransport: Must be one of ${validTransports.join(', ')}`, - field: 'preferredTransport', - severity: 'error', - fixable: true - }); - } - } - - // URL format and HTTPS enforcement (RFC 0001 R1, A2A §5.3) - if (card.url && !this.isValidHttpsUrl(card.url)) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: 'url: Must be a valid HTTPS URL (HTTP not allowed per A2A specification)', - field: 'url', - severity: 'error', - fixable: true - }); - } - - // Check for localhost, private IPs (SSRF protection per RFC 0001) - if (card.url && this.isPrivateOrLocalUrl(card.url)) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: 'url: Cannot use localhost, private IP addresses, or non-routable addresses', - field: 'url', - severity: 'error', - fixable: true - }); - } - - // Protocol version validation - if (card.protocolVersion) { - const validVersions = ['0.1.0', '0.2.0', '0.3.0']; - if (!validVersions.includes(card.protocolVersion)) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `protocolVersion: Must be one of ${validVersions.join(', ')}`, - field: 'protocolVersion', - severity: 'error', - fixable: true - }); - } - } - - // Optional URL fields validation (must be HTTPS if present) - const urlFields = ['iconUrl', 'documentationUrl', 'termsOfServiceUrl', 'privacyPolicyUrl']; - urlFields.forEach(field => { - const value = this.getNestedValue(card, field); - if (value && !this.isValidHttpsUrl(value)) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `${field}: Must be a valid HTTPS URL`, - field: field, - severity: 'error', - fixable: true - }); - } - }); - - // Skills validation - if (card.skills) { - // Validate skills array is not empty (required to have at least one skill) - if (card.skills.length === 0) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: 'skills: Must contain at least one skill. Agents must declare their capabilities.', - field: 'skills', - severity: 'error', - fixable: true - }); - } - - const skillIds = new Set(); - - card.skills.forEach((skill, index) => { - if (!skill.id) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `skills.${index}.id: Required`, - field: `skills.${index}.id`, - severity: 'error', - fixable: true - }); - } else { - // Check for duplicate skill IDs - if (skillIds.has(skill.id)) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `skills.${index}.id: Duplicate skill ID '${skill.id}' - skill IDs must be unique within agent card`, - field: `skills.${index}.id`, - severity: 'error', - fixable: true - }); - } else { - skillIds.add(skill.id); - } - - // Skill ID length validation - if (skill.id.length > 200) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `skills.${index}.id: Maximum 200 characters allowed`, - field: `skills.${index}.id`, - severity: 'error', - fixable: true - }); - } - } - - if (!skill.name) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `skills.${index}.name: Required`, - field: `skills.${index}.name`, - severity: 'error', - fixable: true - }); - } else if (skill.name.length > 200) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `skills.${index}.name: Maximum 200 characters allowed`, - field: `skills.${index}.name`, - severity: 'error', - fixable: true - }); - } - - if (!skill.description) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `skills.${index}.description: Required`, - field: `skills.${index}.description`, - severity: 'error', - fixable: true - }); - } else if (skill.description.length > 2000) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `skills.${index}.description: Maximum 2000 characters allowed`, - field: `skills.${index}.description`, - severity: 'error', - fixable: true - }); - } - - // Validate tags field (required per A2A specification) - if (!skill.tags || !Array.isArray(skill.tags)) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `skills.${index}.tags: Required field. Tags help categorize and discover agent skills.`, - field: `skills.${index}.tags`, - severity: 'error', - fixable: true - }); - } else if (skill.tags.length === 0) { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `skills.${index}.tags: Must contain at least one tag`, - field: `skills.${index}.tags`, - severity: 'error', - fixable: true - }); - } - - if (skill.examples && Array.isArray(skill.examples)) { - skill.examples.forEach((example, exampleIndex) => { - if (typeof example !== 'string') { - errors.push({ - code: 'SCHEMA_VALIDATION_ERROR', - message: `skills.${index}.examples.${exampleIndex}: Expected string, received ${typeof example}`, - field: `skills.${index}.examples.${exampleIndex}`, - severity: 'error', - fixable: true - }); - } - }); - } - }); - } - - // A2A Transport Consistency Validation (§5.6.4) - this.validateTransportConsistency(card, errors, warnings); - - const duration = Date.now() - startTime; - const success = errors.length === 0; - - this.logger.step('Schema validation completed', duration); - this.logger.debug('Schema validation results', { - success, - errorCount: errors.length, - warningCount: warnings.length - }); - - return { success, errors, warnings, duration }; - } - - private validateVersionCompatibility(card: AgentCard, _strictness: string) { - const detectedVersion = card.protocolVersion; - const targetVersion = '0.3.0'; - const mismatches: VersionMismatch[] = []; - - // If no protocolVersion is specified, check for v0.3.0 features - if (!detectedVersion) { - if (card.capabilities?.pushNotifications) { - mismatches.push({ - feature: 'capabilities.pushNotifications', - requiredVersion: '0.3.0', - detectedVersion: 'undefined', - severity: 'warning', - description: 'Push notifications capability requires protocolVersion to be specified (minimum 0.3.0)' - }); - } - - if (card.additionalInterfaces && card.additionalInterfaces.length > 0) { - mismatches.push({ - feature: 'additionalInterfaces', - requiredVersion: '0.3.0', - detectedVersion: 'undefined', - severity: 'warning', - description: 'additionalInterfaces field requires protocolVersion to be specified (minimum 0.3.0)' - }); - } - } else { - // Check if detected version is compatible with used features - if (card.capabilities?.streaming && !this.isVersionCompatible(detectedVersion, '0.3.0')) { - mismatches.push({ - feature: 'capabilities.streaming', - requiredVersion: '0.3.0', - detectedVersion, - severity: 'warning', - description: 'Streaming capability was introduced in A2A v0.3.0' - }); - } - } - - const compatible = mismatches.length === 0; - const suggestions = compatible ? [] : ['Update protocolVersion to "0.3.0" to match used features']; - - return { - detectedVersion: detectedVersion || 'undefined', - targetVersion, - compatible, - mismatches, - suggestions - }; - } - - private validateTransportConsistency(card: AgentCard, errors: any[], warnings: any[]): void { - // A2A §5.6.4 Transport Consistency Requirements - - // 1. preferredTransport must be present (already checked in required fields) - - // 2. Interface completeness: additionalInterfaces SHOULD include main URL - if (card.additionalInterfaces && card.additionalInterfaces.length > 0) { - const mainUrlInterface = card.additionalInterfaces.find( - iface => iface.url === card.url && iface.transport === card.preferredTransport - ); - - if (!mainUrlInterface) { - warnings.push({ - code: 'TRANSPORT_INTERFACE_COMPLETENESS', - message: 'additionalInterfaces should include an entry for the main URL and preferredTransport for completeness', - field: 'additionalInterfaces', - severity: 'warning', - fixable: true - }); - } - - // 3. No conflicts: same URL must not declare conflicting transports - const urlTransportMap = new Map(); - // preferredTransport is optional but defaults to "JSONRPC" per A2A spec - const effectiveTransport = card.preferredTransport || 'JSONRPC'; - urlTransportMap.set(card.url, effectiveTransport); - - card.additionalInterfaces.forEach((iface, index) => { - const existingTransport = urlTransportMap.get(iface.url); - if (existingTransport && existingTransport !== iface.transport) { - errors.push({ - code: 'TRANSPORT_URL_CONFLICT', - message: `Conflicting transport protocols for URL ${iface.url}: ${existingTransport} vs ${iface.transport}`, - field: `additionalInterfaces.${index}`, - severity: 'error', - fixable: true - }); - } else { - urlTransportMap.set(iface.url, iface.transport); - } - }); - } - - // 4. Minimum transport requirement is satisfied by having preferredTransport (already validated) - } - - private addAdditionalWarnings(card: AgentCard, warnings: any[]) { - // Check for gRPC without streaming - if (card.additionalInterfaces) { - const hasGrpc = card.additionalInterfaces.some(iface => iface.transport === 'GRPC'); - const hasStreaming = card.capabilities?.streaming; - - if (hasGrpc && !hasStreaming) { - warnings.push({ - code: 'GRPC_WITHOUT_STREAMING', - message: 'gRPC transport is configured but streaming capability is not enabled', - severity: 'warning', - fixable: true - }); - } - } - } - - private applyStrictnessValidations(card: AgentCard, options: ValidationOptions, errors: any[], warnings: any[]): void { - // In strict mode, promote certain warnings to errors - if (options.strictness === 'strict') { - // Find GRPC_WITHOUT_STREAMING warnings and convert to errors - const grpcWarningIndex = warnings.findIndex(w => w.code === 'GRPC_WITHOUT_STREAMING'); - if (grpcWarningIndex !== -1) { - const warning = warnings.splice(grpcWarningIndex, 1)[0]; - errors.push({ - ...warning, - severity: 'error', - message: `Strict mode: ${warning.message}` - }); - } - } - } - - private async fetchAgentCard(url: string): Promise<{ - card: AgentCard; - usedLegacyEndpoint: boolean; - discoveryUrl: string; - }> { - // Check if the URL is already a well-known endpoint - if (url.includes('/.well-known/agent')) { - const response = await this.httpClient.get(url); - return { - card: response.data as AgentCard, - usedLegacyEndpoint: url.includes('/agent.json'), - discoveryUrl: url - }; - } - - // First try direct URL - try { - const response = await this.httpClient.get(url); - if (response.data && typeof response.data === 'object' && - (response.data.name || response.data.protocolVersion || response.data.provider)) { - return { - card: response.data as AgentCard, - usedLegacyEndpoint: false, - discoveryUrl: url - }; - } - } catch { - // Continue to well-known endpoint fallback - } - - // Try new agent-card.json endpoint - try { - const wellKnownUrl = this.constructWellKnownUrl(url); - const response = await this.httpClient.get(wellKnownUrl); - return { - card: response.data as AgentCard, - usedLegacyEndpoint: false, - discoveryUrl: wellKnownUrl - }; - } catch { - // Try legacy agent.json endpoint - const legacyWellKnownUrl = this.constructLegacyWellKnownUrl(url); - const response = await this.httpClient.get(legacyWellKnownUrl); - return { - card: response.data as AgentCard, - usedLegacyEndpoint: true, - discoveryUrl: legacyWellKnownUrl - }; - } - } - - private constructWellKnownUrl(url: string): string { - try { - const urlObj = new URL(url); - return `${urlObj.protocol}//${urlObj.host}/.well-known/agent-card.json`; - } catch { - return `https://${url}/.well-known/agent-card.json`; - } - } - - private constructLegacyWellKnownUrl(url: string): string { - try { - const urlObj = new URL(url); - return `${urlObj.protocol}//${urlObj.host}/.well-known/agent.json`; - } catch { - return `https://${url}/.well-known/agent.json`; - } - } - - private getNestedValue(obj: any, path: string): any { - return path.split('.').reduce((current, key) => current?.[key], obj); - } - - private isValidUrl(url: string): boolean { - try { - new URL(url); - return true; - } catch { - // Additional check for URLs without protocol - if (url.includes('://') || url.startsWith('www.') || url.includes('.') && !url.includes('\\') && !url.includes('/') && !url.endsWith('.json')) { - try { - new URL('https://' + url); - return true; - } catch { - return false; - } - } - return false; - } - } - - private isValidHttpsUrl(url: string): boolean { - try { - const parsedUrl = new URL(url); - return parsedUrl.protocol === 'https:'; - } catch { - return false; - } - } - - private isPrivateOrLocalUrl(url: string): boolean { - try { - const parsedUrl = new URL(url); - const hostname = parsedUrl.hostname; - - // Check for localhost - if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') { - return true; - } - - // Check for private IP ranges - if (this.isPrivateIPAddress(hostname)) { - return true; - } - - return false; - } catch { - return false; - } - } - - private isPrivateIPAddress(hostname: string): boolean { - // IPv4 private ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 - // IPv4 link-local: 169.254.0.0/16 - const ipv4PrivateRegex = /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.)/; - - // IPv6 private ranges: fc00::/7, fe80::/10 - const ipv6PrivateRegex = /^(fc|fd|fe[89ab])/i; - - if (ipv4PrivateRegex.test(hostname)) { - return true; - } - - if (ipv6PrivateRegex.test(hostname)) { - return true; - } - - return false; - } - - private isVersionCompatible(version: string, required: string): boolean { - try { - return semverCompare(version, required) >= 0; - } catch { - return false; - } - } - - /** - * Validate transport endpoints by testing connectivity and basic functionality - */ - private async validateTransportEndpoints(card: AgentCard, options: ValidationOptions): Promise<{ - validations: any[]; - errors: any[]; - warnings: any[]; - }> { - const validations: any[] = []; - const errors: any[] = []; - const warnings: any[] = []; - - this.logger.step('Testing transport endpoints'); - - // 1. Test primary endpoint (main URL) - this is REQUIRED so failures are errors - await this.testEndpointConnectivity(card.url, 'Primary Endpoint', validations, errors, warnings, options, true); - - // 2. Test preferred transport endpoint - this is REQUIRED so failures are errors - // preferredTransport is optional but defaults to "JSONRPC" per A2A spec - const effectiveTransport = card.preferredTransport || 'JSONRPC'; - await this.testTransportProtocol(card.url, effectiveTransport, 'Preferred Transport', validations, errors, warnings, options, true); - - // 3. Test additional interfaces if present - these are OPTIONAL so failures are warnings - if (card.additionalInterfaces && card.additionalInterfaces.length > 0) { - for (const iface of card.additionalInterfaces) { - await this.testEndpointConnectivity(iface.url, `${iface.transport} Interface`, validations, errors, warnings, options, false); - await this.testTransportProtocol(iface.url, iface.transport, `${iface.transport} Protocol`, validations, errors, warnings, options, false); - } - } - - return { validations, errors, warnings }; - } - - /** - * Test basic HTTP connectivity to an endpoint - */ - private async testEndpointConnectivity( - url: string, - testName: string, - validations: any[], - errors: any[], - warnings: any[], - options: ValidationOptions, - isPrimary: boolean = false - ): Promise { - const timer = this.logger.timer(); - this.logger.debug(`Testing connectivity to ${url}`); - - try { - const response = await this.httpClient.get(url, { - timeout: options.timeout || 10000 - }); - const duration = timer(); - - if (response.status >= 200 && response.status < 300) { - validations.push({ - id: `endpoint_connectivity_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} Connectivity`, - status: 'passed', - message: `Endpoint accessible (${response.status})`, - duration, - details: `HTTP ${response.status} response received in ${duration}ms` - }); - this.logger.debug(`✓ ${testName} connectivity test passed`, { url, status: response.status, duration }); - } else { - validations.push({ - id: `endpoint_connectivity_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} Connectivity`, - status: 'failed', - message: `Endpoint returned ${response.status}`, - duration, - details: `HTTP ${response.status} response indicates endpoint issues` - }); - - // Primary endpoint failures are errors, additional interfaces are warnings - if (isPrimary) { - errors.push({ - code: 'PRIMARY_ENDPOINT_HTTP_ERROR', - message: `Primary endpoint returned HTTP ${response.status}`, - field: 'url', - severity: 'error', - fixable: true - }); - } else { - warnings.push({ - code: 'ADDITIONAL_ENDPOINT_HTTP_ERROR', - message: `${testName} returned HTTP ${response.status}`, - field: 'additionalInterfaces', - severity: 'warning', - fixable: true - }); - } - this.logger.debug(`⚠ ${testName} connectivity test warning`, { url, status: response.status, duration }); - } - } catch (error) { - const duration = timer(); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - - validations.push({ - id: `endpoint_connectivity_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} Connectivity`, - status: 'failed', - message: `Endpoint unreachable`, - duration, - details: `Connection failed: ${errorMessage}` - }); - - // Primary endpoint failures are errors, additional interfaces are warnings - if (isPrimary) { - errors.push({ - code: 'PRIMARY_ENDPOINT_UNREACHABLE', - message: `Primary endpoint is unreachable: ${errorMessage}`, - field: 'url', - severity: 'error', - fixable: true - }); - } else { - warnings.push({ - code: 'ADDITIONAL_ENDPOINT_UNREACHABLE', - message: `${testName} is unreachable: ${errorMessage}`, - field: 'additionalInterfaces', - severity: 'warning', - fixable: true - }); - } - - this.logger.debug(`✗ ${testName} connectivity test failed`, { url, error: errorMessage, duration }); - } - } - - /** - * Test transport protocol specific functionality - */ - private async testTransportProtocol( - url: string, - transport: TransportProtocol, - testName: string, - validations: any[], - errors: any[], - warnings: any[], - options: ValidationOptions, - isPrimary: boolean = false - ): Promise { - const timer = this.logger.timer(); - this.logger.debug(`Testing ${transport} protocol on ${url}`); - - try { - switch (transport) { - case 'JSONRPC': { - await this.testJsonRpcEndpoint(url, testName, validations, errors, warnings, options); - break; - } - case 'GRPC': { - await this.testGrpcEndpoint(url, testName, validations, errors, warnings, options); - break; - } - case 'HTTP+JSON': { - await this.testHttpJsonEndpoint(url, testName, validations, errors, warnings, options); - break; - } - default: { - const duration = timer(); - validations.push({ - id: `transport_protocol_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} Protocol Test`, - status: 'skipped', - message: `Unknown transport protocol: ${transport}`, - duration, - details: `Transport protocol ${transport} is not supported for testing` - }); - } - } - } catch (error) { - const duration = timer(); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - - validations.push({ - id: `transport_protocol_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} Protocol Test`, - status: 'failed', - message: `Protocol test failed`, - duration, - details: `${transport} protocol test failed: ${errorMessage}` - }); - - // Primary transport failures are errors, additional interfaces are warnings - if (isPrimary) { - errors.push({ - code: 'PRIMARY_TRANSPORT_PROTOCOL_ERROR', - message: `Primary transport ${testName} failed: ${errorMessage}`, - field: 'preferredTransport', - severity: 'error', - fixable: true - }); - } else { - warnings.push({ - code: 'ADDITIONAL_TRANSPORT_PROTOCOL_ERROR', - message: `Additional transport ${testName} failed: ${errorMessage}`, - field: 'additionalInterfaces', - severity: 'warning', - fixable: true - }); - } - } - } - - /** - * Test JSON-RPC 2.0 endpoint - */ - private async testJsonRpcEndpoint( - url: string, - testName: string, - validations: any[], - errors: any[], - warnings: any[], - options: ValidationOptions - ): Promise { - const timer = this.logger.timer(); - - try { - // Test with a basic JSON-RPC 2.0 request (method discovery) - const payload = { - jsonrpc: '2.0', - method: 'rpc.discover', - id: 1 - }; - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - body: JSON.stringify(payload), - signal: AbortSignal.timeout(options.timeout || 10000) - }); - - const duration = timer(); - const contentType = response.headers.get('content-type') || ''; - - if (!contentType.includes('application/json')) { - validations.push({ - id: `jsonrpc_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} JSON-RPC`, - status: 'failed', - message: 'Invalid content-type for JSON-RPC', - duration, - details: `Expected application/json, got ${contentType}` - }); - warnings.push({ - code: 'JSONRPC_INVALID_CONTENT_TYPE', - message: `JSON-RPC endpoint should return application/json content-type, got ${contentType}`, - field: 'preferredTransport', - severity: 'warning', - fixable: true - }); - return; - } - - const responseData = await response.json(); - - // Check if response follows JSON-RPC 2.0 structure - if (response.status === 200 && responseData && (responseData.result !== undefined || responseData.error !== undefined)) { - validations.push({ - id: `jsonrpc_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} JSON-RPC`, - status: 'passed', - message: 'JSON-RPC 2.0 endpoint responding correctly', - duration, - details: 'Endpoint accepts JSON-RPC requests and returns valid responses' - }); - } else if (response.status === 405 || response.status === 404) { - validations.push({ - id: `jsonrpc_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} JSON-RPC`, - status: 'failed', - message: `JSON-RPC endpoint returned ${response.status}`, - duration, - details: 'Method not allowed or not found - endpoint may not support JSON-RPC' - }); - warnings.push({ - code: 'JSONRPC_METHOD_NOT_SUPPORTED', - message: `JSON-RPC endpoint returned ${response.status} - may not support JSON-RPC protocol`, - field: 'preferredTransport', - severity: 'warning', - fixable: true - }); - } else { - validations.push({ - id: `jsonrpc_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} JSON-RPC`, - status: 'passed', - message: 'JSON-RPC endpoint accessible', - duration, - details: `Endpoint responds to JSON-RPC requests (${response.status})` - }); - } - } catch (error) { - const duration = timer(); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - - validations.push({ - id: `jsonrpc_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} JSON-RPC`, - status: 'failed', - message: 'JSON-RPC endpoint test failed', - duration, - details: `Failed to test JSON-RPC endpoint: ${errorMessage}` - }); - - warnings.push({ - code: 'JSONRPC_ENDPOINT_ERROR', - message: `JSON-RPC endpoint test failed: ${errorMessage}`, - field: 'preferredTransport', - severity: 'warning', - fixable: true - }); - } - } - - /** - * Test gRPC endpoint (basic connectivity and port check) - */ - private async testGrpcEndpoint( - url: string, - testName: string, - validations: any[], - errors: any[], - warnings: any[], - options: ValidationOptions - ): Promise { - const timer = this.logger.timer(); - - try { - // gRPC testing is more complex - for now just test if the endpoint is reachable - // and check if it looks like a gRPC endpoint - const urlObj = new URL(url); - const isHttps = urlObj.protocol === 'https:'; - const portMatch = url.match(/:(\d+)/); - const port = portMatch ? parseInt(portMatch[1] || '80') : (isHttps ? 443 : 80); - - // Try a basic HTTP/2 connection test (gRPC uses HTTP/2) - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/grpc', - 'TE': 'trailers' - }, - signal: AbortSignal.timeout(options.timeout || 10000) - }); - - const duration = timer(); - - // gRPC endpoints typically return specific status codes for invalid requests - if (response.status === 415 || response.status === 400 || response.headers.get('content-type')?.includes('application/grpc')) { - validations.push({ - id: `grpc_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} gRPC`, - status: 'passed', - message: 'gRPC endpoint detected', - duration, - details: `Endpoint responds like a gRPC server (${response.status})` - }); - } else if (response.status === 404 || response.status === 405) { - validations.push({ - id: `grpc_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} gRPC`, - status: 'failed', - message: `gRPC endpoint returned ${response.status}`, - duration, - details: 'Endpoint may not support gRPC protocol' - }); - warnings.push({ - code: 'GRPC_ENDPOINT_NOT_FOUND', - message: `gRPC endpoint returned ${response.status} - may not support gRPC protocol`, - field: 'additionalInterfaces', - severity: 'warning', - fixable: true - }); - } else { - validations.push({ - id: `grpc_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} gRPC`, - status: 'passed', - message: 'gRPC endpoint accessible', - duration, - details: `Endpoint is reachable on port ${port}` - }); - } - } catch (error) { - const duration = timer(); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - - validations.push({ - id: `grpc_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} gRPC`, - status: 'failed', - message: 'gRPC endpoint test failed', - duration, - details: `Failed to test gRPC endpoint: ${errorMessage}` - }); - - warnings.push({ - code: 'GRPC_ENDPOINT_ERROR', - message: `gRPC endpoint test failed: ${errorMessage}`, - field: 'additionalInterfaces', - severity: 'warning', - fixable: true - }); - } - } - - /** - * Test HTTP+JSON endpoint (REST-like) - */ - private async testHttpJsonEndpoint( - url: string, - testName: string, - validations: any[], - errors: any[], - warnings: any[], - options: ValidationOptions - ): Promise { - const timer = this.logger.timer(); - - try { - // Test common REST patterns - const response = await fetch(url, { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - signal: AbortSignal.timeout(options.timeout || 10000) - }); - - const duration = timer(); - const contentType = response.headers.get('content-type') || ''; - - if (response.status === 200 && contentType.includes('application/json')) { - validations.push({ - id: `http_json_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} HTTP+JSON`, - status: 'passed', - message: 'HTTP+JSON endpoint responding correctly', - duration, - details: 'Endpoint returns JSON responses to HTTP requests' - }); - } else if (response.status === 405) { - // Method not allowed - try POST - const postResponse = await fetch(url, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({}), - signal: AbortSignal.timeout(options.timeout || 10000) - }); - - const postDuration = timer(); - - if (postResponse.status < 500) { - validations.push({ - id: `http_json_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} HTTP+JSON`, - status: 'passed', - message: 'HTTP+JSON endpoint accessible via POST', - duration: postDuration, - details: `Endpoint accepts POST requests (${postResponse.status})` - }); - } else { - validations.push({ - id: `http_json_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} HTTP+JSON`, - status: 'failed', - message: `HTTP+JSON endpoint error (${postResponse.status})`, - duration: postDuration, - details: 'Endpoint returned server error' - }); - } - } else { - validations.push({ - id: `http_json_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} HTTP+JSON`, - status: 'passed', - message: 'HTTP+JSON endpoint accessible', - duration, - details: `Endpoint responds to HTTP requests (${response.status})` - }); - } - } catch (error) { - const duration = timer(); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - - validations.push({ - id: `http_json_${testName.toLowerCase().replace(/\s+/g, '_')}`, - name: `${testName} HTTP+JSON`, - status: 'failed', - message: 'HTTP+JSON endpoint test failed', - duration, - details: `Failed to test HTTP+JSON endpoint: ${errorMessage}` - }); - - warnings.push({ - code: 'HTTP_JSON_ENDPOINT_ERROR', - message: `HTTP+JSON endpoint test failed: ${errorMessage}`, - field: 'preferredTransport', - severity: 'warning', - fixable: true - }); - } - } - - // Convenience methods - async validateProgressive(input: AgentCard | string, options: ValidationOptions = {}): Promise { - return this.validate(input, { ...options, strictness: 'progressive' }); - } - - async validateStrict(input: AgentCard | string, options: ValidationOptions = {}): Promise { - return this.validate(input, { ...options, strictness: 'strict' }); - } - - async validateConservative(input: AgentCard | string, options: ValidationOptions = {}): Promise { - return this.validate(input, { ...options, strictness: 'conservative' }); - } - - async validateSchemaOnly(card: AgentCard, options: ValidationOptions = {}): Promise { - return this.validate(card, { ...options, skipDynamic: true }); - } -} \ No newline at end of file diff --git a/src/validator/http-client.ts b/src/validator/http-client.ts deleted file mode 100644 index 67bd384..0000000 --- a/src/validator/http-client.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { HttpClient, HttpResponse, HttpError, RequestOptions } from '../types'; -import { Logger } from '../utils/logger'; - -export class FetchHttpClient implements HttpClient { - private logger?: Logger; - - constructor(logger?: Logger) { - if (logger) { - this.logger = logger; - } - } - - async get(url: string, options: RequestOptions = {}): Promise { - const startTime = Date.now(); - this.logger?.debug(`Initiating HTTP GET request to ${url}`); - - const controller = new AbortController(); - const timeout = setTimeout( - () => controller.abort(), - options.timeout || 10000 - ); - - try { - const response = await fetch(url, { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'User-Agent': 'capiscio-cli/1.1.0', - ...options.headers - }, - signal: options.signal || controller.signal - }); - - clearTimeout(timeout); - const duration = Date.now() - startTime; - - this.logger?.network('GET', url, response.status, duration); - - if (!response.ok) { - this.logger?.error(`HTTP request failed: ${response.status} ${response.statusText}`); - throw new HttpError( - `HTTP ${response.status}: ${response.statusText}`, - response.status, - this.getErrorCode(response.status) - ); - } - - const data = await response.json(); - return { - status: response.status, - data, - headers: Object.fromEntries(response.headers.entries()) - }; - } catch (error) { - clearTimeout(timeout); - throw this.normalizeError(error); - } - } - - private normalizeError(error: unknown): HttpError { - if (error instanceof HttpError) { - return error; - } - - if (error instanceof Error) { - if (error.name === 'AbortError') { - return new HttpError('Request timeout', 408, 'TIMEOUT'); - } - - if (error.message.includes('fetch')) { - return new HttpError('Network error', 0, 'NETWORK_ERROR'); - } - - if (error.message.includes('ENOTFOUND')) { - return new HttpError('Domain not found - check the URL', 0, 'ENOTFOUND'); - } - - if (error.message.includes('ECONNREFUSED')) { - return new HttpError('Connection refused - agent endpoint not accessible', 0, 'ECONNREFUSED'); - } - - return new HttpError(error.message, 0, 'UNKNOWN'); - } - - return new HttpError('Unknown error', 0, 'UNKNOWN'); - } - - private getErrorCode(status: number): string { - switch (status) { - case 400: return 'BAD_REQUEST'; - case 401: return 'UNAUTHORIZED'; - case 403: return 'FORBIDDEN'; - case 404: return 'NOT_FOUND'; - case 408: return 'TIMEOUT'; - case 429: return 'RATE_LIMITED'; - case 500: return 'INTERNAL_SERVER_ERROR'; - case 502: return 'BAD_GATEWAY'; - case 503: return 'SERVICE_UNAVAILABLE'; - case 504: return 'GATEWAY_TIMEOUT'; - default: return 'HTTP_ERROR'; - } - } -} \ No newline at end of file diff --git a/src/validator/live-tester.ts b/src/validator/live-tester.ts deleted file mode 100644 index 65ade6c..0000000 --- a/src/validator/live-tester.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { validateRuntimeMessage } from './runtime-validators'; -import { AgentCard } from '../types'; - -export interface LiveTestResult { - success: boolean; - endpoint: string; - responseTime: number; - errors: string[]; - request?: any; - response?: any; - timestamp: string; -} - -export interface LiveTestOptions { - timeout?: number; - verbose?: boolean; - testMessage?: string; -} - -export class LiveTester { - private timeout: number; - private verbose: boolean; - - constructor(options: LiveTestOptions = {}) { - this.timeout = options.timeout || 10000; - this.verbose = options.verbose || false; - } - - /** - * Test a live agent by sending a test message and validating the response - */ - async testAgent(agentCard: AgentCard, options: LiveTestOptions = {}): Promise { - const startTime = Date.now(); - const endpoint = agentCard.url; - const testMessage = options.testMessage || 'Hello, are you available?'; - const transport = agentCard.preferredTransport || 'JSONRPC'; - - // Validate endpoint exists - if (!endpoint) { - return { - success: false, - endpoint: 'undefined', - responseTime: 0, - errors: ['Agent card does not specify a valid endpoint URL'], - timestamp: new Date().toISOString() - }; - } - - // Check if transport is supported - if (transport === 'GRPC') { - return { - success: false, - endpoint, - responseTime: 0, - errors: ['GRPC transport is not yet supported for live testing'], - timestamp: new Date().toISOString() - }; - } - - try { - // Build A2A message - const a2aMessage = { - role: 'user', - parts: [ - { - type: 'text', - text: testMessage - } - ] - }; - - // Format request based on transport protocol - let request: any; - let requestBody: any; - - if (transport === 'JSONRPC') { - // JSON-RPC wraps the message in an RPC envelope - request = { - jsonrpc: '2.0', - id: this.generateRequestId(), - method: 'message/send', - params: { - message: a2aMessage, - configuration: { - accepted_output_modes: ['text/plain'] - } - } - }; - requestBody = request; - } else { - // HTTP+JSON and REST send the message directly - request = { - message: a2aMessage, - configuration: { - accepted_output_modes: ['text/plain'] - } - }; - requestBody = request; - } - - if (this.verbose) { - console.log(`[Live Test] Transport: ${transport}`); - console.log('[Live Test] Request:', JSON.stringify(requestBody, null, 2)); - } - - // Call the agent endpoint - const response = await this.callEndpoint(endpoint, requestBody); - const responseTime = Date.now() - startTime; - - if (this.verbose) { - console.log('[Live Test] Response:', JSON.stringify(response, null, 2)); - } - - // Extract the result based on transport protocol - let resultData: any; - - if (transport === 'JSONRPC') { - // JSON-RPC response has result field - resultData = response.result; - - if (!resultData) { - return { - success: false, - endpoint, - responseTime, - errors: ['JSON-RPC response missing result field'], - request: requestBody, - response, - timestamp: new Date().toISOString() - }; - } - } else { - // HTTP+JSON response is the result directly - resultData = response; - } - - // Validate the response using runtime validators - const validation = validateRuntimeMessage(resultData); - - if (!validation.valid) { - return { - success: false, - endpoint, - responseTime, - errors: validation.errors.map(err => err.message), - request: requestBody, - response: resultData, - timestamp: new Date().toISOString() - }; - } - - // Success! - return { - success: true, - endpoint, - responseTime, - errors: [], - request: requestBody, - response: resultData, - timestamp: new Date().toISOString() - }; - - } catch (error) { - const responseTime = Date.now() - startTime; - - return { - success: false, - endpoint, - responseTime, - errors: [this.formatError(error)], - timestamp: new Date().toISOString() - }; - } - } - - /** - * Call the agent endpoint with timeout handling - */ - private async callEndpoint(url: string, requestBody: any): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), this.timeout); - - try { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - signal: controller.signal - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - const contentType = response.headers.get('content-type'); - if (!contentType?.includes('application/json')) { - throw new Error(`Expected JSON response, got ${contentType || 'unknown'}`); - } - - return await response.json(); - - } catch (error) { - clearTimeout(timeoutId); - throw error; - } - } - - /** - * Format error messages for better readability - */ - private formatError(error: unknown): string { - if (error instanceof Error) { - if (error.name === 'AbortError') { - return `Request timed out after ${this.timeout}ms`; - } - - // Network errors - if (error.message.includes('ECONNREFUSED')) { - return 'Connection refused - endpoint unreachable'; - } - if (error.message.includes('ENOTFOUND')) { - return 'DNS resolution failed - host not found'; - } - if (error.message.includes('ETIMEDOUT')) { - return 'Connection timed out'; - } - if (error.message.includes('certificate')) { - return `TLS certificate error: ${error.message}`; - } - - return error.message; - } - - return 'Unknown error occurred'; - } - - /** - * Generate a unique request ID - */ - private generateRequestId(): string { - return `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - } -} diff --git a/src/validator/runtime-validators.ts b/src/validator/runtime-validators.ts deleted file mode 100644 index bde77cb..0000000 --- a/src/validator/runtime-validators.ts +++ /dev/null @@ -1,247 +0,0 @@ -/** - * Runtime Message Validators for A2A Protocol - * - * These validators check runtime messages exchanged between agents and clients - * during protocol execution. Based on a2a-inspector's validation logic. - * - * Validates message types: - * - Task: Initial task assignment - * - StatusUpdate: Task status changes - * - ArtifactUpdate: Artifact/output updates - * - Message: Agent messages/responses - */ - -export interface RuntimeValidationError { - code: string; - message: string; - field?: string; - severity: 'error'; -} - -export interface RuntimeValidationResult { - valid: boolean; - errors: RuntimeValidationError[]; -} - -/** - * Validate a Task message - * Required fields: - * - id: Task identifier - * - status.state: Current task state - */ -export function validateTask(data: any): RuntimeValidationResult { - const errors: RuntimeValidationError[] = []; - - // Validate id field - if (!data.id || typeof data.id !== 'string') { - errors.push({ - code: 'TASK_MISSING_ID', - message: "Task object missing required field: 'id'", - field: 'id', - severity: 'error' - }); - } - - // Validate status.state field - if (!data.status || typeof data.status !== 'object') { - errors.push({ - code: 'TASK_MISSING_STATUS', - message: "Task object missing required field: 'status'", - field: 'status', - severity: 'error' - }); - } else if (!data.status.state || typeof data.status.state !== 'string') { - errors.push({ - code: 'TASK_MISSING_STATUS_STATE', - message: "Task object missing required field: 'status.state'", - field: 'status.state', - severity: 'error' - }); - } - - return { - valid: errors.length === 0, - errors - }; -} - -/** - * Validate a StatusUpdate message - * Required fields: - * - status.state: Current status state - */ -export function validateStatusUpdate(data: any): RuntimeValidationResult { - const errors: RuntimeValidationError[] = []; - - // Validate status.state field - if (!data.status || typeof data.status !== 'object') { - errors.push({ - code: 'STATUS_UPDATE_MISSING_STATUS', - message: "StatusUpdate object missing required field: 'status'", - field: 'status', - severity: 'error' - }); - } else if (!data.status.state || typeof data.status.state !== 'string') { - errors.push({ - code: 'STATUS_UPDATE_MISSING_STATE', - message: "StatusUpdate object missing required field: 'status.state'", - field: 'status.state', - severity: 'error' - }); - } - - return { - valid: errors.length === 0, - errors - }; -} - -/** - * Validate an ArtifactUpdate message - * Required fields: - * - artifact: Artifact object - * - artifact.parts: Non-empty array of artifact parts - */ -export function validateArtifactUpdate(data: any): RuntimeValidationResult { - const errors: RuntimeValidationError[] = []; - - // Validate artifact field - if (!data.artifact || typeof data.artifact !== 'object') { - errors.push({ - code: 'ARTIFACT_UPDATE_MISSING_ARTIFACT', - message: "ArtifactUpdate object missing required field: 'artifact'", - field: 'artifact', - severity: 'error' - }); - } else { - // Validate artifact.parts is a non-empty array - if (!Array.isArray(data.artifact.parts)) { - errors.push({ - code: 'ARTIFACT_MISSING_PARTS_ARRAY', - message: "Artifact object must have a 'parts' array", - field: 'artifact.parts', - severity: 'error' - }); - } else if (data.artifact.parts.length === 0) { - errors.push({ - code: 'ARTIFACT_EMPTY_PARTS', - message: "Artifact object must have a non-empty 'parts' array", - field: 'artifact.parts', - severity: 'error' - }); - } - } - - return { - valid: errors.length === 0, - errors - }; -} - -/** - * Validate a Message - * Required fields: - * - parts: Non-empty array of message parts - * - role: Must be 'agent' for agent messages - */ -export function validateMessage(data: any): RuntimeValidationResult { - const errors: RuntimeValidationError[] = []; - - // Validate parts is a non-empty array - if (!Array.isArray(data.parts)) { - errors.push({ - code: 'MESSAGE_MISSING_PARTS_ARRAY', - message: "Message object must have a 'parts' array", - field: 'parts', - severity: 'error' - }); - } else if (data.parts.length === 0) { - errors.push({ - code: 'MESSAGE_EMPTY_PARTS', - message: "Message object must have a non-empty 'parts' array", - field: 'parts', - severity: 'error' - }); - } - - // Validate role is 'agent' - if (!data.role || typeof data.role !== 'string') { - errors.push({ - code: 'MESSAGE_MISSING_ROLE', - message: "Message object missing required field: 'role'", - field: 'role', - severity: 'error' - }); - } else if (data.role !== 'agent') { - errors.push({ - code: 'MESSAGE_INVALID_ROLE', - message: "Message from agent must have 'role' set to 'agent'", - field: 'role', - severity: 'error' - }); - } - - return { - valid: errors.length === 0, - errors - }; -} - -/** - * Validate a runtime message based on its kind - * Dispatches to the appropriate validator based on message type - */ -export function validateRuntimeMessage(data: any): RuntimeValidationResult { - // Handle null/undefined input - if (!data || typeof data !== 'object') { - return { - valid: false, - errors: [{ - code: 'MESSAGE_INVALID_INPUT', - message: 'Invalid message: expected an object', - severity: 'error' - }] - }; - } - - // Check for required 'kind' field - if (!data.kind || typeof data.kind !== 'string') { - return { - valid: false, - errors: [{ - code: 'MESSAGE_MISSING_KIND', - message: "Response from agent is missing required 'kind' field", - field: 'kind', - severity: 'error' - }] - }; - } - - // Dispatch to appropriate validator based on kind - const kind = data.kind.toLowerCase(); - - switch (kind) { - case 'task': - return validateTask(data); - - case 'status-update': - return validateStatusUpdate(data); - - case 'artifact-update': - return validateArtifactUpdate(data); - - case 'message': - return validateMessage(data); - - default: - return { - valid: false, - errors: [{ - code: 'MESSAGE_UNKNOWN_KIND', - message: `Unknown message kind received: '${data.kind}'`, - field: 'kind', - severity: 'error' - }] - }; - } -} diff --git a/tests/e2e/validation.test.ts b/tests/e2e/validation.test.ts deleted file mode 100644 index 7c51d94..0000000 --- a/tests/e2e/validation.test.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { execSync } from 'child_process'; -import { join } from 'path'; -import { readFileSync } from 'fs'; - -const CLI_PATH = join(process.cwd(), 'dist', 'cli.js'); -const FIXTURES_PATH = join(process.cwd(), 'tests', 'fixtures'); - -describe('End-to-End Validation Tests', () => { - beforeAll(() => { - // Ensure CLI is built - try { - execSync('npm run build', { stdio: 'inherit' }); - } catch (error) { - console.warn('Build failed, assuming CLI is already built'); - } - }); - - describe('Valid Agent Cards', () => { - it('should validate basic valid agent', () => { - const agentPath = join(FIXTURES_PATH, 'valid-agents', 'basic-agent.json'); - - const result = execSync(`node "${CLI_PATH}" validate "${agentPath}"`, { - encoding: 'utf8' - }); - - expect(result).toContain('✅ A2A AGENT VALIDATION PASSED'); - // Score is 85 because of missing skills (warning) - expect(result).toContain('Score: 85/100'); - // Version might not be in the output if not explicitly requested or if format changed - // expect(result).toContain('Version: 0.3.0'); - expect(result).toContain('Agent passed with warnings'); - }); - - it('should validate complex agent with all features', () => { - const agentPath = join(FIXTURES_PATH, 'valid-agents', 'complex-agent.json'); - - const result = execSync(`node "${CLI_PATH}" validate "${agentPath}"`, { - encoding: 'utf8' - }); - - expect(result).toContain('✅ A2A AGENT VALIDATION PASSED'); - // Score is 96 because of missing skill tags - expect(result).toContain('Score: 96/100'); - // expect(result).toContain('Complex Test Agent'); - }); - - it('should fail validation for legacy agent (schema mismatch)', () => { - const agentPath = join(FIXTURES_PATH, 'valid-agents', 'legacy-agent.json'); - - try { - execSync(`node "${CLI_PATH}" validate "${agentPath}"`, { - encoding: 'utf8' - }); - expect.fail('Should have failed validation'); - } catch (error: any) { - expect(error.status).toBe(1); - // Expect JSON unmarshal error or similar - expect(error.stderr).toContain('failed to parse Agent Card JSON'); - } - }); - }); - - describe('Invalid Agent Cards', () => { - it('should fail validation for missing required fields', () => { - const agentPath = join(FIXTURES_PATH, 'invalid-agents', 'missing-required.json'); - - try { - execSync(`node "${CLI_PATH}" validate "${agentPath}"`, { - encoding: 'utf8' - }); - expect.fail('Should have failed validation'); - } catch (error: any) { - expect(error.status).toBe(1); - expect(error.stdout).toContain('❌ A2A AGENT VALIDATION FAILED'); - expect(error.stdout).toContain('ERRORS FOUND'); - expect(error.stdout).toContain('protocolVersion is required'); - expect(error.stdout).toContain('Agent URL is required'); - } - }); - - it('should fail validation for invalid URL format', () => { - const agentPath = join(FIXTURES_PATH, 'invalid-agents', 'invalid-url.json'); - - try { - execSync(`node "${CLI_PATH}" validate "${agentPath}"`, { - encoding: 'utf8' - }); - expect.fail('Should have failed validation'); - } catch (error: any) { - expect(error.status).toBe(1); - expect(error.stdout).toContain('❌ A2A AGENT VALIDATION FAILED'); - expect(error.stdout).toContain('URL must use http, https, or grpc scheme'); - } - }); - - it('should fail validation for invalid version format', () => { - const agentPath = join(FIXTURES_PATH, 'invalid-agents', 'invalid-version.json'); - - try { - execSync(`node "${CLI_PATH}" validate "${agentPath}"`, { - encoding: 'utf8' - }); - expect.fail('Should have failed validation'); - } catch (error: any) { - expect(error.status).toBe(1); - expect(error.stdout).toContain('❌ A2A AGENT VALIDATION FAILED'); - expect(error.stdout).toContain('protocolVersion must be a valid SemVer string'); - } - }); - - it('should handle multiple validation issues', () => { - const agentPath = join(FIXTURES_PATH, 'invalid-agents', 'mixed-issues.json'); - - try { - execSync(`node "${CLI_PATH}" validate "${agentPath}"`, { - encoding: 'utf8' - }); - expect.fail('Should have failed validation'); - } catch (error: any) { - expect(error.status).toBe(1); - expect(error.stdout).toContain('❌ A2A AGENT VALIDATION FAILED'); - expect(error.stdout).toContain('ERRORS FOUND'); - - // Should contain multiple error types - const errorCount = (error.stdout.match(/❌/g) || []).length; - expect(errorCount).toBeGreaterThan(1); // Multiple errors expected - } - }); - }); - - describe('JSON Output Mode', () => { - it('should output valid JSON for successful validation', () => { - const agentPath = join(FIXTURES_PATH, 'valid-agents', 'basic-agent.json'); - - const result = execSync(`node "${CLI_PATH}" validate "${agentPath}" --json`, { - encoding: 'utf8' - }); - - const jsonResult = JSON.parse(result); - expect(jsonResult.success).toBe(true); - expect(jsonResult.score).toBe(85); - expect(jsonResult.errors || []).toHaveLength(0); - // expect(jsonResult.validations).toBeDefined(); // Not in Go output - // expect(jsonResult.versionInfo).toBeDefined(); // Not in Go output - }); - - it('should output valid JSON for failed validation', () => { - const agentPath = join(FIXTURES_PATH, 'invalid-agents', 'missing-required.json'); - - try { - execSync(`node "${CLI_PATH}" validate "${agentPath}" --json`, { - encoding: 'utf8' - }); - expect.fail('Should have failed validation'); - } catch (error: any) { - expect(error.status).toBe(1); - - const jsonResult = JSON.parse(error.stdout); - expect(jsonResult.success).toBe(false); - expect(jsonResult.score).toBeLessThan(100); - expect(jsonResult.errors.length).toBeGreaterThan(0); - - // Verify error structure - expect(jsonResult.errors[0]).toHaveProperty('code'); - expect(jsonResult.errors[0]).toHaveProperty('message'); - expect(jsonResult.errors[0]).toHaveProperty('severity'); - } - }); - - // Removed timing test as it relied on 'validations' array - }); - - describe('Validation Modes', () => { - const testAgent = join(FIXTURES_PATH, 'valid-agents', 'complex-agent.json'); - - // Removed strictness check in JSON output as it's not currently exposed in CLIOutput - // We can only verify that flags don't crash - - it('should run in progressive mode by default', () => { - execSync(`node "${CLI_PATH}" validate "${testAgent}" --json`, { - encoding: 'utf8' - }); - }); - - it('should run in strict mode when specified', () => { - try { - execSync(`node "${CLI_PATH}" validate "${testAgent}" --strict --json`, { - encoding: 'utf8' - }); - expect.fail('Should have failed validation in strict mode due to warnings'); - } catch (error: any) { - expect(error.status).toBe(1); - const jsonResult = JSON.parse(error.stdout); - expect(jsonResult.success).toBe(false); - } - }); - - // Conservative mode is temporarily disabled in CLI wrapper - // it('should run in conservative mode when specified', () => { - // execSync(`node "${CLI_PATH}" validate "${testAgent}" --conservative --json`, { - // encoding: 'utf8' - // }); - // }); - }); - - describe('Schema-only Mode', () => { - it('should validate schema only without network calls', () => { - const agentPath = join(FIXTURES_PATH, 'valid-agents', 'basic-agent.json'); - - const start = Date.now(); - const result = execSync(`node "${CLI_PATH}" validate "${agentPath}" --schema-only`, { - encoding: 'utf8' - }); - const duration = Date.now() - start; - - expect(result).toContain('✅ A2A AGENT VALIDATION PASSED'); - expect(duration).toBeLessThan(1000); // Should be very fast - }); - }); - - describe('Error-only Mode', () => { - it('should show only errors and warnings', () => { - const agentPath = join(FIXTURES_PATH, 'invalid-agents', 'mixed-issues.json'); - - try { - execSync(`node "${CLI_PATH}" validate "${agentPath}" --errors-only`, { - encoding: 'utf8' - }); - expect.fail('Should have failed validation'); - } catch (error: any) { - expect(error.status).toBe(1); - // Expect errors but NOT the header "ERRORS FOUND" if suppressed? - // Actually, my code suppresses "ERRORS FOUND:" header if flagErrorsOnly is true. - // But it prints the issues. - expect(error.stdout).toContain('error:'); - expect(error.stdout).not.toContain('Validation Results for:'); - } - }); - }); - - - describe('Fixture Validation', () => { - it('should have valid fixture files', () => { - // Verify that all fixture files are valid JSON - const validAgents = [ - 'basic-agent.json', - 'complex-agent.json', - 'legacy-agent.json' - ]; - - const invalidAgents = [ - 'missing-required.json', - 'invalid-url.json', - 'invalid-version.json', - 'mixed-issues.json' - ]; - - [...validAgents, ...invalidAgents].forEach(filename => { - const filePath = join(FIXTURES_PATH, - validAgents.includes(filename) ? 'valid-agents' : 'invalid-agents', - filename - ); - - expect(() => { - const content = readFileSync(filePath, 'utf8'); - JSON.parse(content); - }).not.toThrow(`${filename} should be valid JSON`); - }); - }); - }); - - describe('Performance Tests', () => { - it('should complete validation within reasonable time', () => { - const agentPath = join(FIXTURES_PATH, 'valid-agents', 'complex-agent.json'); - - const start = Date.now(); - execSync(`node "${CLI_PATH}" validate "${agentPath}" --schema-only`, { - encoding: 'utf8' - }); - const duration = Date.now() - start; - - expect(duration).toBeLessThan(5000); // Should complete within 5 seconds - }); - - it('should handle large agent cards efficiently', () => { - // Test with complex agent that has many fields - const agentPath = join(FIXTURES_PATH, 'valid-agents', 'complex-agent.json'); - - const start = Date.now(); - const result = execSync(`node "${CLI_PATH}" validate "${agentPath}" --schema-only --json`, { - encoding: 'utf8' - }); - const duration = Date.now() - start; - - const jsonResult = JSON.parse(result); - expect(jsonResult.success).toBe(true); - expect(duration).toBeLessThan(2000); // Should be fast even for complex agents - }); - }); -}); \ No newline at end of file diff --git a/tests/fixtures/invalid-agents/invalid-url.json b/tests/fixtures/invalid-agents/invalid-url.json deleted file mode 100644 index 4ab22bc..0000000 --- a/tests/fixtures/invalid-agents/invalid-url.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "protocolVersion": "0.3.0", - "name": "Invalid URL Agent", - "description": "Agent with invalid URL format", - "url": "not-a-valid-url", - "preferredTransport": "HTTP+JSON", - "provider": { - "organization": "Test Corp" - }, - "version": "1.0.0" -} \ No newline at end of file diff --git a/tests/fixtures/invalid-agents/invalid-version.json b/tests/fixtures/invalid-agents/invalid-version.json deleted file mode 100644 index f1c2d06..0000000 --- a/tests/fixtures/invalid-agents/invalid-version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "protocolVersion": "not-semver", - "name": "Invalid Version Agent", - "description": "Agent with non-semver version", - "url": "https://example.com/agent", - "preferredTransport": "HTTP+JSON", - "provider": { - "organization": "Test Corp" - }, - "version": "1.0.0" -} \ No newline at end of file diff --git a/tests/fixtures/invalid-agents/missing-required.json b/tests/fixtures/invalid-agents/missing-required.json deleted file mode 100644 index bf5d082..0000000 --- a/tests/fixtures/invalid-agents/missing-required.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "Incomplete Agent", - "description": "Missing required fields for testing validation errors" -} \ No newline at end of file diff --git a/tests/fixtures/invalid-agents/mixed-issues.json b/tests/fixtures/invalid-agents/mixed-issues.json deleted file mode 100644 index cea59f2..0000000 --- a/tests/fixtures/invalid-agents/mixed-issues.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "protocolVersion": "0.1.0", - "name": "Mixed Issues Agent", - "description": "Agent with multiple validation issues", - "url": "not-a-url", - "preferredTransport": "INVALID_TRANSPORT", - "provider": { - "organization": "Test Corp" - }, - "version": "not-semver", - "iconUrl": "also-not-a-url", - "capabilities": { - "streaming": true - }, - "additionalInterfaces": [ - { - "url": "invalid-grpc-url", - "transport": "GRPC" - } - ] -} \ No newline at end of file diff --git a/tests/fixtures/valid-agents/basic-agent.json b/tests/fixtures/valid-agents/basic-agent.json deleted file mode 100644 index 0132d94..0000000 --- a/tests/fixtures/valid-agents/basic-agent.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "protocolVersion": "0.3.0", - "name": "Basic Test Agent", - "description": "A minimal valid agent card for testing", - "url": "https://example.com/agent", - "preferredTransport": "JSONRPC", - "provider": { - "organization": "Test Corporation" - }, - "version": "1.0.0" -} \ No newline at end of file diff --git a/tests/fixtures/valid-agents/complex-agent.json b/tests/fixtures/valid-agents/complex-agent.json deleted file mode 100644 index 939c44a..0000000 --- a/tests/fixtures/valid-agents/complex-agent.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "protocolVersion": "0.3.0", - "name": "Complex Test Agent", - "description": "A comprehensive agent card with all optional fields for testing", - "url": "https://api.example.com/agent", - "preferredTransport": "JSONRPC", - "provider": { - "organization": "Advanced AI Corp", - "url": "https://advancedai.com" - }, - "version": "2.1.0", - "iconUrl": "https://api.example.com/icon.png", - "documentationUrl": "https://docs.example.com/agent", - "capabilities": { - "streaming": true, - "pushNotifications": true - }, - "additionalInterfaces": [ - { - "url": "https://grpc.example.com:443", - "transport": "GRPC" - }, - { - "url": "https://ws.example.com/socket", - "transport": "HTTP+JSON" - } - ], - "skills": [ - { - "id": "text-generation", - "name": "Text Generation", - "description": "Generate human-like text based on prompts", - "examples": [ - "Write a story about a robot", - "Explain quantum computing", - "Create a marketing email" - ] - }, - { - "id": "code-analysis", - "name": "Code Analysis", - "description": "Analyze and review code for bugs and improvements", - "examples": [ - "Review this Python function", - "Find security vulnerabilities", - "Suggest performance optimizations" - ] - } - ], - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] -} \ No newline at end of file diff --git a/tests/fixtures/valid-agents/legacy-agent.json b/tests/fixtures/valid-agents/legacy-agent.json deleted file mode 100644 index bb5a7b9..0000000 --- a/tests/fixtures/valid-agents/legacy-agent.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "protocolVersion": "0.2.0", - "name": "Legacy Test Agent", - "description": "An agent card using legacy protocol version", - "url": "https://legacy.example.com/agent", - "preferredTransport": "HTTP+JSON", - "provider": { - "organization": "Legacy Systems Inc" - }, - "version": "0.9.0", - "endpoints": [ - { - "name": "primary", - "url": "https://legacy.example.com/api/v1", - "protocol": "rest", - "version": "1.0" - } - ], - "capabilities": ["generate", "analyze"], - "authentication": { - "type": "bearer", - "required": true - } -} \ No newline at end of file diff --git a/tests/integration/cli.test.ts b/tests/integration/cli.test.ts deleted file mode 100644 index 82cb941..0000000 --- a/tests/integration/cli.test.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { execSync, spawn } from 'child_process'; -import { writeFileSync, mkdirSync, rmSync, existsSync } from 'fs'; -import { join } from 'path'; -import type { ChildProcess } from 'child_process'; - -const CLI_PATH = join(process.cwd(), 'dist', 'cli.js'); -const TEST_DIR = join(process.cwd(), 'test-temp'); - -describe('CLI Integration Tests', () => { - beforeEach(() => { - // Create test directory - if (!existsSync(TEST_DIR)) { - mkdirSync(TEST_DIR, { recursive: true }); - } - }); - - afterEach(() => { - // Clean up test directory - if (existsSync(TEST_DIR)) { - rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - describe('CLI Basic Functionality', () => { - it('should show help when no arguments provided', () => { - const result = execSync(`node "${CLI_PATH}" --help`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - - expect(result).toContain('Usage: capiscio'); - expect(result).toContain('The definitive CLI tool for validating A2A'); - expect(result).toContain('Commands:'); - expect(result).toContain('validate'); - }); - - it('should show version', () => { - const result = execSync(`node "${CLI_PATH}" --version`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - - expect(result.trim()).toMatch(/\d+\.\d+\.\d+/); - }); - - it('should show validate command help', () => { - const result = execSync(`node "${CLI_PATH}" validate --help`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - - expect(result).toContain('Usage: capiscio validate'); - expect(result).toContain('--strict'); - expect(result).toContain('--progressive'); - // expect(result).toContain('--conservative'); - expect(result).toContain('--json'); - expect(result).toContain('--schema-only'); - }); - }); - - describe('File Validation', () => { - it('should validate a valid agent card file', () => { - const validAgent = { - protocolVersion: '0.3.0', - name: 'Test Agent', - description: 'A test agent for CLI testing', - url: 'https://example.com/agent', - preferredTransport: 'HTTP+JSON', - provider: { - organization: 'Test Corp' - }, - version: '1.0.0' - }; - - const agentFile = join(TEST_DIR, 'valid-agent.json'); - writeFileSync(agentFile, JSON.stringify(validAgent, null, 2)); - - const result = execSync(`node "${CLI_PATH}" validate "${agentFile}"`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - - expect(result).toContain('✅ A2A AGENT VALIDATION PASSED'); - // Score is 85 because of missing skills/signatures warnings - expect(result).toContain('Score: 85/100'); - }); - - it('should fail validation for invalid agent card', () => { - const invalidAgent = { - name: 'Invalid Agent', - description: 'Missing required fields' - }; - - const agentFile = join(TEST_DIR, 'invalid-agent.json'); - writeFileSync(agentFile, JSON.stringify(invalidAgent, null, 2)); - - try { - execSync(`node "${CLI_PATH}" validate "${agentFile}"`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - expect.fail('Should have failed validation'); - } catch (error: any) { - expect(error.status).toBe(1); - expect(error.stdout).toContain('❌ A2A AGENT VALIDATION FAILED'); - // Go output format - expect(error.stdout).toContain('protocolVersion is required'); - expect(error.stdout).toContain('Agent URL is required'); - } - }); - - it('should output JSON format when requested', () => { - const validAgent = { - protocolVersion: '0.3.0', - name: 'JSON Test Agent', - description: 'Testing JSON output', - url: 'https://example.com/agent', - preferredTransport: 'HTTP+JSON', - provider: { - organization: 'Test Corp' - }, - version: '1.0.0' - }; - - const agentFile = join(TEST_DIR, 'json-agent.json'); - writeFileSync(agentFile, JSON.stringify(validAgent, null, 2)); - - const result = execSync(`node "${CLI_PATH}" validate "${agentFile}" --json`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - - const jsonResult = JSON.parse(result); - expect(jsonResult.success).toBe(true); - expect(jsonResult.score).toBe(85); - expect(jsonResult.errors || []).toHaveLength(0); - // We added Version to Go output, so we can check for it - expect(jsonResult.version).toBeDefined(); - }); - - it('should handle malformed JSON files', () => { - const agentFile = join(TEST_DIR, 'malformed.json'); - writeFileSync(agentFile, '{ invalid json }'); - - try { - execSync(`node "${CLI_PATH}" validate "${agentFile}"`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - expect.fail('Should have failed for malformed JSON'); - } catch (error: any) { - expect(error.status).toBe(1); - // Go JSON parser error - expect(error.stderr).toMatch(/invalid character|failed to parse/); - } - }); - - it('should handle non-existent files', () => { - try { - execSync(`node "${CLI_PATH}" validate "${join(TEST_DIR, 'nonexistent.json')}"`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - expect.fail('Should have failed for non-existent file'); - } catch (error: any) { - expect(error.status).toBe(1); - expect(error.stderr).toMatch(/failed to read file|no such file/); - } - }); - }); - - describe('Validation Modes', () => { - const testAgent = { - protocolVersion: '0.3.0', - name: 'Mode Test Agent', - description: 'Testing validation modes', - url: 'https://example.com/agent', - preferredTransport: 'GRPC', - provider: { - organization: 'Test Corp' - }, - version: '1.0.0', - additionalInterfaces: [{ - url: 'https://grpc.example.com', - transport: 'GRPC' - }], - capabilities: { - streaming: false // This should trigger warnings - } - }; - - beforeEach(() => { - const agentFile = join(TEST_DIR, 'mode-test-agent.json'); - writeFileSync(agentFile, JSON.stringify(testAgent, null, 2)); - }); - - it('should use progressive mode by default', () => { - const result = execSync(`node "${CLI_PATH}" validate "${join(TEST_DIR, 'mode-test-agent.json')}"`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - - // Progressive mode allows warnings, so it should pass - expect(result).toContain('✅ A2A AGENT VALIDATION PASSED'); - }); - - it('should use strict mode when specified', () => { - try { - execSync(`node "${CLI_PATH}" validate "${join(TEST_DIR, 'mode-test-agent.json')}" --strict`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - expect.fail('Should have failed in strict mode due to warnings'); - } catch (error: any) { - expect(error.status).toBe(1); - expect(error.stdout).toContain('❌ A2A AGENT VALIDATION FAILED'); - } - }); - - // Conservative mode is temporarily disabled - it.skip('should use conservative mode when specified', () => { - // Conservative mode is not currently supported. - }); - }); - - describe('Auto-detection', () => { - it('should auto-detect agent-card.json in current directory', () => { - const validAgent = { - protocolVersion: '0.3.0', - name: 'Auto-detect Test', - description: 'Testing auto-detection', - url: 'https://example.com/agent', - preferredTransport: 'HTTP+JSON', - provider: { - organization: 'Test Corp' - }, - version: '1.0.0' - }; - - writeFileSync(join(TEST_DIR, 'agent-card.json'), JSON.stringify(validAgent, null, 2)); - - const result = execSync(`node "${CLI_PATH}" validate`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - - expect(result).toContain('✅ A2A AGENT VALIDATION PASSED'); - }); - - it('should fail gracefully when no agent card found', () => { - try { - execSync(`node "${CLI_PATH}" validate`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - expect.fail('Should have failed when no agent card found'); - } catch (error: any) { - expect(error.status).toBe(1); - // Go binary error message when file is missing - expect(error.stderr).toMatch(/failed to read file|no such file/); - } - }); - }); - - describe('Schema-only Mode', () => { - it('should skip network calls in schema-only mode', () => { - const validAgent = { - protocolVersion: '0.3.0', - name: 'Schema Only Test', - description: 'Testing schema-only mode', - url: 'https://example.com/agent', - preferredTransport: 'HTTP+JSON', - provider: { - organization: 'Test Corp' - }, - version: '1.0.0' - }; - - const agentFile = join(TEST_DIR, 'schema-only.json'); - writeFileSync(agentFile, JSON.stringify(validAgent, null, 2)); - - const result = execSync(`node "${CLI_PATH}" validate "${agentFile}" --schema-only`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - - expect(result).toContain('✅ A2A AGENT VALIDATION PASSED'); - // Should complete quickly since no network calls - }); - }); - - describe('Error Handling', () => { - it('should handle unknown options gracefully', () => { - try { - execSync(`node "${CLI_PATH}" validate --unknown-option`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - expect.fail('Should have failed for unknown option'); - } catch (error: any) { - expect(error.status).not.toBe(0); - expect(error.stderr).toContain('unknown option'); - } - }); - - it('should handle invalid timeout values', () => { - const validAgent = { - protocolVersion: '0.3.0', - name: 'Timeout Test', - description: 'Testing timeout handling', - url: 'https://example.com/agent', - preferredTransport: 'HTTP+JSON', - provider: { - organization: 'Test Corp' - }, - version: '1.0.0' - }; - - const agentFile = join(TEST_DIR, 'timeout-test.json'); - writeFileSync(agentFile, JSON.stringify(validAgent, null, 2)); - - // Should handle non-numeric timeout gracefully - const result = execSync(`node "${CLI_PATH}" validate "${agentFile}" --timeout abc --schema-only`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - - // Should still validate successfully with default timeout - expect(result).toContain('✅ A2A AGENT VALIDATION PASSED'); - }); - }); - - describe('Output Filtering', () => { - it('should show only errors when errors-only flag is used', () => { - const invalidAgent = { - name: 'Errors Only Test', - description: 'Testing errors-only output' - // Missing required fields - }; - - const agentFile = join(TEST_DIR, 'errors-only.json'); - writeFileSync(agentFile, JSON.stringify(invalidAgent, null, 2)); - - try { - execSync(`node "${CLI_PATH}" validate "${agentFile}" --errors-only`, { - encoding: 'utf8', - cwd: TEST_DIR - }); - expect.fail('Should have failed validation'); - } catch (error: any) { - expect(error.status).toBe(1); - // In errors-only mode, the header might be suppressed, but errors should be present - expect(error.stdout).toMatch(/error:|warning:/); - expect(error.stdout).not.toContain('VALIDATIONS PERFORMED'); - } - }); - }); -}); \ No newline at end of file diff --git a/tsup.config.ts b/tsup.config.ts index bcbecdd..f5c05eb 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -12,14 +12,9 @@ export default defineConfig([ minify: false, splitting: false, treeshake: true, - external: ['commander', 'chalk', 'ora', 'inquirer', 'glob', 'jose'], - esbuildOptions(options) { - options.banner = { - js: '#!/usr/bin/env node' - }; - } + external: ['chalk', 'ora', 'execa', 'axios'], }, - // Library exports + // Library exports (minimal - just BinaryManager for advanced users) { entry: { index: 'src/index.ts' }, format: ['cjs'], @@ -30,6 +25,6 @@ export default defineConfig([ minify: false, splitting: false, treeshake: true, - external: ['commander', 'chalk', 'ora', 'inquirer', 'glob', 'jose'] + external: ['chalk', 'ora', 'execa', 'axios'] } ]); \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts index 1cbcc9b..3041dc8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -26,7 +26,14 @@ export default defineConfig({ 'src/**/*.spec.ts', 'src/__tests__/**', 'src/cli.ts' // CLI entry point is tested via integration - ] + ], + // Coverage thresholds - fail if below 70% + thresholds: { + lines: 70, + functions: 70, + branches: 70, + statements: 70 + } } } }); \ No newline at end of file