fix(console): mirror secure agent setup commands
This commit is contained in:
1 parent
78cb95e288
commit
99dc0fe122
9 files changed
+286
-59
No files matched your search
@@ -56,6 +56,7 @@ This repository publishes the open-source server, console, integration catalog,
|
|||||||
- Avoid compatibility shims without a named public contract.
|
- Avoid compatibility shims without a named public contract.
|
||||||
- Preserve the minimum runtime versions declared by each package.
|
- Preserve the minimum runtime versions declared by each package.
|
||||||
- Keep private keys and API tokens out of process arguments and generated setup commands. Accept them through hidden terminal input or owner-only credential files.
|
- Keep private keys and API tokens out of process arguments and generated setup commands. Accept them through hidden terminal input or owner-only credential files.
|
||||||
|
- Keep Console instruction generation limited to agent identity and public configuration. Adapter commands use hidden CLI prompts; direct SDK examples read named runtime environment variables. Expose credentials through explicit copy controls only.
|
||||||
- Persist credential-bearing files through owner-only same-directory temporary files and atomic rename.
|
- Persist credential-bearing files through owner-only same-directory temporary files and atomic rename.
|
||||||
- Resolve every agent runtime directory as one physical child of `~/.elydora`; reject separators, traversal segments, cross-platform reserved names, symbolic-link directories, and linked identity configs before writes or recursive removal. Validate stored directory identity before changing host CLI configuration, and require an explicit agent ID when discovery is ambiguous.
|
- Resolve every agent runtime directory as one physical child of `~/.elydora`; reject separators, traversal segments, cross-platform reserved names, symbolic-link directories, and linked identity configs before writes or recursive removal. Validate stored directory identity before changing host CLI configuration, and require an explicit agent ID when discovery is ambiguous.
|
||||||
|
|
||||||
|
|||||||
@@ -52,26 +52,26 @@ test('generates every adapter command with the verified SDK flag contract', () =
|
|||||||
for (const language of SDK_LANGUAGES) {
|
for (const language of SDK_LANGUAGES) {
|
||||||
const instructions = buildInstallInstructions(language, {
|
const instructions = buildInstallInstructions(language, {
|
||||||
integration,
|
integration,
|
||||||
credentials,
|
identity: credentials,
|
||||||
token: 'token-contract',
|
|
||||||
baseUrl: 'https://api.example.com',
|
baseUrl: 'https://api.example.com',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(instructions.setup).toContain(`--agent ${integration.id}`);
|
expect(instructions.setup).toContain(`--agent ${integration.id}`);
|
||||||
expect(instructions.setup).toContain('"private-key"');
|
expect(instructions.setup).toContain("'https://api.example.com'");
|
||||||
expect(instructions.setup).toContain('"token-contract"');
|
expect(instructions.setup).not.toContain('private-key');
|
||||||
expect(instructions.setup).toContain('"https://api.example.com"');
|
expect(instructions.setup).not.toContain('private_key');
|
||||||
|
expect(instructions.setup).not.toContain('--token');
|
||||||
|
expect(instructions.setup).not.toContain('token-contract');
|
||||||
|
expect(instructions.secretDelivery).toBe('hidden-prompts');
|
||||||
|
|
||||||
if (language === 'go') {
|
if (language === 'go') {
|
||||||
expect(instructions.setup).toContain('--org-id "org-contract"');
|
expect(instructions.setup).toContain("--org-id 'org-contract'");
|
||||||
expect(instructions.setup).toContain('--agent-id "agent-contract"');
|
expect(instructions.setup).toContain("--agent-id 'agent-contract'");
|
||||||
expect(instructions.setup).toContain('--private-key "private-key"');
|
expect(instructions.setup).toContain("--base-url 'https://api.example.com'");
|
||||||
expect(instructions.setup).toContain('--base-url "https://api.example.com"');
|
|
||||||
} else {
|
} else {
|
||||||
expect(instructions.setup).toContain('--org_id "org-contract"');
|
expect(instructions.setup).toContain("--org_id 'org-contract'");
|
||||||
expect(instructions.setup).toContain('--agent_id "agent-contract"');
|
expect(instructions.setup).toContain("--agent_id 'agent-contract'");
|
||||||
expect(instructions.setup).toContain('--private_key "private-key"');
|
expect(instructions.setup).toContain("--base_url 'https://api.example.com'");
|
||||||
expect(instructions.setup).toContain('--base_url "https://api.example.com"');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,17 +84,43 @@ test('generates direct SDK setup and operation code for each language', () => {
|
|||||||
for (const language of SDK_LANGUAGES) {
|
for (const language of SDK_LANGUAGES) {
|
||||||
const instructions = buildInstallInstructions(language, {
|
const instructions = buildInstallInstructions(language, {
|
||||||
integration: integration!,
|
integration: integration!,
|
||||||
credentials,
|
identity: credentials,
|
||||||
token: 'token-contract',
|
|
||||||
baseUrl: 'https://api.example.com',
|
baseUrl: 'https://api.example.com',
|
||||||
});
|
});
|
||||||
expect(instructions.setup).toContain('agent-contract');
|
expect(instructions.setup).toContain('agent-contract');
|
||||||
expect(instructions.setup).toContain('token-contract');
|
expect(instructions.setup).toContain('ELYDORA_PRIVATE_KEY');
|
||||||
|
expect(instructions.setup).toContain('ELYDORA_API_TOKEN');
|
||||||
|
expect(instructions.setup).not.toContain('private-key');
|
||||||
|
expect(instructions.setup).not.toContain('token-contract');
|
||||||
expect(instructions.usage).toContain('ai.tool_use');
|
expect(instructions.usage).toContain('ai.tool_use');
|
||||||
expect(instructions.verify).toBeUndefined();
|
expect(instructions.verify).toBeUndefined();
|
||||||
|
expect(instructions.secretDelivery).toBe('environment');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('quotes shell metacharacters and rejects unsupported command values', () => {
|
||||||
|
const integration = INTEGRATION_CATALOG.find(({ id }) => id === 'grok');
|
||||||
|
expect(integration).toBeDefined();
|
||||||
|
|
||||||
|
const instructions = buildInstallInstructions('go', {
|
||||||
|
integration: integration!,
|
||||||
|
identity: {
|
||||||
|
agentId: 'agent-contract',
|
||||||
|
kid: 'agent-contract-key-1',
|
||||||
|
orgId: '$(Write-Output injected)',
|
||||||
|
},
|
||||||
|
baseUrl: 'https://api.example.com/`whoami`',
|
||||||
|
});
|
||||||
|
expect(instructions.setup).toContain("'$(Write-Output injected)'");
|
||||||
|
expect(instructions.setup).toContain("'https://api.example.com/`whoami`'");
|
||||||
|
|
||||||
|
expect(() => buildInstallInstructions('go', {
|
||||||
|
integration: integration!,
|
||||||
|
identity: { ...credentials, orgId: "org-'unsafe" },
|
||||||
|
baseUrl: 'https://api.example.com',
|
||||||
|
})).toThrow('Organization ID cannot be represented');
|
||||||
|
});
|
||||||
|
|
||||||
test('classifies malformed successful responses as blocking contract failures', () => {
|
test('classifies malformed successful responses as blocking contract failures', () => {
|
||||||
expect(() => assertRegistrationResponse(
|
expect(() => assertRegistrationResponse(
|
||||||
null as never,
|
null as never,
|
||||||
|
|||||||
@@ -146,17 +146,31 @@ test('registers Grok atomically and renders verified adapter commands', async ({
|
|||||||
await page.getByLabel('Token Expiration').selectOption('24hours');
|
await page.getByLabel('Token Expiration').selectOption('24hours');
|
||||||
await page.getByRole('button', { name: 'Issue API Token' }).click();
|
await page.getByRole('button', { name: 'Issue API Token' }).click();
|
||||||
await expect(page.getByText(TOKEN)).toBeVisible();
|
await expect(page.getByText(TOKEN)).toBeVisible();
|
||||||
|
const privateKey = await page.locator('section[aria-labelledby="private-key-label"] code').innerText();
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
const setup = page.locator('pre');
|
const setup = page.locator('pre');
|
||||||
await expect(setup).toContainText('npx @elydora/sdk install --agent grok');
|
await expect(setup).toContainText('npx @elydora/sdk install --agent grok');
|
||||||
await expect(setup).toContainText(`--org_id "${ORG_ID}"`);
|
await expect(setup).toContainText(`--org_id '${ORG_ID}'`);
|
||||||
await expect(setup).toContainText(`--token "${TOKEN}"`);
|
await expect(setup).not.toContainText(TOKEN);
|
||||||
|
await expect(setup).not.toContainText(privateKey);
|
||||||
|
await expect(setup).not.toContainText('--token');
|
||||||
|
await expect(setup).not.toContainText('--private_key');
|
||||||
|
await expect(page.getByText('The CLI requests both values with terminal echo disabled.')).toBeVisible();
|
||||||
|
|
||||||
|
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||||
|
await page.getByRole('button', { name: 'Copy setup command' }).click();
|
||||||
|
await expect(page.getByRole('button', { name: 'Copy setup command' })).toHaveText('Copied');
|
||||||
|
const copiedSetup = await page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
expect(copiedSetup).not.toContain(TOKEN);
|
||||||
|
expect(copiedSetup).not.toContain(privateKey);
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'go' }).click();
|
await page.getByRole('tab', { name: 'go' }).click();
|
||||||
await expect(setup).toContainText('elydora install --agent grok');
|
await expect(setup).toContainText('elydora install --agent grok');
|
||||||
await expect(setup).toContainText(`--org-id "${ORG_ID}"`);
|
await expect(setup).toContainText(`--org-id '${ORG_ID}'`);
|
||||||
await expect(setup).toContainText('--agent-id "agent-');
|
await expect(setup).toContainText("--agent-id 'agent-");
|
||||||
|
await expect(setup).not.toContainText('--private-key');
|
||||||
|
await expect(setup).not.toContainText('--token');
|
||||||
|
|
||||||
await page.screenshot({ path: 'test-results/agent-registration-grok.png', fullPage: true });
|
await page.screenshot({ path: 'test-results/agent-registration-grok.png', fullPage: true });
|
||||||
|
|
||||||
@@ -174,6 +188,90 @@ test('registers Grok atomically and renders verified adapter commands', async ({
|
|||||||
expect(runtimeErrors).toEqual([]);
|
expect(runtimeErrors).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('renders custom SDK setup with runtime secret references', async ({ page }) => {
|
||||||
|
const observed: ObservedRequest[] = [];
|
||||||
|
const unexpected: string[] = [];
|
||||||
|
const runtimeErrors: string[] = [];
|
||||||
|
page.on('pageerror', (error) => runtimeErrors.push(error.message));
|
||||||
|
page.on('console', (message) => {
|
||||||
|
if (message.type() === 'error') runtimeErrors.push(message.text());
|
||||||
|
});
|
||||||
|
await mockBackend(page, observed, unexpected);
|
||||||
|
|
||||||
|
await page.goto('/agents');
|
||||||
|
await page.getByRole('button', { name: 'Register Agent' }).click();
|
||||||
|
await page.getByRole('button', { name: 'SDK', exact: true }).click();
|
||||||
|
await page.getByLabel('Display Name').fill('Production SDK');
|
||||||
|
await page.locator('form').getByRole('button', { name: 'Register Agent' }).click();
|
||||||
|
|
||||||
|
const privateKey = await page.locator('section[aria-labelledby="private-key-label"] code').innerText();
|
||||||
|
await page.getByRole('button', { name: 'Issue API Token' }).click();
|
||||||
|
await expect(page.getByText(TOKEN)).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
const setup = page.locator('section[aria-labelledby="setup-command-label"] pre');
|
||||||
|
await expect(setup).toContainText('process.env.ELYDORA_PRIVATE_KEY');
|
||||||
|
await expect(setup).toContainText('process.env.ELYDORA_API_TOKEN');
|
||||||
|
await expect(setup).not.toContainText(privateKey);
|
||||||
|
await expect(setup).not.toContainText(TOKEN);
|
||||||
|
await expect(page.getByText(
|
||||||
|
'Set ELYDORA_PRIVATE_KEY and ELYDORA_API_TOKEN in the runtime environment.',
|
||||||
|
)).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'python' }).click();
|
||||||
|
await expect(setup).toContainText('os.environ.get(name)');
|
||||||
|
await expect(setup).not.toContainText(privateKey);
|
||||||
|
await expect(setup).not.toContainText(TOKEN);
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'go' }).click();
|
||||||
|
await expect(setup).toContainText('os.LookupEnv(name)');
|
||||||
|
await expect(setup).not.toContainText(privateKey);
|
||||||
|
await expect(setup).not.toContainText(TOKEN);
|
||||||
|
await page.screenshot({ path: 'test-results/agent-registration-sdk.png', fullPage: true });
|
||||||
|
await page.getByRole('dialog').locator(':scope > div').last().evaluate(
|
||||||
|
(content) => content.scrollTo(0, content.scrollHeight),
|
||||||
|
);
|
||||||
|
await page.screenshot({ path: 'test-results/agent-registration-sdk-lower.png', fullPage: true });
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await expect(page.getByRole('button', { name: 'Copy private key' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: 'Done' })).toBeVisible();
|
||||||
|
const pageOverflows = await page.evaluate(
|
||||||
|
() => document.documentElement.scrollWidth > window.innerWidth,
|
||||||
|
);
|
||||||
|
expect(pageOverflows).toBe(false);
|
||||||
|
await page.screenshot({ path: 'test-results/agent-registration-sdk-mobile.png', fullPage: true });
|
||||||
|
|
||||||
|
const registrationRequests = observed.filter(({ path }) => path === '/v1/agents/register');
|
||||||
|
expect(registrationRequests).toHaveLength(1);
|
||||||
|
expect(registrationRequests[0]?.body).toMatchObject({ integration_type: 'sdk' });
|
||||||
|
expect(unexpected).toEqual([]);
|
||||||
|
expect(runtimeErrors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loads the Chinese registration resources', async ({ page }) => {
|
||||||
|
const observed: ObservedRequest[] = [];
|
||||||
|
const unexpected: string[] = [];
|
||||||
|
const runtimeErrors: string[] = [];
|
||||||
|
page.on('pageerror', (error) => runtimeErrors.push(error.message));
|
||||||
|
page.on('console', (message) => {
|
||||||
|
if (message.type() === 'error') runtimeErrors.push(message.text());
|
||||||
|
});
|
||||||
|
await page.addInitScript(() => localStorage.setItem('i18nextLng', 'zh'));
|
||||||
|
await mockBackend(page, observed, unexpected);
|
||||||
|
|
||||||
|
await page.goto('/agents');
|
||||||
|
await expect(page.locator('html')).toHaveAttribute('lang', 'zh');
|
||||||
|
await page.getByRole('button', { name: '注册代理' }).click();
|
||||||
|
await expect(page.getByText('CLI 适配器')).toBeVisible();
|
||||||
|
await expect(page.getByText('自定义 SDK')).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: 'OpenAI Codex' })).toBeVisible();
|
||||||
|
|
||||||
|
expect(observed.filter(({ path }) => path === '/v1/agents')).toHaveLength(1);
|
||||||
|
expect(unexpected).toEqual([]);
|
||||||
|
expect(runtimeErrors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test('blocks the success state when the backend returns a different integration', async ({ page }) => {
|
test('blocks the success state when the backend returns a different integration', async ({ page }) => {
|
||||||
const observed: ObservedRequest[] = [];
|
const observed: ObservedRequest[] = [];
|
||||||
const unexpected: string[] = [];
|
const unexpected: string[] = [];
|
||||||
|
|||||||
@@ -30,8 +30,16 @@ export default function ConnectStep({
|
|||||||
const [language, setLanguage] = useState<SdkLanguage>('node');
|
const [language, setLanguage] = useState<SdkLanguage>('node');
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:8787';
|
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:8787';
|
||||||
const instructions = useMemo(
|
const instructions = useMemo(
|
||||||
() => buildInstallInstructions(language, { integration, credentials, token, baseUrl }),
|
() => buildInstallInstructions(language, {
|
||||||
[baseUrl, credentials, integration, language, token],
|
integration,
|
||||||
|
identity: {
|
||||||
|
agentId: credentials.agentId,
|
||||||
|
kid: credentials.kid,
|
||||||
|
orgId: credentials.orgId,
|
||||||
|
},
|
||||||
|
baseUrl,
|
||||||
|
}),
|
||||||
|
[baseUrl, credentials.agentId, credentials.kid, credentials.orgId, integration, language],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -63,18 +71,50 @@ export default function ConnectStep({
|
|||||||
<h3 id="setup-command-label" className="section-label">
|
<h3 id="setup-command-label" className="section-label">
|
||||||
{integration.mode === 'adapter' ? t('agentRegistration.install') : t('agentRegistration.configure')}
|
{integration.mode === 'adapter' ? t('agentRegistration.install') : t('agentRegistration.configure')}
|
||||||
</h3>
|
</h3>
|
||||||
<CopyControl value={instructions.setup} />
|
<CopyControl
|
||||||
|
value={instructions.setup}
|
||||||
|
ariaLabel={t('agentRegistration.copySetup')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<pre className="p-4 bg-ink text-[#EAEAE5] font-mono text-[10px] leading-relaxed whitespace-pre-wrap break-all overflow-x-auto">
|
<pre className="p-4 bg-ink text-[#EAEAE5] font-mono text-[10px] leading-relaxed whitespace-pre-wrap break-all overflow-x-auto">
|
||||||
{instructions.setup}
|
{instructions.setup}
|
||||||
</pre>
|
</pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="border-y border-border"
|
||||||
|
role="group"
|
||||||
|
aria-label={t('agentRegistration.steps.credentials')}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between py-2 border-b border-border">
|
||||||
|
<span className="section-label">{t('agentRegistration.privateKey')}</span>
|
||||||
|
<CopyControl
|
||||||
|
value={credentials.privateKey}
|
||||||
|
ariaLabel={t('agentRegistration.copyPrivateKey')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<span className="section-label">{t('agentRegistration.apiToken')}</span>
|
||||||
|
<CopyControl
|
||||||
|
value={token}
|
||||||
|
ariaLabel={t('agentRegistration.copyApiToken')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 font-mono text-[10px] text-ink-dim">
|
||||||
|
{t(`agentRegistration.secretDelivery.${instructions.secretDelivery}`)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{instructions.usage && (
|
{instructions.usage && (
|
||||||
<section aria-labelledby="record-operation-label">
|
<section aria-labelledby="record-operation-label">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h3 id="record-operation-label" className="section-label">{t('agentRegistration.record')}</h3>
|
<h3 id="record-operation-label" className="section-label">{t('agentRegistration.record')}</h3>
|
||||||
<CopyControl value={instructions.usage} />
|
<CopyControl
|
||||||
|
value={instructions.usage}
|
||||||
|
ariaLabel={t('agentRegistration.copyUsage')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<pre className="p-4 bg-ink text-[#EAEAE5] font-mono text-[10px] leading-relaxed whitespace-pre-wrap break-all overflow-x-auto">
|
<pre className="p-4 bg-ink text-[#EAEAE5] font-mono text-[10px] leading-relaxed whitespace-pre-wrap break-all overflow-x-auto">
|
||||||
{instructions.usage}
|
{instructions.usage}
|
||||||
@@ -86,7 +126,10 @@ export default function ConnectStep({
|
|||||||
<section aria-labelledby="verify-command-label">
|
<section aria-labelledby="verify-command-label">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h3 id="verify-command-label" className="section-label">{t('agentRegistration.verify')}</h3>
|
<h3 id="verify-command-label" className="section-label">{t('agentRegistration.verify')}</h3>
|
||||||
<CopyControl value={instructions.verify} />
|
<CopyControl
|
||||||
|
value={instructions.verify}
|
||||||
|
ariaLabel={t('agentRegistration.copyVerify')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<code className="block px-3 py-2 border-y border-border font-mono text-[11px] text-ink">
|
<code className="block px-3 py-2 border-y border-border font-mono text-[11px] text-ink">
|
||||||
{instructions.verify}
|
{instructions.verify}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import { useTranslation } from 'react-i18next';
|
|||||||
interface CopyControlProps {
|
interface CopyControlProps {
|
||||||
readonly value: string;
|
readonly value: string;
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
|
readonly ariaLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CopyControl({ value, className = '' }: CopyControlProps) {
|
export default function CopyControl({ value, className = '', ariaLabel }: CopyControlProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [status, setStatus] = useState<'idle' | 'copied' | 'failed'>('idle');
|
const [status, setStatus] = useState<'idle' | 'copied' | 'failed'>('idle');
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ export default function CopyControl({ value, className = '' }: CopyControlProps)
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={copy}
|
onClick={copy}
|
||||||
|
aria-label={ariaLabel}
|
||||||
className="font-mono text-[10px] uppercase tracking-wider text-ink-dim hover:text-ink"
|
className="font-mono text-[10px] uppercase tracking-wider text-ink-dim hover:text-ink"
|
||||||
>
|
>
|
||||||
{status === 'copied' ? t('common.copied') : t('common.copy')}
|
{status === 'copied' ? t('common.copied') : t('common.copy')}
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ export default function CredentialsStep({
|
|||||||
<h3 id="private-key-label" className="section-label text-red-700">
|
<h3 id="private-key-label" className="section-label text-red-700">
|
||||||
{t('agentRegistration.privateKeySaveNow')}
|
{t('agentRegistration.privateKeySaveNow')}
|
||||||
</h3>
|
</h3>
|
||||||
<CopyControl value={credentials.privateKey} />
|
<CopyControl
|
||||||
|
value={credentials.privateKey}
|
||||||
|
ariaLabel={t('agentRegistration.copyPrivateKey')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<code className="block p-3 bg-ink text-[#EAEAE5] font-mono text-[10px] break-all select-all">
|
<code className="block p-3 bg-ink text-[#EAEAE5] font-mono text-[10px] break-all select-all">
|
||||||
{credentials.privateKey}
|
{credentials.privateKey}
|
||||||
@@ -104,7 +107,10 @@ export default function CredentialsStep({
|
|||||||
<span className="font-mono text-[10px] text-ink-dim">
|
<span className="font-mono text-[10px] text-ink-dim">
|
||||||
{t('agentRegistration.tokenIssued')}
|
{t('agentRegistration.tokenIssued')}
|
||||||
</span>
|
</span>
|
||||||
<CopyControl value={token} />
|
<CopyControl
|
||||||
|
value={token}
|
||||||
|
ariaLabel={t('agentRegistration.copyApiToken')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<code className="block p-3 bg-ink text-[#EAEAE5] font-mono text-[10px] break-all select-all">
|
<code className="block p-3 bg-ink text-[#EAEAE5] font-mono text-[10px] break-all select-all">
|
||||||
{token}
|
{token}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import type { IntegrationCatalogItem } from './integrations';
|
import type { IntegrationCatalogItem } from './integrations';
|
||||||
import type { AgentCredentials } from './credentials';
|
|
||||||
|
|
||||||
export const SDK_LANGUAGES = ['node', 'python', 'go'] as const;
|
export const SDK_LANGUAGES = ['node', 'python', 'go'] as const;
|
||||||
export type SdkLanguage = (typeof SDK_LANGUAGES)[number];
|
export type SdkLanguage = (typeof SDK_LANGUAGES)[number];
|
||||||
|
type SecretDelivery = 'hidden-prompts' | 'environment';
|
||||||
|
|
||||||
|
interface AgentInstallIdentity {
|
||||||
|
readonly agentId: string;
|
||||||
|
readonly kid: string;
|
||||||
|
readonly orgId: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface InstructionInput {
|
interface InstructionInput {
|
||||||
readonly integration: IntegrationCatalogItem;
|
readonly integration: IntegrationCatalogItem;
|
||||||
readonly credentials: AgentCredentials;
|
readonly identity: AgentInstallIdentity;
|
||||||
readonly token: string;
|
|
||||||
readonly baseUrl: string;
|
readonly baseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,37 +21,39 @@ export interface InstallInstructions {
|
|||||||
readonly usage?: string;
|
readonly usage?: string;
|
||||||
readonly verify?: string;
|
readonly verify?: string;
|
||||||
readonly postInstall?: readonly string[];
|
readonly postInstall?: readonly string[];
|
||||||
|
readonly secretDelivery: SecretDelivery;
|
||||||
}
|
}
|
||||||
|
|
||||||
function quoted(value: string): string {
|
function shellQuoted(value: string, label: string): string {
|
||||||
return `"${value.replaceAll('"', '\\"')}"`;
|
if (!value || /['\u0000-\u001f\u007f]/u.test(value)) {
|
||||||
|
throw new Error(`${label} cannot be represented in the generated shell command.`);
|
||||||
|
}
|
||||||
|
return `'${value}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function adapterCommand(
|
function adapterCommand(
|
||||||
language: SdkLanguage,
|
language: SdkLanguage,
|
||||||
{ integration, credentials, token, baseUrl }: InstructionInput,
|
{ integration, identity, baseUrl }: InstructionInput,
|
||||||
): string {
|
): string {
|
||||||
const values = {
|
const values = {
|
||||||
agent: integration.id,
|
agent: integration.id,
|
||||||
orgId: quoted(credentials.orgId),
|
orgId: shellQuoted(identity.orgId, 'Organization ID'),
|
||||||
agentId: quoted(credentials.agentId),
|
agentId: shellQuoted(identity.agentId, 'Agent ID'),
|
||||||
privateKey: quoted(credentials.privateKey),
|
kid: shellQuoted(identity.kid, 'Key ID'),
|
||||||
kid: quoted(credentials.kid),
|
baseUrl: shellQuoted(baseUrl, 'API base URL'),
|
||||||
token: quoted(token),
|
|
||||||
baseUrl: quoted(baseUrl),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (language === 'go') {
|
if (language === 'go') {
|
||||||
return [
|
return [
|
||||||
'go install github.com/Elydora-Infrastructure/Elydora-Go-SDK/cmd/elydora@latest',
|
'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}`,
|
`elydora install --agent ${values.agent} --org-id ${values.orgId} --agent-id ${values.agentId} --kid ${values.kid} --base-url ${values.baseUrl}`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
const executable = language === 'node'
|
const executable = language === 'node'
|
||||||
? 'npx @elydora/sdk install'
|
? 'npx @elydora/sdk install'
|
||||||
: 'python -m pip install elydora\nelydora 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}`;
|
return `${executable} --agent ${values.agent} --org_id ${values.orgId} --agent_id ${values.agentId} --kid ${values.kid} --base_url ${values.baseUrl}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function postInstallSteps(integration: IntegrationCatalogItem): readonly string[] | undefined {
|
function postInstallSteps(integration: IntegrationCatalogItem): readonly string[] | undefined {
|
||||||
@@ -63,31 +70,47 @@ function postInstallSteps(integration: IntegrationCatalogItem): readonly string[
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sdkSetup(language: SdkLanguage, input: InstructionInput): string {
|
function sdkSetup(language: SdkLanguage, input: InstructionInput): string {
|
||||||
const { credentials, token, baseUrl } = input;
|
const { identity, baseUrl } = input;
|
||||||
if (language === 'python') {
|
if (language === 'python') {
|
||||||
return `python -m pip install elydora
|
return `python -m pip install elydora
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from elydora import ElydoraClient
|
from elydora import ElydoraClient
|
||||||
|
|
||||||
|
def require_env(name: str) -> str:
|
||||||
|
value = os.environ.get(name)
|
||||||
|
if not value:
|
||||||
|
raise RuntimeError(f"{name} is required")
|
||||||
|
return value
|
||||||
|
|
||||||
client = ElydoraClient(
|
client = ElydoraClient(
|
||||||
org_id=${JSON.stringify(credentials.orgId)},
|
org_id=${JSON.stringify(identity.orgId)},
|
||||||
agent_id=${JSON.stringify(credentials.agentId)},
|
agent_id=${JSON.stringify(identity.agentId)},
|
||||||
private_key=${JSON.stringify(credentials.privateKey)},
|
private_key=require_env("ELYDORA_PRIVATE_KEY"),
|
||||||
base_url=${JSON.stringify(baseUrl)},
|
base_url=${JSON.stringify(baseUrl)},
|
||||||
token=${JSON.stringify(token)},
|
token=require_env("ELYDORA_API_TOKEN"),
|
||||||
)
|
)
|
||||||
client.set_kid(${JSON.stringify(credentials.kid)})`;
|
client.set_kid(${JSON.stringify(identity.kid)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (language === 'go') {
|
if (language === 'go') {
|
||||||
return `go get github.com/Elydora-Infrastructure/Elydora-Go-SDK
|
return `go get github.com/Elydora-Infrastructure/Elydora-Go-SDK
|
||||||
|
|
||||||
|
requireEnv := func(name string) string {
|
||||||
|
value, ok := os.LookupEnv(name)
|
||||||
|
if !ok || value == "" {
|
||||||
|
panic(name + " is required")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
client, err := elydora.NewClient(&elydora.Config{
|
client, err := elydora.NewClient(&elydora.Config{
|
||||||
OrgID: ${JSON.stringify(credentials.orgId)},
|
OrgID: ${JSON.stringify(identity.orgId)},
|
||||||
AgentID: ${JSON.stringify(credentials.agentId)},
|
AgentID: ${JSON.stringify(identity.agentId)},
|
||||||
PrivateKey: ${JSON.stringify(credentials.privateKey)},
|
PrivateKey: requireEnv("ELYDORA_PRIVATE_KEY"),
|
||||||
BaseURL: ${JSON.stringify(baseUrl)},
|
BaseURL: ${JSON.stringify(baseUrl)},
|
||||||
Token: ${JSON.stringify(token)},
|
Token: requireEnv("ELYDORA_API_TOKEN"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -98,14 +121,20 @@ if err != nil {
|
|||||||
|
|
||||||
import { ElydoraClient } from '@elydora/sdk';
|
import { ElydoraClient } from '@elydora/sdk';
|
||||||
|
|
||||||
|
const privateKey = process.env.ELYDORA_PRIVATE_KEY;
|
||||||
|
const token = process.env.ELYDORA_API_TOKEN;
|
||||||
|
if (!privateKey || !token) {
|
||||||
|
throw new Error('ELYDORA_PRIVATE_KEY and ELYDORA_API_TOKEN are required');
|
||||||
|
}
|
||||||
|
|
||||||
const client = new ElydoraClient({
|
const client = new ElydoraClient({
|
||||||
orgId: ${JSON.stringify(credentials.orgId)},
|
orgId: ${JSON.stringify(identity.orgId)},
|
||||||
agentId: ${JSON.stringify(credentials.agentId)},
|
agentId: ${JSON.stringify(identity.agentId)},
|
||||||
privateKey: ${JSON.stringify(credentials.privateKey)},
|
privateKey,
|
||||||
kid: ${JSON.stringify(credentials.kid)},
|
kid: ${JSON.stringify(identity.kid)},
|
||||||
baseUrl: ${JSON.stringify(baseUrl)},
|
baseUrl: ${JSON.stringify(baseUrl)},
|
||||||
});
|
});
|
||||||
client.setToken(${JSON.stringify(token)});`;
|
client.setToken(token);`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sdkUsage(language: SdkLanguage): string {
|
function sdkUsage(language: SdkLanguage): string {
|
||||||
@@ -153,11 +182,13 @@ export function buildInstallInstructions(
|
|||||||
setup: adapterCommand(language, input),
|
setup: adapterCommand(language, input),
|
||||||
verify: language === 'node' ? 'npx @elydora/sdk status' : 'elydora status',
|
verify: language === 'node' ? 'npx @elydora/sdk status' : 'elydora status',
|
||||||
postInstall: postInstallSteps(input.integration),
|
postInstall: postInstallSteps(input.integration),
|
||||||
|
secretDelivery: 'hidden-prompts',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setup: sdkSetup(language, input),
|
setup: sdkSetup(language, input),
|
||||||
usage: sdkUsage(language),
|
usage: sdkUsage(language),
|
||||||
|
secretDelivery: 'environment',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -307,6 +307,7 @@
|
|||||||
"submitting": "Registering...",
|
"submitting": "Registering...",
|
||||||
"submit": "Register Agent",
|
"submit": "Register Agent",
|
||||||
"failedToRegister": "Failed to register agent.",
|
"failedToRegister": "Failed to register agent.",
|
||||||
|
"privateKey": "Private Key",
|
||||||
"privateKeySaveNow": "Private Key — Save This Now",
|
"privateKeySaveNow": "Private Key — Save This Now",
|
||||||
"privateKeyWarning": "This will not be shown again. Store it as an environment variable or in a secrets manager.",
|
"privateKeyWarning": "This will not be shown again. Store it as an environment variable or in a secrets manager.",
|
||||||
"apiToken": "API Token",
|
"apiToken": "API Token",
|
||||||
@@ -333,6 +334,11 @@
|
|||||||
"sdk": "Custom SDK"
|
"sdk": "Custom SDK"
|
||||||
},
|
},
|
||||||
"integration": "Integration",
|
"integration": "Integration",
|
||||||
|
"copySetup": "Copy setup command",
|
||||||
|
"copyUsage": "Copy operation example",
|
||||||
|
"copyVerify": "Copy verification command",
|
||||||
|
"copyPrivateKey": "Copy private key",
|
||||||
|
"copyApiToken": "Copy API token",
|
||||||
"copyFailed": "Copy failed.",
|
"copyFailed": "Copy failed.",
|
||||||
"invalidExpiration": "Select a valid expiration.",
|
"invalidExpiration": "Select a valid expiration.",
|
||||||
"tokenIssued": "Issued once. Store it securely.",
|
"tokenIssued": "Issued once. Store it securely.",
|
||||||
@@ -340,7 +346,11 @@
|
|||||||
"record": "Record an operation",
|
"record": "Record an operation",
|
||||||
"verify": "Verify",
|
"verify": "Verify",
|
||||||
"postInstall": "Post-install steps",
|
"postInstall": "Post-install steps",
|
||||||
"sdkLanguage": "SDK language"
|
"sdkLanguage": "SDK language",
|
||||||
|
"secretDelivery": {
|
||||||
|
"hidden-prompts": "The CLI requests both values with terminal echo disabled.",
|
||||||
|
"environment": "Set ELYDORA_PRIVATE_KEY and ELYDORA_API_TOKEN in the runtime environment."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"chain": {
|
"chain": {
|
||||||
"title": "Chain Visualization",
|
"title": "Chain Visualization",
|
||||||
|
|||||||
@@ -307,6 +307,7 @@
|
|||||||
"submitting": "注册中...",
|
"submitting": "注册中...",
|
||||||
"submit": "注册代理",
|
"submit": "注册代理",
|
||||||
"failedToRegister": "注册代理失败。",
|
"failedToRegister": "注册代理失败。",
|
||||||
|
"privateKey": "私钥",
|
||||||
"privateKeySaveNow": "私钥 — 请立即保存",
|
"privateKeySaveNow": "私钥 — 请立即保存",
|
||||||
"privateKeyWarning": "此私钥不会再次显示。请将其存储为环境变量或保存在密钥管理器中。",
|
"privateKeyWarning": "此私钥不会再次显示。请将其存储为环境变量或保存在密钥管理器中。",
|
||||||
"apiToken": "API 令牌",
|
"apiToken": "API 令牌",
|
||||||
@@ -333,6 +334,11 @@
|
|||||||
"sdk": "自定义 SDK"
|
"sdk": "自定义 SDK"
|
||||||
},
|
},
|
||||||
"integration": "集成",
|
"integration": "集成",
|
||||||
|
"copySetup": "复制安装命令",
|
||||||
|
"copyUsage": "复制操作示例",
|
||||||
|
"copyVerify": "复制验证命令",
|
||||||
|
"copyPrivateKey": "复制私钥",
|
||||||
|
"copyApiToken": "复制 API 令牌",
|
||||||
"copyFailed": "复制失败。",
|
"copyFailed": "复制失败。",
|
||||||
"invalidExpiration": "请选择有效期限。",
|
"invalidExpiration": "请选择有效期限。",
|
||||||
"tokenIssued": "仅显示一次,请安全保存。",
|
"tokenIssued": "仅显示一次,请安全保存。",
|
||||||
@@ -340,7 +346,11 @@
|
|||||||
"record": "记录操作",
|
"record": "记录操作",
|
||||||
"verify": "验证",
|
"verify": "验证",
|
||||||
"postInstall": "安装后步骤",
|
"postInstall": "安装后步骤",
|
||||||
"sdkLanguage": "SDK 语言"
|
"sdkLanguage": "SDK 语言",
|
||||||
|
"secretDelivery": {
|
||||||
|
"hidden-prompts": "CLI 会在关闭终端回显后请求这两项凭据。",
|
||||||
|
"environment": "请在运行环境中设置 ELYDORA_PRIVATE_KEY 与 ELYDORA_API_TOKEN。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"chain": {
|
"chain": {
|
||||||
"title": "链可视化",
|
"title": "链可视化",
|
||||||
|
|||||||
Reference in new issue
Block a user