Skip to content

Latest commit

 

History

History
322 lines (245 loc) · 12.1 KB

File metadata and controls

322 lines (245 loc) · 12.1 KB

UAID Implementation - Approach B (UUID Primary Key)

Overview

Successfully implemented Approach B: UUID as primary key, UAID in separate field. This provides optimal database performance, clean URL routing, and simple migration path.

Architecture Decision

Approach A (Rejected): UAID as primary key

  • Would require String(512) primary key and all foreign keys
  • Complex URL routing with special characters (:, ;, =, https://)
  • Caused 404 errors on agent edit
  • Larger database indexes and slower joins

Approach B (Implemented): UUID as primary key, UAID in separate field ✅

  • Fixed 36-char UUID primary key for optimal indexing
  • Clean URLs: /admin/a2a/123e4567-e89b-...
  • UAID stored in separate nullable field for cross-gateway routing
  • Backward compatible with existing UUID-only agents

Changes Made

1. Database Schema (mcpgateway/db.py)

class A2AAgent(Base):
    # Primary key: always UUID (String(36))
    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: uuid.uuid4().hex)

    # UAID fields (optional, for cross-gateway routing)
    uaid: Mapped[Optional[str]] = mapped_column(String(512), nullable=True, unique=True)
    uaid_registry: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
    uaid_proto: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
    uaid_native_id: Mapped[Optional[str]] = mapped_column(String(767), nullable=True)

Foreign Keys (reverted to String(36)):

  • server_a2a_association.a2a_agent_id
  • a2a_agent_metrics.a2a_agent_id
  • a2a_agent_metrics_hourly.a2a_agent_id

2. Migration (d3e4f5a6b7c8_add_uaid_field_to_a2a_agents.py)

Simple migration: Only adds new columns, no PK/FK changes

def upgrade():
    # Add uaid column with unique index
    op.add_column("a2a_agents", sa.Column("uaid", sa.String(512), nullable=True))
    op.create_index("ix_a2a_agents_uaid", "a2a_agents", ["uaid"], unique=True)

    # Add metadata columns
    op.add_column("a2a_agents", sa.Column("uaid_registry", sa.String(255), nullable=True))
    op.add_column("a2a_agents", sa.Column("uaid_proto", sa.String(50), nullable=True))
    op.add_column("a2a_agents", sa.Column("uaid_native_id", sa.String(767), nullable=True))

Idempotent: Safe to run multiple times, skips if columns exist.

3. Service Layer (mcpgateway/services/a2a_service.py)

# Generate UAID if requested
if getattr(agent_data, "generate_uaid", False):
    uaid = generate_uaid(
        registry=getattr(agent_data, "uaid_registry", None) or "context-forge",
        name=agent_data.name,
        version=getattr(agent_data, "version", None) or "1.0.0",
        protocol=getattr(agent_data, "uaid_protocol", None) or "a2a",
        native_id=agent_data.endpoint_url,
        skills=getattr(agent_data, "uaid_skills", None) or [],
    )

    # Store UAID in separate field, UUID in id (optimal indexing and routing)
    uaid_metadata = {
        "uaid": uaid,
        "uaid_registry": registry,
        "uaid_proto": protocol,
        "uaid_native_id": agent_data.endpoint_url,
    }

# Create agent (id auto-generated as UUID)
new_agent = DbA2AAgent(
    name=agent_data.name,
    **uaid_metadata,  # Empty dict if generate_uaid=False
    ...
)

4. Admin UI (No Changes Needed!)

JavaScript already handles this correctly:

  • Uses agent.id (always UUID) in URLs: /admin/a2a/{agent.id}
  • Checks agent.uaid to show UAID badge and metadata
  • View/edit forms display UAID section when present

5. Tests Updated

# Test expectations updated (tests/unit/mcpgateway/services/test_a2a_service.py)
assert captured_agent.uaid is not None  # Changed from checking id
assert captured_agent.uaid.startswith("uaid:aid:")
assert captured_agent.uaid_registry == "context-forge"

All 142 A2A service tests pass ✅

