diff --git a/AGENTS.md b/AGENTS.md index 63bda70..17679b1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,7 @@ This repository owns the published `@elydora/sdk` package, its `elydora` CLI, lo - Verify every agent hook contract against current official provider documentation before changing an adapter. - Treat `../Elydora-Open-Source/integrations/catalog.json` as the cross-product provider inventory. +- Keep the exported `INTEGRATION_TYPES` tuple aligned with that inventory, and require `integration_type` in every agent registration request. - Keep adapter delivery claims aligned with executable tests in this repository. - Synchronize completed provider behavior into the Python SDK, Go SDK, Open Source distribution, Console, Docs, and landing page through separate reviewed commits. diff --git a/README.md b/README.md index b7af7b1..252c5e0 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ const result = await client.verifyOperation(operationId); // Register a new agent const agent = await client.registerAgent({ agent_id: 'my-agent', + integration_type: 'codex', display_name: 'My Agent', responsible_entity: 'team@example.com', keys: [{ kid: 'key-v1', public_key: '', algorithm: 'ed25519' }], diff --git a/src/client.ts b/src/client.ts index da2242f..e4283c8 100644 --- a/src/client.ts +++ b/src/client.ts @@ -37,6 +37,7 @@ import type { ErrorResponse, } from './types.js'; import { ElydoraError } from './errors.js'; +import { assertIntegrationType } from './integration-types.js'; import { jcsCanonicalise, computePayloadHash, @@ -161,6 +162,7 @@ export class ElydoraClient { // ------------------------------------------------------------------------- async registerAgent(request: RegisterAgentRequest): Promise { + assertIntegrationType(request?.integration_type); return this.request('POST', '/v1/agents/register', request); } @@ -169,6 +171,7 @@ export class ElydoraClient { } async updateAgent(agentId: string, integType: IntegrationType): Promise { + assertIntegrationType(integType); return this.request('PATCH', `/v1/agents/${encodeURIComponent(agentId)}`, { integration_type: integType }); } diff --git a/src/index.ts b/src/index.ts index 6c209a6..42e3949 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ export { ElydoraClient } from './client.js'; export { ElydoraError } from './errors.js'; +export { INTEGRATION_TYPES } from './types.js'; export { jcsCanonicalise, diff --git a/src/integration-types.ts b/src/integration-types.ts new file mode 100644 index 0000000..c14119e --- /dev/null +++ b/src/integration-types.ts @@ -0,0 +1,11 @@ +import { INTEGRATION_TYPES, type IntegrationType } from './types.js'; + +export function assertIntegrationType(value: unknown): asserts value is IntegrationType { + const isSupported = typeof value === 'string' + && INTEGRATION_TYPES.some((integrationType) => integrationType === value); + if (!isSupported) { + throw new TypeError( + `Invalid integration_type "${String(value)}". Expected one of: ${INTEGRATION_TYPES.join(', ')}`, + ); + } +} diff --git a/src/types.ts b/src/types.ts index cc66d0d..e3fa1da 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,10 +11,28 @@ export type RbacRole = | 'compliance_auditor' | 'readonly_investigator' | 'integration_engineer'; -export type IntegrationType = - | 'claudecode' | 'cursor' | 'gemini' | 'kirocli' | 'kiroide' - | 'opencode' | 'copilot' | 'letta' | 'codex' | 'kimi' - | 'enterprise' | 'gui' | 'sdk' | 'other'; +export const INTEGRATION_TYPES = [ + 'augment', + 'claudecode', + 'cline', + 'codex', + 'copilot', + 'cursor', + 'droid', + 'gemini', + 'grok', + 'kimi', + 'kirocli', + 'kiroide', + 'letta', + 'opencode', + 'qwen', + 'enterprise', + 'gui', + 'sdk', + 'other', +] as const; +export type IntegrationType = (typeof INTEGRATION_TYPES)[number]; export type AdminAction = | 'agent.register' | 'agent.update' | 'agent.freeze' | 'agent.unfreeze' | 'agent.revoke' | 'agent.delete' | 'key.revoke' | 'export.create' @@ -193,6 +211,7 @@ export interface EAR { export interface RegisterAgentRequest { readonly agent_id: string; + readonly integration_type: IntegrationType; readonly display_name?: string; readonly responsible_entity?: string; readonly keys: ReadonlyArray<{ diff --git a/test/agent-registration-contract.test.mjs b/test/agent-registration-contract.test.mjs new file mode 100644 index 0000000..adcea57 --- /dev/null +++ b/test/agent-registration-contract.test.mjs @@ -0,0 +1,131 @@ +import assert from 'node:assert/strict'; +import http from 'node:http'; +import test from 'node:test'; + +import { ElydoraClient, INTEGRATION_TYPES } from '../dist/index.js'; + +const EXPECTED_INTEGRATION_TYPES = [ + 'augment', + 'claudecode', + 'cline', + 'codex', + 'copilot', + 'cursor', + 'droid', + 'gemini', + 'grok', + 'kimi', + 'kirocli', + 'kiroide', + 'letta', + 'opencode', + 'qwen', + 'enterprise', + 'gui', + 'sdk', + 'other', +]; + +async function createFixture() { + const requests = []; + const server = http.createServer(async (request, response) => { + const chunks = []; + for await (const chunk of request) chunks.push(chunk); + const body = JSON.parse(Buffer.concat(chunks).toString('utf8')); + requests.push({ + method: request.method, + path: request.url, + authorization: request.headers.authorization, + body, + }); + response.writeHead(201, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify({ + agent: { + agent_id: body.agent_id, + org_id: 'org-1', + display_name: body.display_name, + responsible_entity: body.responsible_entity, + integration_type: body.integration_type, + status: 'active', + created_at: 1, + updated_at: 1, + }, + keys: [], + })); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const address = server.address(); + assert(address && typeof address === 'object'); + + const client = new ElydoraClient({ + orgId: 'org-1', + agentId: 'admin-agent', + privateKey: 'unused', + baseUrl: `http://127.0.0.1:${address.port}`, + maxRetries: 0, + }); + client.setToken('api-token'); + + return { + client, + requests, + async close() { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + }, + }; +} + +test('integration types match the public API contract', () => { + assert.deepEqual(INTEGRATION_TYPES, EXPECTED_INTEGRATION_TYPES); +}); + +test('agent registration sends integration_type in one authenticated request', async () => { + const fixture = await createFixture(); + try { + await assert.rejects( + fixture.client.registerAgent({ agent_id: 'agent-1', keys: [] }), + { + name: 'TypeError', + message: /Invalid integration_type "undefined"/, + }, + ); + await assert.rejects( + fixture.client.registerAgent({ + agent_id: 'agent-1', + integration_type: 'future-cli', + keys: [], + }), + { + name: 'TypeError', + message: /Invalid integration_type "future-cli"/, + }, + ); + assert.equal(fixture.requests.length, 0); + + const response = await fixture.client.registerAgent({ + agent_id: 'agent-1', + integration_type: 'grok', + display_name: 'Grok Agent', + responsible_entity: 'platform@example.com', + keys: [{ kid: 'key-v1', public_key: 'public-key', algorithm: 'ed25519' }], + }); + + assert.equal(response.agent.integration_type, 'grok'); + assert.deepEqual(fixture.requests, [{ + method: 'POST', + path: '/v1/agents/register', + authorization: 'Bearer api-token', + body: { + agent_id: 'agent-1', + integration_type: 'grok', + display_name: 'Grok Agent', + responsible_entity: 'platform@example.com', + keys: [{ kid: 'key-v1', public_key: 'public-key', algorithm: 'ed25519' }], + }, + }]); + } finally { + await fixture.close(); + } +});