fix: enforce atomic agent registration contract

This commit is contained in:
ZacharyZhang-NY committed 2026-07-19 15:38:40 -04:00
1 parent 47d323c17b
commit f64d00673c
58 files changed
+2692 -1667

No files matched your search

@@ -0,0 +1,114 @@
'use client';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { AgentCredentials } from '@/features/agent-registration/credentials';
import type { IntegrationCatalogItem } from '@/features/agent-registration/integrations';
import {
buildInstallInstructions,
SDK_LANGUAGES,
type SdkLanguage,
} from '@/features/agent-registration/install-instructions';
import CopyControl from './CopyControl';
interface ConnectStepProps {
readonly integration: IntegrationCatalogItem;
readonly credentials: AgentCredentials;
readonly token: string;
readonly onBack: () => void;
readonly onDone: () => void;
}
export default function ConnectStep({
integration,
credentials,
token,
onBack,
onDone,
}: ConnectStepProps) {
const { t } = useTranslation();
const [language, setLanguage] = useState<SdkLanguage>('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],
);
return (
<div className="space-y-5">
<div className="flex items-center justify-between border-b border-border pb-3">
<span className="section-label">{t('agentRegistration.integration')}</span>
<span className="font-mono text-[12px] text-ink">{integration.name}</span>
</div>
<div className="flex border-b border-border" role="tablist" aria-label={t('agentRegistration.sdkLanguage')}>
{SDK_LANGUAGES.map((item) => (
<button
key={item}
type="button"
role="tab"
aria-selected={language === item}
onClick={() => setLanguage(item)}
className={`px-4 py-2 font-mono text-[10px] uppercase tracking-wider border-b-2 -mb-px ${
language === item ? 'border-ink text-ink' : 'border-transparent text-ink-dim hover:text-ink'
}`}
>
{item}
</button>
))}
</div>
<section aria-labelledby="setup-command-label">
<div className="flex items-center justify-between mb-2">
<h3 id="setup-command-label" className="section-label">
{integration.mode === 'adapter' ? t('agentRegistration.install') : t('agentRegistration.configure')}
</h3>
<CopyControl value={instructions.setup} />
</div>
<pre className="p-4 bg-ink text-[#EAEAE5] font-mono text-[10px] leading-relaxed whitespace-pre-wrap break-all overflow-x-auto">
{instructions.setup}
</pre>
</section>
{instructions.usage && (
<section aria-labelledby="record-operation-label">
<div className="flex items-center justify-between mb-2">
<h3 id="record-operation-label" className="section-label">{t('agentRegistration.record')}</h3>
<CopyControl value={instructions.usage} />
</div>
<pre className="p-4 bg-ink text-[#EAEAE5] font-mono text-[10px] leading-relaxed whitespace-pre-wrap break-all overflow-x-auto">
{instructions.usage}
</pre>
</section>
)}
{instructions.verify && (
<section aria-labelledby="verify-command-label">
<div className="flex items-center justify-between mb-2">
<h3 id="verify-command-label" className="section-label">{t('agentRegistration.verify')}</h3>
<CopyControl value={instructions.verify} />
</div>
<code className="block px-3 py-2 border-y border-border font-mono text-[11px] text-ink">
{instructions.verify}
</code>
</section>
)}
{instructions.postInstall && (
<ol className="border-t border-border" aria-label={t('agentRegistration.postInstall')}>
{instructions.postInstall.map((step, index) => (
<li key={step} className="grid grid-cols-[2rem_1fr] gap-2 py-2 border-b border-border font-mono text-[11px] text-ink">
<span className="text-ink-dim tabular-nums">{index + 1}</span>
<span>{step}</span>
</li>
))}
</ol>
)}
<div className="flex items-center justify-between pt-2">
<button type="button" className="btn-ghost" onClick={onBack}>{t('common.back')}</button>
<button type="button" className="btn-brutalist" onClick={onDone}>{t('agentRegistration.done')}</button>
</div>
</div>
);
}
@@ -0,0 +1,40 @@
'use client';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
interface CopyControlProps {
readonly value: string;
readonly className?: string;
}
export default function CopyControl({ value, className = '' }: CopyControlProps) {
const { t } = useTranslation();
const [status, setStatus] = useState<'idle' | 'copied' | 'failed'>('idle');
const copy = async () => {
try {
await navigator.clipboard.writeText(value);
setStatus('copied');
} catch {
setStatus('failed');
}
};
return (
<span className={`inline-flex items-center gap-2 ${className}`}>
<button
type="button"
onClick={copy}
className="font-mono text-[10px] uppercase tracking-wider text-ink-dim hover:text-ink"
>
{status === 'copied' ? t('common.copied') : t('common.copy')}
</button>
{status === 'failed' && (
<span className="font-mono text-[10px] text-red-600" role="alert">
{t('agentRegistration.copyFailed')}
</span>
)}
</span>
);
}
@@ -0,0 +1,168 @@
'use client';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { AgentCredentials } from '@/features/agent-registration/credentials';
import CopyControl from './CopyControl';
const EXPIRATION_OPTIONS = [
{ id: '24hours', seconds: 86_400 },
{ id: '7days', seconds: 604_800 },
{ id: '1month', seconds: 2_592_000 },
{ id: '1year', seconds: 31_536_000 },
{ id: 'custom', seconds: -1 },
{ id: 'neverExpire', seconds: null },
] as const;
const MAX_CUSTOM_DAYS = 365;
type ExpirationId = (typeof EXPIRATION_OPTIONS)[number]['id'];
interface CredentialsStepProps {
readonly credentials: AgentCredentials;
readonly token?: string;
readonly issuingToken: boolean;
readonly tokenError: string | null;
readonly onIssueToken: (ttlSeconds: number | null) => void;
readonly onContinue: () => void;
}
function CredentialRow({ label, value }: { readonly label: string; readonly value: string }) {
return (
<div className="grid grid-cols-[7rem_minmax(0,1fr)_auto] items-center gap-3 py-2 border-b border-border last:border-b-0">
<dt className="section-label">{label}</dt>
<dd className="font-mono text-[11px] text-ink truncate" title={value}>{value}</dd>
<dd><CopyControl value={value} /></dd>
</div>
);
}
export default function CredentialsStep({
credentials,
token,
issuingToken,
tokenError,
onIssueToken,
onContinue,
}: CredentialsStepProps) {
const { t } = useTranslation();
const [expiration, setExpiration] = useState<ExpirationId>('24hours');
const [customDays, setCustomDays] = useState('');
const [validationError, setValidationError] = useState<string | null>(null);
const issueToken = () => {
const option = EXPIRATION_OPTIONS.find(({ id }) => id === expiration);
if (!option) {
setValidationError(t('agentRegistration.invalidExpiration'));
return;
}
if (option.seconds === -1) {
const days = Number.parseInt(customDays, 10);
if (!Number.isSafeInteger(days) || days < 1 || days > MAX_CUSTOM_DAYS) {
setValidationError(t('agentRegistration.enterValidDays'));
return;
}
setValidationError(null);
onIssueToken(days * 86_400);
return;
}
setValidationError(null);
onIssueToken(option.seconds);
};
return (
<div className="space-y-6">
<dl>
<CredentialRow label={t('agentRegistration.credAgentId')} value={credentials.agentId} />
<CredentialRow label={t('agentRegistration.credKeyId')} value={credentials.kid} />
<CredentialRow label={t('agentRegistration.credOrgId')} value={credentials.orgId} />
</dl>
<section aria-labelledby="private-key-label">
<div className="flex items-center justify-between mb-2">
<h3 id="private-key-label" className="section-label text-red-700">
{t('agentRegistration.privateKeySaveNow')}
</h3>
<CopyControl value={credentials.privateKey} />
</div>
<code className="block p-3 bg-ink text-[#EAEAE5] font-mono text-[10px] break-all select-all">
{credentials.privateKey}
</code>
<p className="font-mono text-[10px] text-ink-dim mt-2">
{t('agentRegistration.privateKeyWarning')}
</p>
</section>
<section aria-labelledby="api-token-label" className="border-t border-border pt-5">
<h3 id="api-token-label" className="section-label mb-3">
{t('agentRegistration.apiToken')}
</h3>
{token ? (
<div>
<div className="flex items-center justify-between mb-2">
<span className="font-mono text-[10px] text-ink-dim">
{t('agentRegistration.tokenIssued')}
</span>
<CopyControl value={token} />
</div>
<code className="block p-3 bg-ink text-[#EAEAE5] font-mono text-[10px] break-all select-all">
{token}
</code>
</div>
) : (
<div className="flex flex-col sm:flex-row sm:items-end gap-3">
<div className="flex-1">
<label htmlFor="token-expiration" className="section-label block mb-1.5">
{t('agentRegistration.tokenExpiration')}
</label>
<select
id="token-expiration"
value={expiration}
onChange={(event) => setExpiration(event.target.value as ExpirationId)}
className="w-full px-3 py-2 bg-transparent border border-border font-mono text-[12px] text-ink focus:outline-none focus:border-ink"
disabled={issuingToken}
>
{EXPIRATION_OPTIONS.map(({ id }) => (
<option key={id} value={id}>{t(`tokenExpiration.${id}`)}</option>
))}
</select>
</div>
{expiration === 'custom' && (
<div className="sm:w-28">
<label htmlFor="custom-token-days" className="section-label block mb-1.5">
{t('agentRegistration.days')}
</label>
<input
id="custom-token-days"
type="number"
min="1"
step="1"
value={customDays}
onChange={(event) => setCustomDays(event.target.value)}
className="w-full px-3 py-2 bg-transparent border border-border font-mono text-[12px] text-ink focus:outline-none focus:border-ink"
disabled={issuingToken}
/>
</div>
)}
<button type="button" className="btn-brutalist" onClick={issueToken} disabled={issuingToken}>
{issuingToken ? t('agentRegistration.issuing') : t('agentRegistration.issueApiToken')}
</button>
</div>
)}
{(validationError || tokenError) && (
<p className="font-mono text-[11px] text-red-600 mt-2" role="alert">
{validationError ?? tokenError}
</p>
)}
</section>
<div className="flex justify-end pt-2">
<button type="button" className="btn-brutalist" onClick={onContinue} disabled={!token}>
{t('common.continue')}
</button>
</div>
</div>
);
}
@@ -0,0 +1,101 @@
'use client';
import { useState, type FormEvent } from 'react';
import { useTranslation } from 'react-i18next';
import type { IntegrationCatalogItem } from '@/features/agent-registration/integrations';
export interface AgentDetails {
readonly displayName: string;
readonly responsibleEntity?: string;
}
interface DetailsStepProps {
readonly integration: IntegrationCatalogItem;
readonly submitting: boolean;
readonly blocked: boolean;
readonly error: string | null;
readonly onSubmit: (details: AgentDetails) => void;
readonly onBack: () => void;
}
export default function DetailsStep({
integration,
submitting,
blocked,
error,
onSubmit,
onBack,
}: DetailsStepProps) {
const { t } = useTranslation();
const [displayName, setDisplayName] = useState('');
const [responsibleEntity, setResponsibleEntity] = useState('');
const [validationError, setValidationError] = useState<string | null>(null);
const submit = (event: FormEvent) => {
event.preventDefault();
const normalizedName = displayName.trim();
if (!normalizedName) {
setValidationError(t('agentRegistration.displayNameRequired'));
return;
}
setValidationError(null);
onSubmit({
displayName: normalizedName,
responsibleEntity: responsibleEntity.trim() || undefined,
});
};
return (
<form onSubmit={submit} className="space-y-5">
<div className="flex items-center justify-between border-b border-border pb-3">
<span className="section-label">{t('agentRegistration.integration')}</span>
<span className="font-mono text-[12px] text-ink">{integration.name}</span>
</div>
{(validationError || error) && (
<div className="px-3 py-2 border border-red-300 bg-red-50 text-red-700 font-mono text-[11px]" role="alert">
{validationError ?? error}
</div>
)}
<div>
<label htmlFor="agent-display-name" className="section-label block mb-1.5">
{t('agentRegistration.displayName')}
</label>
<input
id="agent-display-name"
value={displayName}
onChange={(event) => setDisplayName(event.target.value)}
className="w-full px-3 py-2 bg-transparent border border-border font-mono text-[13px] text-ink placeholder:text-ink-dim focus:outline-none focus:border-ink"
placeholder={t('agentRegistration.displayNamePlaceholder')}
disabled={submitting || blocked}
required
autoFocus
/>
</div>
<div>
<label htmlFor="agent-responsible-entity" className="section-label block mb-1.5">
{t('agentRegistration.responsibleEntity')}
</label>
<input
id="agent-responsible-entity"
value={responsibleEntity}
onChange={(event) => setResponsibleEntity(event.target.value)}
className="w-full px-3 py-2 bg-transparent border border-border font-mono text-[13px] text-ink placeholder:text-ink-dim focus:outline-none focus:border-ink"
placeholder={t('agentRegistration.responsibleEntityPlaceholder')}
disabled={submitting || blocked}
/>
</div>
<div className="flex items-center justify-between pt-2">
<button type="button" className="btn-ghost" onClick={onBack} disabled={submitting || blocked}>
{t('common.back')}
</button>
<button type="submit" className="btn-brutalist" disabled={submitting || blocked}>
{submitting ? t('agentRegistration.submitting') : t('agentRegistration.submit')}
</button>
</div>
</form>
);
}
@@ -0,0 +1,54 @@
'use client';
import { useTranslation } from 'react-i18next';
import {
INTEGRATION_CATALOG,
type IntegrationCatalogItem,
type IntegrationMode,
} from '@/features/agent-registration/integrations';
interface IntegrationStepProps {
readonly onSelect: (integration: IntegrationCatalogItem) => void;
}
function IntegrationGroup({
mode,
onSelect,
}: {
readonly mode: IntegrationMode;
readonly onSelect: (integration: IntegrationCatalogItem) => void;
}) {
const { t } = useTranslation();
const integrations = INTEGRATION_CATALOG.filter((item) => item.mode === mode);
return (
<section aria-labelledby={`integration-${mode}`}>
<h3 id={`integration-${mode}`} className="section-label mb-2">
{t(`agentRegistration.integrationGroups.${mode}`)}
</h3>
<div className="grid sm:grid-cols-2 border-t border-border sm:[&>button:nth-child(odd)]:border-r">
{integrations.map((integration) => (
<button
key={integration.id}
type="button"
onClick={() => onSelect(integration)}
className="group flex items-center justify-between gap-3 px-3 py-3 text-left border-b border-border hover:bg-surface focus-visible:bg-surface focus-visible:outline-none"
>
<span className="font-mono text-[12px] text-ink">{integration.name}</span>
<span className="font-mono text-[10px] text-ink-dim opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 transition-opacity" aria-hidden="true">
→
</span>
</button>
))}
</div>
</section>
);
}
export default function IntegrationStep({ onSelect }: IntegrationStepProps) {
return (
<div className="space-y-6">
<IntegrationGroup mode="adapter" onSelect={onSelect} />
<IntegrationGroup mode="sdk" onSelect={onSelect} />
</div>
);
}
@@ -0,0 +1,28 @@
'use client';
import { useTranslation } from 'react-i18next';
const STEPS = ['integration', 'details', 'credentials', 'connect'] as const;
export type RegistrationStep = (typeof STEPS)[number];
export default function RegistrationProgress({ current }: { current: RegistrationStep }) {
const { t } = useTranslation();
const activeIndex = STEPS.indexOf(current);
return (
<ol className="grid grid-cols-4 border-b border-border mb-6" aria-label={t('agentRegistration.progress')}>
{STEPS.map((step, index) => (
<li
key={step}
className={`pb-2 font-mono text-[10px] uppercase tracking-wider border-b -mb-px ${
index <= activeIndex ? 'border-ink text-ink' : 'border-transparent text-ink-dim'
}`}
aria-current={index === activeIndex ? 'step' : undefined}
>
<span className="mr-1.5 tabular-nums">{index + 1}</span>
<span className="hidden sm:inline">{t(`agentRegistration.steps.${step}`)}</span>
</li>
))}
</ol>
);
}