polyc-a2a 2026.9.0

polychrome A2A edge: serves a domain-signed Agent Card and drives message/send tasks onto a turn.
//! `GET /.well-known/agent-card.json` serves a v1.0-shaped Agent Card carrying
//! a signature that verifies against the deployment's ed25519 principal.

#![allow(clippy::unwrap_used, clippy::pedantic, missing_docs)]

use std::{future::Future, pin::Pin, sync::Arc};

use axum::body::Body;
use http::{Request, StatusCode};
use http_body_util::BodyExt as _;
use polyc_a2a::{
    AppState, InMemoryTaskStore, TurnOutcome, TurnRequest, TurnRunner,
    UnconfiguredApprovalResponder,
    card::{CardConfig, signed_card, verify_card},
    router,
};
use polyc_crypto::Signer;
use polyc_runtime::admission::AdmissionGate;
use serde_json::Value;
use tower::ServiceExt as _;

/// A turn runner the card endpoint never invokes (GET runs no turn).
struct NoopRunner;
impl TurnRunner for NoopRunner {
    fn run_turn<'a>(
        &'a self,
        _req: TurnRequest,
    ) -> Pin<Box<dyn Future<Output = TurnOutcome> + Send + 'a>> {
        Box::pin(async {
            TurnOutcome::Completed {
                text: String::new(),
            }
        })
    }
}

fn state(signer: &Signer) -> AppState {
    let card = signed_card(
        &CardConfig {
            name: "Polychrome".to_owned(),
            description: "A polychrome deployment reachable over A2A.".to_owned(),
            url: "https://agent.example/".to_owned(),
            version: "0.1.3".to_owned(),
        },
        signer,
    );
    AppState {
        card: Arc::new(card),
        runner: Arc::new(NoopRunner),
        approvals: Arc::new(UnconfiguredApprovalResponder),
        store: Arc::new(InMemoryTaskStore::new()),
        turn_limit: AdmissionGate::new(64),
        // The card endpoint is unauthenticated discovery metadata; only
        // `POST /` enforces the bearer token (see `tests/auth.rs`).
        peers: polyc_a2a::PeerAuthenticator::single("test-peer", "unused-by-agent-card-endpoint")
            .unwrap(),
    }
}

#[tokio::test]
async fn agent_card_is_v1_shaped_and_signed() {
    let signer = Signer::from_seed(7);
    let app = router(state(&signer));

    let response = app
        .oneshot(
            Request::builder()
                .uri("/.well-known/agent-card.json")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::OK);
    let body = response.into_body().collect().await.unwrap().to_bytes();
    let card: Value = serde_json::from_slice(&body).unwrap();

    // v1.0 shape: the transport block is `supportedInterfaces`, NOT top-level
    // `url`/`preferredTransport`/`protocolVersion`/`stateTransitionHistory`.
    assert_eq!(card["name"], "Polychrome");
    assert_eq!(card["version"], "0.1.3");
    assert!(card.get("url").is_none(), "v1.0 has no top-level url");
    assert!(card.get("protocolVersion").is_none());
    assert_eq!(
        card["supportedInterfaces"][0]["url"],
        "https://agent.example/"
    );
    assert_eq!(card["supportedInterfaces"][0]["protocolBinding"], "JSONRPC");
    assert_eq!(card["supportedInterfaces"][0]["protocolVersion"], "1.0");
    assert!(card["capabilities"].is_object());
    assert!(card["capabilities"].get("stateTransitionHistory").is_none());
    assert!(card["defaultInputModes"].as_array().is_some());
    assert!(card["defaultOutputModes"].as_array().is_some());
    assert!(
        card["skills"]
            .as_array()
            .is_some_and(|skills| !skills.is_empty())
    );

    // Signed identity: a `signatures` array that verifies against the principal.
    assert!(
        card["signatures"]
            .as_array()
            .is_some_and(|sigs| !sigs.is_empty())
    );
    assert!(
        verify_card(&card, &signer.public_key_bytes()),
        "card signature must verify against the deployment principal"
    );
}