Benefits Achieved

  1. Fixes 404 Error ✅ URLs use clean UUIDs: /admin/a2a/123e4567-... No special characters (:, ;, =, https://)

  2. Optimal Performance

    • Fixed 36-char primary key (fast btree index)
    • Fixed 36-char foreign keys (efficient joins)
    • No VARCHAR(512) bloat in metrics tables
  3. Simple Migration

    • Only adds columns (no ALTER PRIMARY KEY)
    • No foreign key changes required
    • Idempotent and reversible
  4. Clean Separation

    • id: Internal identifier (database, API, URLs)
    • uaid: External identifier (cross-gateway routing)
  5. Backward Compatible

    • Existing UUID agents unchanged
    • UAID field nullable
    • Mixed UUID/UAID agents in same table
  6. Future Proof

    • Can add other external identifiers later
    • Dual lookup support (by UUID or UAID)
    • Standard database design pattern

Usage Examples

Creating a UAID Agent

curl -X POST /api/a2a/agents \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "name": "My Agent",
    "endpoint_url": "https://agent.example.com",
    "generate_uaid": true,
    "uaid_registry": "context-forge",
    "uaid_protocol": "a2a"
  }'

Response:

{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "uaid": "uaid:aid:4xacv6pi...;uid=0;registry=context-forge;proto=a2a;nativeId=https://agent.example.com",
  "name": "My Agent",
  ...
}

Looking Up by UUID (Internal)

curl -X GET /admin/a2a/123e4567-e89b-12d3-a456-426614174000

Looking Up by UAID (Cross-Gateway)

# Future endpoint: GET /api/a2a/agents/by-uaid?uaid=...
agent = db.query(A2AAgent).filter(A2AAgent.uaid == uaid_string).first()

Test Results

✅ All 142 A2A service tests pass
✅ All 33 UAID utility tests pass
✅ 280 passed, 63 skipped in full test run

Migration Path

For development databases that already have the old VARCHAR(512) columns:

  1. Option A: Drop and recreate database (recommended for dev)

    rm mcp.db
    alembic upgrade head
  2. Option B: Manual ALTER TABLE (if data must be preserved)

    -- SQLite doesn't support ALTER COLUMN, need to recreate table
    -- PostgreSQL can use:
    ALTER TABLE a2a_agents ALTER COLUMN id TYPE VARCHAR(36);
    ALTER TABLE server_a2a_association ALTER COLUMN a2a_agent_id TYPE VARCHAR(36);
    ALTER TABLE a2a_agent_metrics ALTER COLUMN a2a_agent_id TYPE VARCHAR(36);
    ALTER TABLE a2a_agent_metrics_hourly ALTER COLUMN a2a_agent_id TYPE VARCHAR(36);

For fresh databases or production (this PR not yet merged):

  • Migration runs cleanly
  • Creates all tables with correct schema from db.py

Security Considerations

Cross-Gateway Authentication Gap

⚠️ Critical Security Warning: UAID-based cross-gateway routing in v1.0 does NOT implement authentication for outbound HTTP calls to remote gateways.

Current Behavior:

  • Cross-gateway HTTP calls (via extract_routing_info() and a2a_service.invoke_agent()) do NOT forward bearer tokens or session context
  • Remote gateways receive completely unauthenticated requests
  • No user identity or authorization context is passed to target gateway

Security Implications:

  1. Remote Gateway Must Authenticate: Target gateway MUST enforce AUTH_REQUIRED=true. If disabled, public agents are accessible without any authentication barrier.

  2. No Authorization Context: Remote gateway cannot enforce RBAC based on the originating user. All cross-gateway calls execute with the target gateway's public access level (no team scoping, no role checks).

  3. Trust Boundary: This gateway implicitly trusts the remote gateway's access control implementation. A compromised or misconfigured remote gateway becomes a security vector for your federation.

Code Locations:

  • mcpgateway/services/a2a_service.py lines 1855-1888: Comprehensive security comment block
  • mcpgateway/services/a2a_service.py lines 1828-1831: UAID_ALLOWED_DOMAINS validation logic
  • mcpgateway/utils/uaid.py lines 78-95: DoS protection (UAID length validation)

DoS Protection

UAID Length Validation:

  • Maximum UAID length: configurable via UAID_MAX_LENGTH (default 2048, matches database column limit)
  • Validation occurs at parse entry point (parse_uaid() in mcpgateway/utils/uaid.py)
  • Prevents resource exhaustion attacks via excessively long UAID strings
  • Configuration constraint: 512 <= UAID_MAX_LENGTH <= 2048 (cannot exceed database schema limit)

