diff --git a/AGENTS.md b/AGENTS.md index b8547af..da1a044 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,6 +56,7 @@ This repository publishes the open-source server, console, integration catalog, - Avoid compatibility shims without a named public contract. - 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 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. - 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. diff --git a/packages/console/e2e/agent-registration-contract.spec.ts b/packages/console/e2e/agent-registration-contract.spec.ts index 31c5925..5d28036 100644 --- a/packages/console/e2e/agent-registration-contract.spec.ts +++ b/packages/console/e2e/agent-registration-contract.spec.ts @@ -52,26 +52,26 @@ test('generates every adapter command with the verified SDK flag contract', () = for (const language of SDK_LANGUAGES) { const instructions = buildInstallInstructions(language, { integration, - credentials, - token: 'token-contract', + identity: credentials, baseUrl: 'https://api.example.com', }); expect(instructions.setup).toContain(`--agent ${integration.id}`); - expect(instructions.setup).toContain('"private-key"'); - expect(instructions.setup).toContain('"token-contract"'); - expect(instructions.setup).toContain('"https://api.example.com"'); + expect(instructions.setup).toContain("'https://api.example.com'"); + expect(instructions.setup).not.toContain('private-key'); + 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') { - expect(instructions.setup).toContain('--org-id "org-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("--org-id 'org-contract'"); + expect(instructions.setup).toContain("--agent-id 'agent-contract'"); + expect(instructions.setup).toContain("--base-url 'https://api.example.com'"); } else { - expect(instructions.setup).toContain('--org_id "org-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("--org_id 'org-contract'"); + expect(instructions.setup).toContain("--agent_id 'agent-contract'"); + 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) { const instructions = buildInstallInstructions(language, { integration: integration!, - credentials, - token: 'token-contract', + identity: credentials, baseUrl: 'https://api.example.com', }); 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.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', () => { expect(() => assertRegistrationResponse( null as never, diff --git a/packages/console/e2e/agent-registration.spec.ts b/packages/console/e2e/agent-registration.spec.ts index 8f14d5f..f6837de 100644 --- a/packages/console/e2e/agent-registration.spec.ts +++ b/packages/console/e2e/agent-registration.spec.ts @@ -146,17 +146,31 @@ test('registers Grok atomically and renders verified adapter commands', async ({ await page.getByLabel('Token Expiration').selectOption('24hours'); await page.getByRole('button', { name: 'Issue API Token' }).click(); 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(); const setup = page.locator('pre'); await expect(setup).toContainText('npx @elydora/sdk install --agent grok'); - await expect(setup).toContainText(`--org_id "${ORG_ID}"`); - await expect(setup).toContainText(`--token "${TOKEN}"`); + await expect(setup).toContainText(`--org_id '${ORG_ID}'`); + 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 expect(setup).toContainText('elydora install --agent grok'); - await expect(setup).toContainText(`--org-id "${ORG_ID}"`); - await expect(setup).toContainText('--agent-id "agent-'); + await expect(setup).toContainText(`--org-id '${ORG_ID}'`); + 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 }); @@ -174,6 +188,90 @@ test('registers Grok atomically and renders verified adapter commands', async ({ 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 }) => { const observed: ObservedRequest[] = []; const unexpected: string[] = []; diff --git a/packages/console/src/components/agent-registration/ConnectStep.tsx b/packages/console/src/components/agent-registration/ConnectStep.tsx index 9ccb00d..62c8588 100644 --- a/packages/console/src/components/agent-registration/ConnectStep.tsx +++ b/packages/console/src/components/agent-registration/ConnectStep.tsx @@ -30,8 +30,16 @@ export default function ConnectStep({ const [language, setLanguage] = useState('node'); const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:8787'; const instructions = useMemo( - () => buildInstallInstructions(language, { integration, credentials, token, baseUrl }), - [baseUrl, credentials, integration, language, token], + () => buildInstallInstructions(language, { + integration, + identity: { + agentId: credentials.agentId, + kid: credentials.kid, + orgId: credentials.orgId, + }, + baseUrl, + }), + [baseUrl, credentials.agentId, credentials.kid, credentials.orgId, integration, language], ); return ( @@ -63,18 +71,50 @@ export default function ConnectStep({

{integration.mode === 'adapter' ? t('agentRegistration.install') : t('agentRegistration.configure')}

- +
           {instructions.setup}
         
+
+
+
+ {t('agentRegistration.privateKey')} + +
+
+ {t('agentRegistration.apiToken')} + +
+
+

+ {t(`agentRegistration.secretDelivery.${instructions.secretDelivery}`)} +

+
+ {instructions.usage && (

{t('agentRegistration.record')}

- +
             {instructions.usage}
@@ -86,7 +126,10 @@ export default function ConnectStep({
         

{t('agentRegistration.verify')}

- +
{instructions.verify} diff --git a/packages/console/src/components/agent-registration/CopyControl.tsx b/packages/console/src/components/agent-registration/CopyControl.tsx index 3aac52d..ed72934 100644 --- a/packages/console/src/components/agent-registration/CopyControl.tsx +++ b/packages/console/src/components/agent-registration/CopyControl.tsx @@ -6,9 +6,10 @@ import { useTranslation } from 'react-i18next'; interface CopyControlProps { readonly value: 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 [status, setStatus] = useState<'idle' | 'copied' | 'failed'>('idle'); @@ -26,6 +27,7 @@ export default function CopyControl({ value, className = '' }: CopyControlProps)