fix: enforce atomic agent registration contract
This commit is contained in:
1 parent
47d323c17b
commit
f64d00673c
58 files changed
+2692
-1667
No files matched your search
@@ -0,0 +1,37 @@
|
||||
export interface AgentCredentials {
|
||||
readonly agentId: string;
|
||||
readonly kid: string;
|
||||
readonly publicKey: string;
|
||||
readonly privateKey: string;
|
||||
readonly orgId: string;
|
||||
}
|
||||
|
||||
function requireJwkValue(value: string | undefined, field: 'd' | 'x'): string {
|
||||
if (!value) {
|
||||
throw new Error(`Web Crypto did not export the Ed25519 JWK ${field} value.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function generateAgentCredentials(orgId: string): Promise<AgentCredentials> {
|
||||
const normalizedOrgId = orgId.trim();
|
||||
if (!normalizedOrgId) {
|
||||
throw new Error('An active organization is required to register an agent.');
|
||||
}
|
||||
|
||||
const agentId = `agent-${crypto.randomUUID()}`;
|
||||
const kid = `${agentId}-key-1`;
|
||||
const keyPair = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']);
|
||||
const [privateJwk, publicJwk] = await Promise.all([
|
||||
crypto.subtle.exportKey('jwk', keyPair.privateKey),
|
||||
crypto.subtle.exportKey('jwk', keyPair.publicKey),
|
||||
]);
|
||||
|
||||
return {
|
||||
agentId,
|
||||
kid,
|
||||
publicKey: requireJwkValue(publicJwk.x, 'x'),
|
||||
privateKey: requireJwkValue(privateJwk.d, 'd'),
|
||||
orgId: normalizedOrgId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import type { IntegrationCatalogItem } from './integrations';
|
||||
import type { AgentCredentials } from './credentials';
|
||||
|
||||
export const SDK_LANGUAGES = ['node', 'python', 'go'] as const;
|
||||
export type SdkLanguage = (typeof SDK_LANGUAGES)[number];
|
||||
|
||||
interface InstructionInput {
|
||||
readonly integration: IntegrationCatalogItem;
|
||||
readonly credentials: AgentCredentials;
|
||||
readonly token: string;
|
||||
readonly baseUrl: string;
|
||||
}
|
||||
|
||||
export interface InstallInstructions {
|
||||
readonly setup: string;
|
||||
readonly usage?: string;
|
||||
readonly verify?: string;
|
||||
readonly postInstall?: readonly string[];
|
||||
}
|
||||
|
||||
function quoted(value: string): string {
|
||||
return `"${value.replaceAll('"', '\\"')}"`;
|
||||
}
|
||||
|
||||
function adapterCommand(
|
||||
language: SdkLanguage,
|
||||
{ integration, credentials, token, baseUrl }: InstructionInput,
|
||||
): string {
|
||||
const values = {
|
||||
agent: integration.id,
|
||||
orgId: quoted(credentials.orgId),
|
||||
agentId: quoted(credentials.agentId),
|
||||
privateKey: quoted(credentials.privateKey),
|
||||
kid: quoted(credentials.kid),
|
||||
token: quoted(token),
|
||||
baseUrl: quoted(baseUrl),
|
||||
};
|
||||
|
||||
if (language === 'go') {
|
||||
return [
|
||||
'go install github.com/Elydora-Infrastructure/Elydora-Go-SDK/cmd/elydora@latest',
|
||||
`elydora install --agent ${values.agent} --org-id ${values.orgId} --agent-id ${values.agentId} --private-key ${values.privateKey} --kid ${values.kid} --token ${values.token} --base-url ${values.baseUrl}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const executable = language === 'node'
|
||||
? 'npx @elydora/sdk install'
|
||||
: 'python -m pip install elydora\nelydora install';
|
||||
return `${executable} --agent ${values.agent} --org_id ${values.orgId} --agent_id ${values.agentId} --private_key ${values.privateKey} --kid ${values.kid} --token ${values.token} --base_url ${values.baseUrl}`;
|
||||
}
|
||||
|
||||
function postInstallSteps(integration: IntegrationCatalogItem): readonly string[] | undefined {
|
||||
if (integration.postInstall === 'review-hooks') {
|
||||
return ['Open the agent and run /hooks to review the active Elydora hooks.'];
|
||||
}
|
||||
if (integration.postInstall === 'start-kiro') {
|
||||
return [
|
||||
'Kiro CLI v2: kiro-cli --agent elydora-audit',
|
||||
'Kiro CLI v3: kiro-cli --v3',
|
||||
];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function sdkSetup(language: SdkLanguage, input: InstructionInput): string {
|
||||
const { credentials, token, baseUrl } = input;
|
||||
if (language === 'python') {
|
||||
return `python -m pip install elydora
|
||||
|
||||
from elydora import ElydoraClient
|
||||
|
||||
client = ElydoraClient(
|
||||
org_id=${JSON.stringify(credentials.orgId)},
|
||||
agent_id=${JSON.stringify(credentials.agentId)},
|
||||
private_key=${JSON.stringify(credentials.privateKey)},
|
||||
base_url=${JSON.stringify(baseUrl)},
|
||||
token=${JSON.stringify(token)},
|
||||
)
|
||||
client.set_kid(${JSON.stringify(credentials.kid)})`;
|
||||
}
|
||||
|
||||
if (language === 'go') {
|
||||
return `go get github.com/Elydora-Infrastructure/Elydora-Go-SDK
|
||||
|
||||
client, err := elydora.NewClient(&elydora.Config{
|
||||
OrgID: ${JSON.stringify(credentials.orgId)},
|
||||
AgentID: ${JSON.stringify(credentials.agentId)},
|
||||
PrivateKey: ${JSON.stringify(credentials.privateKey)},
|
||||
BaseURL: ${JSON.stringify(baseUrl)},
|
||||
Token: ${JSON.stringify(token)},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}`;
|
||||
}
|
||||
|
||||
return `npm install @elydora/sdk
|
||||
|
||||
import { ElydoraClient } from '@elydora/sdk';
|
||||
|
||||
const client = new ElydoraClient({
|
||||
orgId: ${JSON.stringify(credentials.orgId)},
|
||||
agentId: ${JSON.stringify(credentials.agentId)},
|
||||
privateKey: ${JSON.stringify(credentials.privateKey)},
|
||||
kid: ${JSON.stringify(credentials.kid)},
|
||||
baseUrl: ${JSON.stringify(baseUrl)},
|
||||
});
|
||||
client.setToken(${JSON.stringify(token)});`;
|
||||
}
|
||||
|
||||
function sdkUsage(language: SdkLanguage): string {
|
||||
if (language === 'python') {
|
||||
return `operation = client.create_operation(
|
||||
operation_type="ai.tool_use",
|
||||
subject={"session_id": "session-123"},
|
||||
action={"tool": "shell"},
|
||||
payload={"command": "example"},
|
||||
)
|
||||
receipt = client.submit_operation(operation)`;
|
||||
}
|
||||
|
||||
if (language === 'go') {
|
||||
return `operation, err := client.CreateOperation(&elydora.CreateOperationParams{
|
||||
OperationType: "ai.tool_use",
|
||||
Subject: map[string]any{"session_id": "session-123"},
|
||||
Action: map[string]any{"tool": "shell"},
|
||||
Payload: map[string]any{"command": "example"},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, err = client.SubmitOperation(operation)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}`;
|
||||
}
|
||||
|
||||
return `const operation = client.createOperation({
|
||||
operationType: 'ai.tool_use',
|
||||
subject: { session_id: 'session-123' },
|
||||
action: { tool: 'shell' },
|
||||
payload: { command: 'example' },
|
||||
});
|
||||
await client.submitOperation(operation);`;
|
||||
}
|
||||
|
||||
export function buildInstallInstructions(
|
||||
language: SdkLanguage,
|
||||
input: InstructionInput,
|
||||
): InstallInstructions {
|
||||
if (input.integration.mode === 'adapter') {
|
||||
return {
|
||||
setup: adapterCommand(language, input),
|
||||
verify: language === 'node' ? 'npx @elydora/sdk status' : 'elydora status',
|
||||
postInstall: postInstallSteps(input.integration),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
setup: sdkSetup(language, input),
|
||||
usage: sdkUsage(language),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
INTEGRATION_TYPES,
|
||||
type IntegrationType,
|
||||
} from '../../../../server/src/shared/types/enums';
|
||||
|
||||
export { INTEGRATION_TYPES };
|
||||
|
||||
export type IntegrationMode = 'adapter' | 'sdk';
|
||||
export type PostInstallAction = 'review-hooks' | 'start-kiro';
|
||||
|
||||
export interface IntegrationCatalogItem {
|
||||
readonly id: IntegrationType;
|
||||
readonly name: string;
|
||||
readonly mode: IntegrationMode;
|
||||
readonly postInstall?: PostInstallAction;
|
||||
}
|
||||
|
||||
const INTEGRATION_METADATA: Record<
|
||||
IntegrationType,
|
||||
Omit<IntegrationCatalogItem, 'id'>
|
||||
> = {
|
||||
augment: { name: 'Augment Code', mode: 'adapter' },
|
||||
claudecode: { name: 'Claude Code', mode: 'adapter' },
|
||||
cline: { name: 'Cline', mode: 'adapter' },
|
||||
codex: { name: 'OpenAI Codex', mode: 'adapter', postInstall: 'review-hooks' },
|
||||
copilot: { name: 'GitHub Copilot CLI', mode: 'adapter' },
|
||||
cursor: { name: 'Cursor', mode: 'adapter' },
|
||||
droid: { name: 'Factory Droid', mode: 'adapter', postInstall: 'review-hooks' },
|
||||
gemini: { name: 'Gemini CLI', mode: 'adapter' },
|
||||
grok: { name: 'Grok Build', mode: 'adapter' },
|
||||
kimi: { name: 'Kimi CLI', mode: 'adapter', postInstall: 'review-hooks' },
|
||||
kirocli: { name: 'Kiro CLI', mode: 'adapter', postInstall: 'start-kiro' },
|
||||
kiroide: { name: 'Kiro IDE', mode: 'adapter' },
|
||||
letta: { name: 'Letta Code', mode: 'adapter' },
|
||||
opencode: { name: 'OpenCode', mode: 'adapter' },
|
||||
qwen: { name: 'Qwen Code', mode: 'adapter', postInstall: 'review-hooks' },
|
||||
enterprise: { name: 'Enterprise Agent', mode: 'sdk' },
|
||||
gui: { name: 'GUI Agent', mode: 'sdk' },
|
||||
sdk: { name: 'SDK', mode: 'sdk' },
|
||||
other: { name: 'Other', mode: 'sdk' },
|
||||
};
|
||||
|
||||
export const INTEGRATION_CATALOG: readonly IntegrationCatalogItem[] =
|
||||
INTEGRATION_TYPES.map((id) => ({ id, ...INTEGRATION_METADATA[id] }));
|
||||
|
||||
export const ADAPTER_INTEGRATION_IDS: ReadonlySet<IntegrationType> = new Set(
|
||||
INTEGRATION_CATALOG.filter(({ mode }) => mode === 'adapter').map(({ id }) => id),
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { IntegrationType, RegisterAgentResponse } from '@elydora/shared';
|
||||
import type { AgentCredentials } from './credentials';
|
||||
|
||||
export class RegistrationContractError extends Error {
|
||||
override readonly name = 'RegistrationContractError';
|
||||
}
|
||||
|
||||
function contractError(message: string): never {
|
||||
throw new RegistrationContractError(message);
|
||||
}
|
||||
|
||||
export function assertRegistrationResponse(
|
||||
response: RegisterAgentResponse,
|
||||
credentials: AgentCredentials,
|
||||
integrationType: IntegrationType,
|
||||
): void {
|
||||
if (response?.agent?.agent_id !== credentials.agentId) {
|
||||
contractError('The registration response returned a different agent ID.');
|
||||
}
|
||||
if (response?.agent?.org_id !== credentials.orgId) {
|
||||
contractError('The registration response returned a different organization ID.');
|
||||
}
|
||||
if (response?.agent?.integration_type !== integrationType) {
|
||||
contractError('The registration response returned a different integration type.');
|
||||
}
|
||||
|
||||
const registeredKey = Array.isArray(response?.keys)
|
||||
? response.keys.find(({ kid }) => kid === credentials.kid)
|
||||
: undefined;
|
||||
if (!registeredKey || registeredKey.public_key !== credentials.publicKey) {
|
||||
contractError('The registration response did not confirm the generated public key.');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user