fix(sdk): enforce agent registration integration type
This commit is contained in:
1 parent
d59b0101b1
commit
0e35b845a4
7 files changed
+171
-4
No files matched your search
@@ -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.
|
- 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.
|
- 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.
|
- 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.
|
- Synchronize completed provider behavior into the Python SDK, Go SDK, Open Source distribution, Console, Docs, and landing page through separate reviewed commits.
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ const result = await client.verifyOperation(operationId);
|
|||||||
// Register a new agent
|
// Register a new agent
|
||||||
const agent = await client.registerAgent({
|
const agent = await client.registerAgent({
|
||||||
agent_id: 'my-agent',
|
agent_id: 'my-agent',
|
||||||
|
integration_type: 'codex',
|
||||||
display_name: 'My Agent',
|
display_name: 'My Agent',
|
||||||
responsible_entity: 'team@example.com',
|
responsible_entity: 'team@example.com',
|
||||||
keys: [{ kid: 'key-v1', public_key: '<base64url>', algorithm: 'ed25519' }],
|
keys: [{ kid: 'key-v1', public_key: '<base64url>', algorithm: 'ed25519' }],
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import type {
|
|||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { ElydoraError } from './errors.js';
|
import { ElydoraError } from './errors.js';
|
||||||
|
import { assertIntegrationType } from './integration-types.js';
|
||||||
import {
|
import {
|
||||||
jcsCanonicalise,
|
jcsCanonicalise,
|
||||||
computePayloadHash,
|
computePayloadHash,
|
||||||
@@ -161,6 +162,7 @@ export class ElydoraClient {
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
async registerAgent(request: RegisterAgentRequest): Promise<RegisterAgentResponse> {
|
async registerAgent(request: RegisterAgentRequest): Promise<RegisterAgentResponse> {
|
||||||
|
assertIntegrationType(request?.integration_type);
|
||||||
return this.request<RegisterAgentResponse>('POST', '/v1/agents/register', request);
|
return this.request<RegisterAgentResponse>('POST', '/v1/agents/register', request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +171,7 @@ export class ElydoraClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateAgent(agentId: string, integType: IntegrationType): Promise<UpdateAgentResponse> {
|
async updateAgent(agentId: string, integType: IntegrationType): Promise<UpdateAgentResponse> {
|
||||||
|
assertIntegrationType(integType);
|
||||||
return this.request<UpdateAgentResponse>('PATCH', `/v1/agents/${encodeURIComponent(agentId)}`, { integration_type: integType });
|
return this.request<UpdateAgentResponse>('PATCH', `/v1/agents/${encodeURIComponent(agentId)}`, { integration_type: integType });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export { ElydoraClient } from './client.js';
|
export { ElydoraClient } from './client.js';
|
||||||
export { ElydoraError } from './errors.js';
|
export { ElydoraError } from './errors.js';
|
||||||
|
export { INTEGRATION_TYPES } from './types.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
jcsCanonicalise,
|
jcsCanonicalise,
|
||||||
|
|||||||
@@ -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(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
-4
@@ -11,10 +11,28 @@ export type RbacRole =
|
|||||||
| 'compliance_auditor'
|
| 'compliance_auditor'
|
||||||
| 'readonly_investigator'
|
| 'readonly_investigator'
|
||||||
| 'integration_engineer';
|
| 'integration_engineer';
|
||||||
export type IntegrationType =
|
export const INTEGRATION_TYPES = [
|
||||||
| 'claudecode' | 'cursor' | 'gemini' | 'kirocli' | 'kiroide'
|
'augment',
|
||||||
| 'opencode' | 'copilot' | 'letta' | 'codex' | 'kimi'
|
'claudecode',
|
||||||
| 'enterprise' | 'gui' | 'sdk' | 'other';
|
'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 =
|
export type AdminAction =
|
||||||
| 'agent.register' | 'agent.update' | 'agent.freeze' | 'agent.unfreeze'
|
| 'agent.register' | 'agent.update' | 'agent.freeze' | 'agent.unfreeze'
|
||||||
| 'agent.revoke' | 'agent.delete' | 'key.revoke' | 'export.create'
|
| 'agent.revoke' | 'agent.delete' | 'key.revoke' | 'export.create'
|
||||||
@@ -193,6 +211,7 @@ export interface EAR {
|
|||||||
|
|
||||||
export interface RegisterAgentRequest {
|
export interface RegisterAgentRequest {
|
||||||
readonly agent_id: string;
|
readonly agent_id: string;
|
||||||
|
readonly integration_type: IntegrationType;
|
||||||
readonly display_name?: string;
|
readonly display_name?: string;
|
||||||
readonly responsible_entity?: string;
|
readonly responsible_entity?: string;
|
||||||
readonly keys: ReadonlyArray<{
|
readonly keys: ReadonlyArray<{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user