Configuration:

# .env or environment variable
UAID_MAX_LENGTH=2048  # Default, matches database column capacity

Safety Features:

  • Database column hard limit: String(2048) in a2a_agents.uaid column
  • Runtime validation logs warning if UAID_MAX_LENGTH > 2048 and uses database limit
  • Parse rejection throws ValueError with detailed message for monitoring

Domain Allowlist (Cross-Gateway Trust Control)

Configuration:

# .env or environment variable
UAID_ALLOWED_DOMAINS=["trusted-gateway.example.com", "partner.org"]

Behavior:

  • Empty list [] (default): Allow cross-gateway routing to ANY domain (least secure)
  • Non-empty list: Only allow routing to endpoints ending in specified domain suffixes (more secure)
  • Validation occurs in mcpgateway/services/a2a_service.py lines 1828-1831

Recommended Production Configuration:

# Option 1: Allowlist trusted gateways only (recommended)
UAID_ALLOWED_DOMAINS=["production-gateway.internal", "trusted-partner.com"]

# Option 2: If no trusted external gateways, leave empty to prevent cross-gateway routing
# Note: Empty list allows ALL domains by code design. To prevent cross-gateway routing,
# ensure no UAID agents are registered, or implement network-level controls.

Current Mitigations

  1. UAID_ALLOWED_DOMAINS: Restricts outbound calls to operator-specified trusted domains
  2. Correlation ID Logging: Every cross-gateway call logs a correlation ID for distributed tracing and security audit
  3. Operator Guidance: Documentation (README.md, .env.example, code comments) clearly warns about authentication gap
  4. DoS Protection: UAID length validation prevents resource exhaustion attacks

Future Security Enhancements (Roadmap)

The following security features are planned for future releases:

  1. Bearer Token Forwarding: Pass originating user's authentication token to remote gateway (requires gateway-to-gateway trust establishment protocol)
  2. Mutual TLS (mTLS): Gateway-to-gateway authentication via X.509 certificates
  3. Trusted Gateway Registry: Cryptographic signature verification for gateway identity (prevents MITM and gateway impersonation)
  4. Per-UAID Access Policies: Fine-grained allowlist/denylist at the individual UAID level (beyond domain-level control)

Security Testing

Test Coverage:

  • tests/unit/mcpgateway/utils/test_uaid.py - DoS protection tests (lines added in Task 5)
  • tests/unit/mcpgateway/services/test_a2a_service.py - Cross-gateway routing HTTP error handling, domain allowlist enforcement, team-based access control (lines 3520-3609)

Manual Testing Checklist:

  1. Verify UAID_MAX_LENGTH rejects UAIDs exceeding configured limit
  2. Verify UAID_ALLOWED_DOMAINS blocks disallowed domains
  3. Verify cross-gateway calls log correlation IDs for tracing
  4. Verify remote gateway authentication enforcement (external test with partner gateway)

Operator Recommendations

Production Deployment:

  1. Set UAID_ALLOWED_DOMAINS to a restrictive allowlist of trusted gateway domains
  2. Ensure AUTH_REQUIRED=true on ALL gateways in your federation
  3. Enable observability logging to monitor cross-gateway calls via correlation IDs
  4. Document your gateway federation topology and trust relationships
  5. Regularly audit UAID_ALLOWED_DOMAINS allowlist for stale or compromised domains

Risk Assessment:

  • Low Risk: Internal-only deployment with no external UAID agents (no cross-gateway routing)
  • Medium Risk: Federated deployment with trusted partner gateways using UAID_ALLOWED_DOMAINS allowlist
  • High Risk: Open federation with UAID_ALLOWED_DOMAINS=[] (empty, allows all domains) - NOT RECOMMENDED for production

Documentation

See also:

  • /Users/rakhidutta/pr/mcp-context-forge/mcpgateway/utils/uaid.py - UAID generation
  • /Users/rakhidutta/pr/mcp-context-forge/tests/unit/mcpgateway/utils/test_uaid.py - UAID tests
  • HCS-14 specification (referenced in code comments)