/** * AGNTCY Conformance Test Suite for SentryAgent.ai AgentIdP. * * Verifies that the platform conforms to the AGNTCY agent identity specification: * 1. Agent registration creates a DID:WEB identifier. * 2. Token issuance for agent client (client_credentials grant). * 3. A2A delegation chain create + verify (gated by A2A_ENABLED). * 4. Compliance report generation returns a valid AGNTCY structure. */ import crypto from 'crypto'; import request from 'supertest'; import { Application } from 'express'; import { v4 as uuidv4 } from 'uuid'; import { Pool } from 'pg'; // ── Environment setup — must happen before importing app ────────────────────── const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, }); process.env['DATABASE_URL'] = process.env['TEST_DATABASE_URL'] ?? 'postgresql://sentryagent:sentryagent@localhost:5432/sentryagent_idp_test'; process.env['REDIS_URL'] = process.env['TEST_REDIS_URL'] ?? 'redis://localhost:6379/1'; process.env['JWT_PRIVATE_KEY'] = privateKey; process.env['JWT_PUBLIC_KEY'] = publicKey; process.env['NODE_ENV'] = 'test'; process.env['COMPLIANCE_ENABLED'] = 'true'; // Ensure A2A tests only run when the feature is on const a2aEnabled = process.env['A2A_ENABLED'] !== 'false'; // ── Imports (after env is set) ──────────────────────────────────────────────── import { createApp } from '../../src/app.js'; import { signToken } from '../../src/utils/jwt.js'; import { closePool } from '../../src/db/pool.js'; import { closeRedisClient } from '../../src/cache/redis.js'; // ── Helpers ─────────────────────────────────────────────────────────────────── /** * Creates a signed JWT for use in test requests. * * @param sub - Subject (agentId). * @param scope - Space-separated OAuth 2.0 scopes. * @param organizationId - Optional organization_id claim. * @returns Signed JWT string. */ function makeToken( sub: string, scope: string = 'agents:read agents:write audit:read', organizationId?: string, ): string { const payload: Record = { sub, client_id: sub, scope, jti: uuidv4() }; if (organizationId !== undefined) { payload['organization_id'] = organizationId; } return signToken(payload as Parameters[0], privateKey); } // ── Test suite ──────────────────────────────────────────────────────────────── describe('AGNTCY Conformance Suite', () => { let app: Application; let pool: Pool; // ── Migrations required for conformance tests ───────────────────────────── const migrations = [ `CREATE TABLE IF NOT EXISTS organizations ( organization_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL UNIQUE, tier VARCHAR(32) NOT NULL DEFAULT 'free', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() )`, `CREATE TABLE IF NOT EXISTS agents ( agent_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID REFERENCES organizations(organization_id), email VARCHAR(255) NOT NULL UNIQUE, agent_type VARCHAR(32) NOT NULL, version VARCHAR(64) NOT NULL, capabilities TEXT[] NOT NULL DEFAULT '{}', owner VARCHAR(128) NOT NULL, deployment_env VARCHAR(16) NOT NULL, status VARCHAR(24) NOT NULL DEFAULT 'active', did VARCHAR(512), did_document JSONB, did_created_at TIMESTAMPTZ, is_public BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() )`, `CREATE TABLE IF NOT EXISTS credentials ( credential_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), client_id UUID NOT NULL, secret_hash VARCHAR(255) NOT NULL, vault_path VARCHAR(512), status VARCHAR(16) NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ )`, `CREATE TABLE IF NOT EXISTS audit_events ( event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), agent_id UUID NOT NULL, organization_id UUID, action VARCHAR(64) NOT NULL, outcome VARCHAR(16) NOT NULL, ip_address VARCHAR(64) NOT NULL DEFAULT '127.0.0.1', user_agent TEXT NOT NULL DEFAULT 'test', metadata JSONB NOT NULL DEFAULT '{}', hash VARCHAR(64) NOT NULL DEFAULT '', previous_hash VARCHAR(64) NOT NULL DEFAULT '', timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() )`, `CREATE TABLE IF NOT EXISTS token_revocations ( jti UUID PRIMARY KEY, expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ NOT NULL DEFAULT NOW() )`, `CREATE TABLE IF NOT EXISTS agent_did_keys ( key_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), agent_id UUID NOT NULL REFERENCES agents(agent_id), key_type VARCHAR(32) NOT NULL, public_key TEXT NOT NULL, private_key_encrypted TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() )`, `CREATE TABLE IF NOT EXISTS delegation_chains ( chain_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), delegator_id UUID NOT NULL, delegatee_id UUID NOT NULL, scope TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ, token TEXT )`, ]; beforeAll(async () => { app = await createApp(); pool = new Pool({ connectionString: process.env['DATABASE_URL'] }); for (const sql of migrations) { await pool.query(sql); } }); afterEach(async () => { await pool.query('DELETE FROM audit_events'); await pool.query('DELETE FROM credentials'); await pool.query('DELETE FROM agents'); await pool.query('DELETE FROM organizations'); }); afterAll(async () => { await pool.end(); await closePool(); await closeRedisClient(); }); // ── Conformance test 1: Agent registration creates DID:WEB identifier ───── describe('Conformance 1 — Agent registration creates DID:WEB identifier', () => { it('should register an agent and return a did field starting with did:web:', async () => { const agentId = uuidv4(); const token = makeToken(agentId, 'agents:read agents:write'); const res = await request(app) .post('/api/v1/agents') .set('Authorization', `Bearer ${token}`) .send({ email: `conformance-agent-${agentId}@sentryagent.ai`, agentType: 'screener', version: '1.0.0', capabilities: ['identity:read'], owner: 'conformance-team', deploymentEnv: 'development', }); expect(res.status).toBe(201); expect(res.body.agentId).toBeDefined(); // Verify DID is present and conforms to did:web: scheme if (res.body.did !== undefined && res.body.did !== null) { expect(typeof res.body.did).toBe('string'); expect((res.body.did as string).startsWith('did:web:')).toBe(true); } }); }); // ── Conformance test 2: Token issuance via client_credentials grant ──────── describe('Conformance 2 — Token issuance for agent client (client_credentials)', () => { it('should issue a Bearer JWT via client_credentials grant', async () => { const agentId = uuidv4(); const setupToken = makeToken(agentId, 'agents:read agents:write'); // Register agent await pool.query( `INSERT INTO agents (agent_id, email, agent_type, version, capabilities, owner, deployment_env, status) VALUES ($1, $2, 'screener', '1.0.0', '{"identity:read"}', 'conformance-team', 'development', 'active')`, [agentId, `cred-test-${agentId}@sentryagent.ai`], ); // Generate credentials via API const credRes = await request(app) .post(`/api/v1/agents/${agentId}/credentials`) .set('Authorization', `Bearer ${setupToken}`) .send({}); expect(credRes.status).toBe(201); const { clientSecret } = credRes.body as { clientSecret: string }; // Issue token via client_credentials grant const tokenRes = await request(app) .post('/api/v1/token') .type('form') .send({ grant_type: 'client_credentials', client_id: agentId, client_secret: clientSecret, scope: 'agents:read', }); expect(tokenRes.status).toBe(200); expect(tokenRes.body.access_token).toBeDefined(); expect(tokenRes.body.token_type).toBe('Bearer'); expect(typeof tokenRes.body.access_token).toBe('string'); // Verify JWT structure (3 parts separated by dots) const jwtParts = (tokenRes.body.access_token as string).split('.'); expect(jwtParts).toHaveLength(3); }); }); // ── Conformance test 3: A2A delegation chain (gated by A2A_ENABLED) ──────── (a2aEnabled ? describe : describe.skip)( 'Conformance 3 — A2A delegation chain create + verify (A2A_ENABLED=true)', () => { it('should create and verify a delegation chain between two agents', async () => { const delegatorId = uuidv4(); const delegateeId = uuidv4(); // Insert both agents directly await pool.query( `INSERT INTO agents (agent_id, email, agent_type, version, capabilities, owner, deployment_env, status) VALUES ($1, $2, 'orchestrator', '1.0.0', '{"agents:delegate"}', 'delegator-team', 'development', 'active'), ($3, $4, 'screener', '1.0.0', '{"agents:read"}', 'delegatee-team', 'development', 'active')`, [ delegatorId, `delegator-${delegatorId}@sentryagent.ai`, delegateeId, `delegatee-${delegateeId}@sentryagent.ai`, ], ); const delegatorToken = makeToken(delegatorId, 'agents:read agents:write'); // Create delegation chain const createRes = await request(app) .post('/api/v1/oauth2/token/delegate') .set('Authorization', `Bearer ${delegatorToken}`) .send({ delegatee_id: delegateeId, scope: 'agents:read', }); // Accept 201 (created) or 200 (already exists) expect([200, 201]).toContain(createRes.status); const delegationToken: string = createRes.body.token ?? createRes.body.delegation_token ?? createRes.body.access_token ?? ''; // Verify delegation chain if a token was returned if (delegationToken !== '') { const verifyRes = await request(app) .post('/api/v1/oauth2/token/verify-delegation') .set('Authorization', `Bearer ${delegatorToken}`) .send({ token: delegationToken }); expect([200, 204]).toContain(verifyRes.status); } }); }, ); // ── Conformance test 4: Compliance report returns valid AGNTCY structure ─── describe('Conformance 4 — Compliance report returns valid AGNTCY structure', () => { it('should return a compliance report with all required AGNTCY fields', async () => { const orgId = uuidv4(); const agentId = uuidv4(); // Create organization and agent await pool.query( `INSERT INTO organizations (organization_id, name, tier) VALUES ($1, $2, 'free')`, [orgId, `conformance-org-${orgId}`], ); await pool.query( `INSERT INTO agents (agent_id, organization_id, email, agent_type, version, capabilities, owner, deployment_env, status) VALUES ($1, $2, $3, 'screener', '1.0.0', '{"identity:read"}', 'conformance-team', 'development', 'active')`, [agentId, orgId, `report-test-${agentId}@sentryagent.ai`], ); const token = makeToken(agentId, 'agents:read audit:read', orgId); const res = await request(app) .get('/api/v1/compliance/report') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); // Verify all required AGNTCY fields are present expect(res.body.generated_at).toBeDefined(); expect(res.body.tenant_id).toBeDefined(); expect(res.body.agntcy_schema_version).toBe('1.0'); expect(res.body.sections).toBeInstanceOf(Array); expect(res.body.sections.length).toBeGreaterThan(0); expect(['pass', 'fail', 'warn']).toContain(res.body.overall_status); // Verify generated_at is a valid ISO 8601 string const generatedAt = new Date(res.body.generated_at as string); expect(generatedAt.getTime()).not.toBeNaN(); // Verify each section has required fields for (const section of res.body.sections as Array>) { expect(typeof section['name']).toBe('string'); expect(['pass', 'fail', 'warn']).toContain(section['status']); expect(typeof section['details']).toBe('string'); } // Verify expected sections are present const sectionNames = (res.body.sections as Array>).map( (s) => s['name'], ); expect(sectionNames).toContain('agent-identity'); expect(sectionNames).toContain('audit-trail'); }); it('should return X-Cache: HIT on second request within cache window', async () => { const orgId = uuidv4(); const agentId = uuidv4(); await pool.query( `INSERT INTO organizations (organization_id, name, tier) VALUES ($1, $2, 'free')`, [orgId, `cache-test-org-${orgId}`], ); await pool.query( `INSERT INTO agents (agent_id, organization_id, email, agent_type, version, capabilities, owner, deployment_env, status) VALUES ($1, $2, $3, 'screener', '1.0.0', '{}', 'cache-team', 'development', 'active')`, [agentId, orgId, `cache-test-${agentId}@sentryagent.ai`], ); const token = makeToken(agentId, 'agents:read audit:read', orgId); // First request — populates cache await request(app) .get('/api/v1/compliance/report') .set('Authorization', `Bearer ${token}`); // Second request — should be served from cache const secondRes = await request(app) .get('/api/v1/compliance/report') .set('Authorization', `Bearer ${token}`); expect(secondRes.status).toBe(200); expect(secondRes.headers['x-cache']).toBe('HIT'); expect(secondRes.body.from_cache).toBe(true); }); }); });