From 98ce68b992411cdc5d216892b35f890185306e88 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:29:12 -0700 Subject: [PATCH 1/4] Keep spawn credentials out of agent Settings and reap expired children Co-authored-by: tern Agent-Model: codex/gpt-6-sol/medium Agent-Model: codex/gpt-6.1-sol/medium --- .../service/runtimeTokenRevoke.test.js | 45 +++++++++++ backend/models/AgentCredential.ts | 7 ++ backend/routes/credentials.ts | 16 +++- backend/services/schedulerService.ts | 16 ++++ backend/services/spawnCredentialReaper.ts | 17 ++++ frontend/src/i18n/locales/en.json | 4 + frontend/src/i18n/locales/zh-CN.json | 4 + .../v2/__tests__/V2AgentSettingsPage.test.tsx | 35 ++++++++- .../src/v2/agents/V2AgentSettingsPage.tsx | 78 +++++++++++-------- frontend/src/v2/agents/v2-agent-settings.css | 5 ++ 10 files changed, 191 insertions(+), 36 deletions(-) create mode 100644 backend/services/spawnCredentialReaper.ts diff --git a/backend/__tests__/service/runtimeTokenRevoke.test.js b/backend/__tests__/service/runtimeTokenRevoke.test.js index 479b27471f..9de3b2eda5 100644 --- a/backend/__tests__/service/runtimeTokenRevoke.test.js +++ b/backend/__tests__/service/runtimeTokenRevoke.test.js @@ -39,6 +39,8 @@ jest.mock('../../services/starterTaskService', () => ({ completeConnectAgentStar const Pod = require('../../models/Pod'); const User = require('../../models/User'); const AgentCredential = require('../../models/AgentCredential'); +const Machine = require('../../models/Machine'); +const { reapSpawnCredentials, SPAWN_CREDENTIAL_RETENTION_MS } = require('../../services/spawnCredentialReaper'); const { AgentInstallation } = require('../../models/AgentRegistry'); const AgentIdentityService = require('../../services/agentIdentityService'); const { issueRuntimeTokenForAgent } = require('../../routes/registry/tokens'); @@ -93,6 +95,49 @@ describe('revoking a runtime token kills it in every store', () => { }); afterEach(() => jest.restoreAllMocks()); + test('Settings lists long-lived keys with machine names, without spawn children', async () => { + await seed(); + const agentUser = await AgentIdentityService.getOrCreateAgentUser(AGENT, { instanceId: 'default' }); + const machineId = 'machine-settings-key'; + await Machine.create({ ownerUserId: mockOwner.id, machineId, name: "Sam's MacBook" }); + await AgentCredential.create([ + { tokenHash: 'long-lived-key', kind: 'runtime', ownerUserId: mockOwner.id, + agentUserId: agentUser._id, machineId, label: 'Runtime token' }, + { tokenHash: 'spawn-child-key', kind: 'runtime', ownerUserId: mockOwner.id, + agentUserId: agentUser._id, machineId, label: 'spawn:batch-1', scopes: ['spawn'], + expiresAt: new Date(Date.now() + 60_000) }, + ]); + const response = await request(app).get(`/api/credentials?agentName=${AGENT}&instanceId=default`); + expect(response.status).toBe(200); + expect(response.body).toEqual([expect.objectContaining({ + label: 'Runtime token', machineName: "Sam's MacBook", status: 'active', + })]); + expect(JSON.stringify(response.body)).not.toContain('spawn:'); + await AgentCredential.deleteOne({ tokenHash: 'long-lived-key' }); + const spawnOnly = await request(app).get(`/api/credentials?agentName=${AGENT}&instanceId=default`); + expect(spawnOnly.status).toBe(200); + expect(spawnOnly.body).toEqual([]); + }); + + test('reaper removes old spawn children only, including revoked children', async () => { + const now = new Date('2026-10-24T08:00:00Z'); + const cutoff = new Date(now.getTime() - SPAWN_CREDENTIAL_RETENTION_MS); + const agentUser = await AgentIdentityService.getOrCreateAgentUser(AGENT, { instanceId: 'default' }); + const common = { kind: 'runtime', ownerUserId: mockOwner.id, agentUserId: agentUser._id }; + await AgentCredential.create([ + { ...common, tokenHash: 'expired-spawn', scopes: ['spawn'], + expiresAt: new Date(cutoff.getTime() - 1000) }, + { ...common, tokenHash: 'revoked-spawn', scopes: ['spawn'], status: 'revoked', + revokedAt: new Date(cutoff.getTime() - 1000), expiresAt: new Date(cutoff.getTime() - 1000) }, + { ...common, tokenHash: 'recent-spawn', scopes: ['spawn'], + expiresAt: new Date(cutoff.getTime() + 1000) }, + { ...common, tokenHash: 'old-runtime', expiresAt: new Date(cutoff.getTime() - 1000) }, + ]); + expect(await reapSpawnCredentials(now)).toBe(2); + expect((await AgentCredential.find({}).select('tokenHash').lean()).map((row) => row.tokenHash).sort()) + .toEqual(['old-runtime', 'recent-spawn']); + }); + test('the owner revoking it from the token list: the next runtime call is a 401', async () => { const { workspace } = await seed(); const issued = await request(app) diff --git a/backend/models/AgentCredential.ts b/backend/models/AgentCredential.ts index 7eaba5271d..8ba30beb4b 100644 --- a/backend/models/AgentCredential.ts +++ b/backend/models/AgentCredential.ts @@ -61,6 +61,13 @@ AgentCredentialSchema.index({ ownerUserId: 1, status: 1 }); AgentCredentialSchema.index({ parentId: 1 }); AgentCredentialSchema.index({ agentUserId: 1 }); AgentCredentialSchema.index({ machineId: 1, kind: 1, status: 1 }); +// Mongo's TTL monitor is the storage backstop for per-spawn children. The +// scheduler also reaps them so cleanup is observable and does not depend on +// the monitor catching up after a rollout. +AgentCredentialSchema.index( + { expiresAt: 1 }, + { expireAfterSeconds: 3600, partialFilterExpression: { scopes: 'spawn' } }, +); // Revoke a credential AND every descendant, breadth-first. One level deep // today (daemon → runtime), but written as a walk so a deeper chain can diff --git a/backend/routes/credentials.ts b/backend/routes/credentials.ts index 192d54ac4c..49a8b9c8d8 100644 --- a/backend/routes/credentials.ts +++ b/backend/routes/credentials.ts @@ -9,6 +9,7 @@ import rateLimit from 'express-rate-limit'; import { cloudflareIpRateLimitKeyGenerator } from '../middleware/ipRateLimit'; import { createHash } from 'crypto'; import AgentCredential from '../models/AgentCredential'; +import Machine from '../models/Machine'; import { AgentInstallation } from '../models/AgentRegistry'; import { ownsRuntimeIdentity } from '../services/runtimeIdentityOwnership'; import { revokeRuntimeTokenEverywhere } from './registry/tokens'; @@ -83,13 +84,24 @@ router.get('/', credentialRateLimit, auth, async (req: AuthReq, res: express.Res } }, { upsert: true }, ))); - filter = { agentUserId: agent._id, kind: 'runtime' }; + // Spawn children are short-lived implementation credentials. Filtering + // in Mongo keeps a busy seat's thousands of children out of Settings. + filter = { agentUserId: agent._id, kind: 'runtime', scopes: { $ne: 'spawn' } }; } const rows = await AgentCredential.find(filter) .select('-tokenHash') .sort({ createdAt: -1 }) .lean(); - return res.json(rows); + const machineIds = [...new Set(rows.map((row) => row.machineId).filter(Boolean))]; + const machines = machineIds.length + ? await Machine.find({ machineId: { $in: machineIds } }).select('machineId name').lean() + : []; + const machineNames = new Map(machines.map((machine) => [machine.machineId, machine.name])); + return res.json(rows.map((row) => ({ + ...row, + ...(row.machineId && machineNames.has(row.machineId) + ? { machineName: machineNames.get(row.machineId) } : {}), + }))); } catch (err) { console.error('Error listing credentials:', err); return res.status(500).json({ message: 'Server error' }); diff --git a/backend/services/schedulerService.ts b/backend/services/schedulerService.ts index 3343e51110..b90bf8b011 100644 --- a/backend/services/schedulerService.ts +++ b/backend/services/schedulerService.ts @@ -527,6 +527,21 @@ class SchedulerService { { scheduled: false, timezone: 'UTC' }, ); + const spawnCredentialReapJob: CronJob = cron.schedule( + '*/5 * * * *', + async () => { + try { + // eslint-disable-next-line global-require, @typescript-eslint/no-require-imports + const { reapSpawnCredentials } = require('./spawnCredentialReaper'); + const removed = await reapSpawnCredentials(); + if (removed) console.log(`[spawn-credentials] reaped=${removed}`); + } catch (error) { + console.error('[spawn-credentials] reap failed:', error); + } + }, + { scheduled: false, timezone: 'UTC' }, + ); + // TASK-260: remove the stranger journey's throwaway accounts (they cannot // delete themselves). Half an hour after each 02:17/14:17 journey run; // only accounts older than two hours, so a run in flight is never touched. @@ -587,6 +602,7 @@ class SchedulerService { installableReconcileJob, decisionCardReconcileJob, approvalToolCallSweepJob, + spawnCredentialReapJob, spendAlarmJob, ...(process.env.SYNTHETIC_ACCOUNT_PURGE === 'true' ? [syntheticAccountPurgeJob] : []), ]; diff --git a/backend/services/spawnCredentialReaper.ts b/backend/services/spawnCredentialReaper.ts new file mode 100644 index 0000000000..83ffa50d1d --- /dev/null +++ b/backend/services/spawnCredentialReaper.ts @@ -0,0 +1,17 @@ +import AgentCredential from '../models/AgentCredential'; + +// Keep a short window for diagnosing a finished spawn, then remove its leaf +// credential. Authorization already stops at expiresAt; this is storage GC. +export const SPAWN_CREDENTIAL_RETENTION_MS = 60 * 60 * 1000; + +export async function reapSpawnCredentials(now: Date = new Date()): Promise { + const cutoff = new Date(now.getTime() - SPAWN_CREDENTIAL_RETENTION_MS); + const result = await AgentCredential.deleteMany({ + scopes: 'spawn', + expiresAt: { $lte: cutoff }, + }); + return result.deletedCount || 0; +} + +module.exports = { SPAWN_CREDENTIAL_RETENTION_MS, reapSpawnCredentials }; +Object.assign(module.exports, exports); diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 604a52d729..479a006536 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2424,6 +2424,10 @@ "done": "Done", "empty": "No runtime keys yet.", "runtimeKey": "Runtime key", + "cliKey": "CLI key", + "daemonKey": "Daemon key", + "noActiveKeys": "No active keys.", + "revokedCount": "{{count}} revoked", "active": "Active", "revoked": "Revoked", "created": "Created {{date}}", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index c3c3e779c7..41e9257cc8 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2415,6 +2415,10 @@ "done": "完成", "empty": "暂无运行时密钥。", "runtimeKey": "运行时密钥", + "cliKey": "CLI 密钥", + "daemonKey": "守护进程密钥", + "noActiveKeys": "没有有效的密钥。", + "revokedCount": "已撤销 {{count}} 个", "active": "有效", "revoked": "已撤销", "created": "创建于 {{date}}", diff --git a/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx b/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx index 7ef96b0156..3fbd6200d5 100644 --- a/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx @@ -218,7 +218,7 @@ test('nondefault wrapper instances never link to a default-instance reconnect', test('regeneration waits for the explicit running-key replacement confirmation', async () => { axios.post.mockResolvedValue({ data: { token: 'cm_agent_once' } }); renderPage(); - await screen.findByText('Laptop'); + await screen.findByText('Runtime key · Laptop'); fireEvent.click(screen.getByRole('button', { name: 'Regenerate key' })); expect(axios.post).not.toHaveBeenCalled(); expect(screen.getByTestId('agent-key-replace-confirmation')).toHaveTextContent('Replacing it will disconnect'); @@ -260,7 +260,7 @@ test('an in-flight replacement never reveals its key on another agent page', asy , ); - await screen.findByText('Laptop'); + await screen.findByText('Runtime key · Laptop'); fireEvent.click(screen.getByRole('button', { name: 'Regenerate key' })); fireEvent.click(screen.getByRole('button', { name: /replace the running agent/i })); await waitFor(() => expect(axios.post).toHaveBeenCalledTimes(1)); @@ -302,7 +302,7 @@ test('a partial installation owner cannot see identity-wide key controls', async test('revocation uses the credential id only after confirmation', async () => { axios.delete.mockResolvedValue({ data: { revoked: 1 } }); renderPage(); - await screen.findByText('Laptop'); + await screen.findByText('Runtime key · Laptop'); fireEvent.click(screen.getByRole('button', { name: 'Revoke' })); expect(axios.delete).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole('button', { name: 'Revoke key' })); @@ -311,6 +311,35 @@ test('revocation uses the credential id only after confirmation', async () => { })); }); +test('shows active keys first and keeps revoked history collapsed', async () => { + const defaultGet = axios.get.getMockImplementation(); + axios.get.mockImplementation((url, options) => ( + url === '/api/credentials' ? Promise.resolve({ data: [ + { _id: 'old', label: 'Old token', status: 'revoked', revokedAt: '2026-10-02T00:00:00Z' }, + { _id: 'daemon', label: 'Runtime token', machineId: 'opaque-id', machineName: "Sam's MacBook", status: 'active' }, + ] }) : defaultGet(url, options) + )); + renderPage(); + expect(await screen.findByText("Daemon key · Sam's MacBook")).toBeInTheDocument(); + const disclosure = screen.getByText('1 revoked'); + expect(disclosure.closest('details')).not.toHaveAttribute('open'); + fireEvent.click(disclosure); + expect(disclosure.closest('details')).toHaveAttribute('open'); + expect(screen.getByText('Runtime key · Old token')).toBeInTheDocument(); +}); + +test('a revoked-only key list has a useful empty state and accessible history', async () => { + const defaultGet = axios.get.getMockImplementation(); + axios.get.mockImplementation((url, options) => ( + url === '/api/credentials' ? Promise.resolve({ data: [ + { _id: 'old', label: 'Previous key', status: 'revoked' }, + ] }) : defaultGet(url, options) + )); + renderPage(); + expect(await screen.findByText('No active keys.')).toBeInTheDocument(); + expect(screen.getByText('1 revoked').closest('details')).not.toHaveAttribute('open'); +}); + test('selects an installation the owner can manage without exposing a foreign pod', async () => { axios.get.mockImplementation((url) => { if (url.includes('/manage-settings')) return Promise.resolve({ data: { diff --git a/frontend/src/v2/agents/V2AgentSettingsPage.tsx b/frontend/src/v2/agents/V2AgentSettingsPage.tsx index 1cbe33d8a4..ee732975fc 100644 --- a/frontend/src/v2/agents/V2AgentSettingsPage.tsx +++ b/frontend/src/v2/agents/V2AgentSettingsPage.tsx @@ -16,6 +16,8 @@ import './v2-agent-settings.css'; interface RuntimeKey { _id: string; label: string; + machineId?: string | null; + machineName?: string; createdAt?: string; lastUsedAt?: string | null; revokedAt?: string | null; @@ -184,6 +186,42 @@ const V2AgentSettingsPage: React.FC = () => { ...runtimeControls, ...(runtimeManaged !== undefined ? { managed: runtimeManaged } : {}), }; + const activeKeys = keys.filter((key) => key.status === 'active'); + const revokedKeys = keys.filter((key) => key.status === 'revoked'); + const keyLabel = (key: RuntimeKey): string => { + const defaultLabel = !key.label || key.label === 'Runtime token' || key.label === 'Agent Settings replacement'; + const kind = key.machineId ? t('agentSettings.daemonKey') + : runtime.runtimeType === 'wrapper' ? t('agentSettings.cliKey') : t('agentSettings.runtimeKey'); + const detail = key.machineId ? key.machineName : (defaultLabel ? null : key.label); + return detail ? `${kind} · ${detail}` : kind; + }; + const renderKey = (key: RuntimeKey) => ( +
  • +
    + {keyLabel(key)} + {key.status === 'revoked' ? t('agentSettings.revoked') : t('agentSettings.active')} +
    +
    + {t('agentSettings.created', { date: dateLabel(key.createdAt, t) })} + {t('agentSettings.lastUsed', { date: dateLabel(key.lastUsedAt, t) })} + {key.status === 'revoked' && {t('agentSettings.revokedAt', { date: dateLabel(key.revokedAt, t) })}} +
    + {key.status === 'active' && pendingAction === key._id && ( +
    +

    {t('agentSettings.revokeWarning')}

    +
    + + +
    +
    + )} + {key.status === 'active' && pendingAction !== key._id && ( + + )} +
  • + ); return (
    @@ -222,37 +260,15 @@ const V2AgentSettingsPage: React.FC = () => { )} - {keysForbidden ?

    {t('agentSettings.keyUnavailable')}

    - : keys.length === 0 ?

    {t('agentSettings.empty')}

    : ( -
      - {keys.map((key) => ( -
    • -
      - {key.label || t('agentSettings.runtimeKey')} - {key.status === 'revoked' ? t('agentSettings.revoked') : t('agentSettings.active')} -
      -
      - {t('agentSettings.created', { date: dateLabel(key.createdAt, t) })} - {t('agentSettings.lastUsed', { date: dateLabel(key.lastUsedAt, t) })} - {key.status === 'revoked' && {t('agentSettings.revokedAt', { date: dateLabel(key.revokedAt, t) })}} -
      - {key.status === 'active' && pendingAction === key._id && ( -
      -

      {t('agentSettings.revokeWarning')}

      -
      - - -
      -
      - )} - {key.status === 'active' && pendingAction !== key._id && ( - - )} -
    • - ))} -
    + {keysForbidden ?

    {t('agentSettings.keyUnavailable')}

    : ( + <> + {activeKeys.length ?
      {activeKeys.map(renderKey)}
    + :

    {t('agentSettings.noActiveKeys')}

    } + {revokedKeys.length > 0 &&
    + {t('agentSettings.revokedCount', { count: revokedKeys.length })} +
      {revokedKeys.map(renderKey)}
    +
    } + )} {installation && } diff --git a/frontend/src/v2/agents/v2-agent-settings.css b/frontend/src/v2/agents/v2-agent-settings.css index 44473bbd38..09af72d833 100644 --- a/frontend/src/v2/agents/v2-agent-settings.css +++ b/frontend/src/v2/agents/v2-agent-settings.css @@ -69,6 +69,10 @@ .v2-agent-settings__error { color: var(--v2-danger); } .v2-agent-settings__empty { margin-top: 24px; color: var(--v2-text-secondary); } .v2-agent-settings__keys { list-style: none; padding: 0; margin: 20px 0 0; } +.v2-agent-settings__revoked-keys { margin-top: 20px; border-top: 1px solid var(--v2-border-soft); padding-top: 14px; } +.v2-agent-settings__revoked-keys summary { width: fit-content; cursor: pointer; color: var(--v2-text-secondary); font-family: var(--v2-font-mono); font-size: 12px; } +.v2-agent-settings__revoked-keys summary:focus-visible { box-shadow: var(--v2-focus-ring); outline: 1px solid var(--v2-accent); } +.v2-agent-settings__revoked-keys .v2-agent-settings__keys { margin-top: 8px; } .v2-agent-settings__key { position: relative; padding: 14px 0; border-top: 1px solid var(--v2-border-soft); } .v2-agent-settings__key-main { display: flex; gap: 12px; align-items: baseline; font-size: 14px; } .v2-agent-settings__key-main span { color: var(--v2-text-tertiary); font-size: 12px; } @@ -102,5 +106,6 @@ @media (max-width: 760px) { .v2-agent-settings { padding: 16px; } + .v2-agent-settings__section-head { flex-direction: column; align-items: flex-start; } .v2-agent-settings__key-meta { display: grid; gap: 4px; } } From 1bbd4b5eee240607a896d50b52b8f21426e769f0 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:32:41 -0700 Subject: [PATCH 2/4] Pin agent Settings phone header layout Co-authored-by: tern Agent-Model: codex/gpt-6-sol/medium Agent-Model: codex/gpt-6.1-sol/medium --- frontend/src/v2/__tests__/v2-layout-invariants.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 890abb1f21..4b8ff883a9 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -185,6 +185,13 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(mediaBlockContaining(agentSettings, '.v2-agent-settings__key-meta')).toContain('display: grid'); }); + test('agent Settings section headers stack at phone width so the key hint stays visible', () => { + const phone = mediaBlockContaining(agentSettings, '.v2-agent-settings__section-head'); + const header = selectorRuleBody(phone, '.v2-agent-settings__section-head'); + expect(header).toContain('flex-direction: column'); + expect(header).toContain('align-items: flex-start'); + }); + test('agent Settings buttons retain their chrome over the v2 button reset', () => { const button = ruleBody(agentSettings, '.v2-root .v2-agent-settings button:not(.MuiButtonBase-root)'); expect(button).toContain('border: 1px solid var(--v2-border)'); From ed047bce23ca26042c749001b805728fc4223aa2 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:37:10 -0700 Subject: [PATCH 3/4] Keep Chinese revoked count unspaced Co-authored-by: tern Agent-Model: codex/gpt-6-sol/medium Agent-Model: codex/gpt-6.1-sol/medium --- frontend/src/i18n/locales/zh-CN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 41e9257cc8..70ccce86de 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2418,7 +2418,7 @@ "cliKey": "CLI 密钥", "daemonKey": "守护进程密钥", "noActiveKeys": "没有有效的密钥。", - "revokedCount": "已撤销 {{count}} 个", + "revokedCount": "已撤销{{count}}个", "active": "有效", "revoked": "已撤销", "created": "创建于 {{date}}", From 371e15a0216c1c09515f32b5bd2eaac4ae9f07b8 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:49:38 -0700 Subject: [PATCH 4/4] Label machine-bound runtime credentials by runtime kind Co-authored-by: tern Agent-Model: codex/gpt-6.1-sol/medium --- frontend/src/i18n/locales/en.json | 1 - frontend/src/i18n/locales/zh-CN.json | 1 - frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx | 2 +- frontend/src/v2/agents/V2AgentSettingsPage.tsx | 3 +-- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 479a006536..6e0810dadf 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2425,7 +2425,6 @@ "empty": "No runtime keys yet.", "runtimeKey": "Runtime key", "cliKey": "CLI key", - "daemonKey": "Daemon key", "noActiveKeys": "No active keys.", "revokedCount": "{{count}} revoked", "active": "Active", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 70ccce86de..357b9aeea8 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2416,7 +2416,6 @@ "empty": "暂无运行时密钥。", "runtimeKey": "运行时密钥", "cliKey": "CLI 密钥", - "daemonKey": "守护进程密钥", "noActiveKeys": "没有有效的密钥。", "revokedCount": "已撤销{{count}}个", "active": "有效", diff --git a/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx b/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx index 3fbd6200d5..95e209d7df 100644 --- a/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx @@ -320,7 +320,7 @@ test('shows active keys first and keeps revoked history collapsed', async () => ] }) : defaultGet(url, options) )); renderPage(); - expect(await screen.findByText("Daemon key · Sam's MacBook")).toBeInTheDocument(); + expect(await screen.findByText("Runtime key · Sam's MacBook")).toBeInTheDocument(); const disclosure = screen.getByText('1 revoked'); expect(disclosure.closest('details')).not.toHaveAttribute('open'); fireEvent.click(disclosure); diff --git a/frontend/src/v2/agents/V2AgentSettingsPage.tsx b/frontend/src/v2/agents/V2AgentSettingsPage.tsx index ee732975fc..f7918bc560 100644 --- a/frontend/src/v2/agents/V2AgentSettingsPage.tsx +++ b/frontend/src/v2/agents/V2AgentSettingsPage.tsx @@ -190,8 +190,7 @@ const V2AgentSettingsPage: React.FC = () => { const revokedKeys = keys.filter((key) => key.status === 'revoked'); const keyLabel = (key: RuntimeKey): string => { const defaultLabel = !key.label || key.label === 'Runtime token' || key.label === 'Agent Settings replacement'; - const kind = key.machineId ? t('agentSettings.daemonKey') - : runtime.runtimeType === 'wrapper' ? t('agentSettings.cliKey') : t('agentSettings.runtimeKey'); + const kind = runtime.runtimeType === 'wrapper' ? t('agentSettings.cliKey') : t('agentSettings.runtimeKey'); const detail = key.machineId ? key.machineName : (defaultLabel ? null : key.label); return detail ? `${kind} · ${detail}` : kind; };