Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions apps/dashboard/components/HarnessAuthModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,25 @@ import { useState } from 'react'
import { inputCls } from '../lib/utils'
import { HARNESS_AUTH } from '../lib/harness-auth'

// Native auth for the run-harness harnesses (codex/pi/vibe/kimi) — the generic
// Native auth for the adapter-backed harnesses — the generic
// parallel to GrokAuthModal. codex/kimi offer a one-click native login (browser/
// device OAuth, captured for CI); every one of the four also takes a provider
// API key, and all fall back to the shared OpenRouter key. Posts to
// device OAuth, captured for CI); others take a provider API key. Some harnesses
// also fall back to the shared OpenRouter key. Posts to
// /api/harness-auth via onHarnessAuth (no arg = OAuth, {key} = provider key).

const TITLES: Record<string, string> = { codex: 'Codex', kimi: 'Kimi', pi: 'Pi', vibe: 'Vibe' }
const TITLES: Record<string, string> = {
codex: 'Codex',
kimi: 'Kimi',
pi: 'Pi',
vibe: 'Vibe',
cursor: 'Cursor CLI',
hermes: 'Hermes',
glm: 'GLM Coding Plan',
}
const OAUTH_BLURB: Record<string, string> = {
codex: 'Run codex on your ChatGPT plan. A browser tab opens to approve on OpenAI; the session is captured for CI. Needs the codex CLI installed.',
kimi: 'Run kimi on your Moonshot account (device-code flow). A browser tab opens to approve; the session is captured for CI. Needs the kimi CLI installed.',
hermes: 'Run Hermes through Nous Portal. A browser tab opens for the official Nous OAuth flow; auth.json and config.yaml are captured for CI. Needs the Hermes CLI installed.',
}

interface HarnessAuthModalProps {
Expand Down Expand Up @@ -49,7 +58,10 @@ export function HarnessAuthModal({ harness, loading, onClose, onHarnessAuth }: H
{spec.apiKey && (
<>
{spec.oauth && <div className="my-[var(--space-md)] border-t border-[rgba(250,250,250,0.10)]" />}
<p className="text-xs text-primary-50 font-mono mb-[var(--space-md)]">{spec.oauth ? 'Or use a provider API key (no browser flow).' : 'Paste a provider API key.'} Any of the four also runs on the shared OpenRouter key.</p>
<p className="text-xs text-primary-50 font-mono mb-[var(--space-md)]">
{spec.oauth ? 'Or use a provider API key (no browser flow).' : 'Paste a provider API key.'}
{spec.authSecrets.includes('OPENROUTER_API_KEY') && ' This harness can also use the shared OpenRouter key.'}
</p>
<input type="password" value={key} onChange={(e) => setKey(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && submitKey()} placeholder={spec.apiKey.placeholder} className={`${inputCls} mb-[var(--space-md)]`} />
<button onClick={submitKey} disabled={!key.trim() || loading} className="w-full bg-aeon-panel text-aeon-fg border border-[rgba(250,250,250,0.14)] text-sm py-3 font-mono uppercase tracking-[2px] hover:border-aeon-red transition-colors disabled:opacity-50">{loading ? '...' : 'Save key'}</button>
</>
Expand Down
5 changes: 1 addition & 4 deletions apps/dashboard/components/SecretsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { InstantModeCard } from './InstantModeCard'
import { LangfuseRegionCard } from './LangfuseRegionCard'
import { TelegramCommandsCard } from './TelegramCommandsCard'
import { TelegramChatIdHelper } from './TelegramChatIdHelper'
import { OAUTH_SECRET_HARNESS } from '../lib/harness-auth'

// Logo shown next to each credential group's header. Brand groups use their
// favicon; non-brand groups use a glyph.
Expand Down Expand Up @@ -51,10 +52,6 @@ interface SecretsPanelProps {
harnessConnecting?: boolean
}

// The OAuth-capture secrets (a tar of a login you can't paste) → the harness
// whose native login sets them. Rendered with a Connect button, like Claude/Grok.
const OAUTH_SECRET_HARNESS: Record<string, string> = { CODEX_AUTH: 'codex', KIMI_AUTH: 'kimi' }

export function SecretsPanel({ secrets, skills, busy, repo, harness, focusKey, onFocusHandled, onSave, onDelete, onSelectSkill, onConnectClaude, connecting, onConnectGrok, grokConnecting, onConnectHarness, harnessConnecting }: SecretsPanelProps) {
const [editingSecret, setEditingSecret] = useState<string | null>(null)
const [secretValue, setSecretValue] = useState('')
Expand Down
19 changes: 19 additions & 0 deletions apps/dashboard/lib/harness-auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, it } from 'node:test'
import { strict as assert } from 'node:assert'

import { OAUTH_SECRET_HARNESS } from './harness-auth'

describe('OAuth harness credentials', () => {
it('maps every captured login secret to its Connect/Reconnect flow', () => {
assert.deepEqual(OAUTH_SECRET_HARNESS, {
CODEX_AUTH: 'codex',
KIMI_AUTH: 'kimi',
HERMES_AUTH: 'hermes',
})
})

it('does not treat pasteable API keys as captured login sessions', () => {
assert.equal(OAUTH_SECRET_HARNESS.CURSOR_API_KEY, undefined)
assert.equal(OAUTH_SECRET_HARNESS.GLM_API_KEY, undefined)
})
})
9 changes: 9 additions & 0 deletions apps/dashboard/lib/harness-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ const HARNESS_AUTH_SPECS = {
// sites were dead code the compiler believed could never fire.
export const HARNESS_AUTH: Record<string, HarnessAuthSpec | undefined> = HARNESS_AUTH_SPECS

// Settings uses this to render Connect/Reconnect for every captured OAuth
// credential. Derive it from the registry so adding a harness in one place
// cannot leave its secret looking like a generic pasteable API key.
export const OAUTH_SECRET_HARNESS: Readonly<Record<string, string>> = Object.fromEntries(
Object.entries(HARNESS_AUTH).flatMap(([harness, spec]) =>
spec?.oauth ? [[spec.oauth.secret, harness]] : [],
),
)

// The URL a device-auth flow prints for the operator to approve in the browser.
// Permissive on purpose — codex (ChatGPT) and kimi (Moonshot) print different
// hosts, and we only need the first https URL to hand to openBrowser.
Expand Down
10 changes: 6 additions & 4 deletions apps/dashboard/lib/secrets-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ export const BUILTIN_SECRETS: Omit<Secret, 'isSet'>[] = [
{ name: 'ANTHROPIC_API_KEY', group: 'Core', description: 'How Claude Code signs in - option 2 of 2. A pay-as-you-go Anthropic API key (sk-ant-...) billed via the Console, or any Anthropic-compatible key for a proxy. Create one at console.anthropic.com.', either: 'auth' },
{ name: 'BANKR_LLM_KEY', group: 'Core', description: 'Bankr Gateway API key (bk_...) - enable at bankr.bot/api-keys' },
{ name: 'OPENROUTER_API_KEY', group: 'Core', description: 'OpenRouter API key (sk-or-...) - dual purpose: (1) routes Claude Code through openrouter.ai (a gateway provider), and (2) the shared fallback auth for the codex, pi, vibe and kimi harnesses, which all run on OpenRouter via run-harness. One key covers all four. Create at openrouter.ai/keys' },
// Native harness auth - each of the run-harness harnesses can run on its OWN
// provider instead of the shared OpenRouter key. The two OAuth captures
// (CODEX_AUTH/KIMI_AUTH) are set by `aeon auth --harness <h>` or the dashboard's
// Connect button, which drive the CLI's login and store the captured session.
// Native harness auth - each run-harness harness can use its own provider.
// OAuth captures are set by `aeon auth --harness <h>` or the dashboard's
// Connect button, which drives the CLI login and stores the captured session.
{ name: 'CODEX_AUTH', group: 'Core', description: 'Codex (ChatGPT) login captured for CI - a base64 tar of ~/.codex/auth.json. Set it with `aeon auth --harness codex` or the dashboard "Connect ChatGPT" button (runs `codex login`, stores the session here). Runs the codex harness on your ChatGPT plan instead of OpenRouter.' },
{ name: 'KIMI_AUTH', group: 'Core', description: 'Kimi (Moonshot) device login captured for CI - a base64 tar of ~/.kimi-code/credentials and config.toml. Set it with `aeon auth --harness kimi` or the dashboard "Connect Kimi" button. Runs the kimi harness on your Moonshot account instead of OpenRouter.' },
{ name: 'CURSOR_API_KEY', group: 'Core', description: 'Cursor API key for the Cursor CLI harness. Set it here or with `aeon auth --harness cursor --key`; the key is stored as a GitHub Actions secret and never committed. Create one in Cursor settings.' },
{ name: 'HERMES_AUTH', group: 'Core', description: 'Hermes Nous Portal login captured for CI - a base64 tar of ~/.hermes/auth.json and config.yaml. Set it with `aeon auth --harness hermes` or the dashboard "Connect Nous Portal" button, which opens the official Nous OAuth flow and stores the resulting session.' },
{ name: 'GLM_API_KEY', group: 'Core', description: 'Z.AI GLM Coding Plan API key for the GLM harness. Set it here or with `aeon auth --harness glm --key`; the key is stored as a GitHub Actions secret and never committed. Create one in the Z.AI Coding Plan console.' },
{ name: 'OPENAI_API_KEY', group: 'Core', description: 'OpenAI API key (sk-...) - API-key auth for the codex harness (alternative to the ChatGPT login) and a provider pi can use. Create at platform.openai.com/api-keys' },
{ name: 'MOONSHOT_API_KEY', group: 'Core', description: 'Moonshot API key - API-key auth for the kimi harness (alternative to the device login). From platform.moonshot.ai' },
{ name: 'MISTRAL_API_KEY', group: 'Core', description: "Mistral API key - native auth for the vibe harness (its default provider). vibe's own `vibe` sign-in stores its credential in the OS keychain (not a portable file, like Claude Code), so it can't be captured for CI - set the key here instead. Create at console.mistral.ai" },
Expand Down
10 changes: 10 additions & 0 deletions apps/dashboard/lib/service-icon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,19 @@ describe("resolveServiceMark", () => {

it("routes a credential to its brand favicon", () => {
assert.match(resolveServiceMark({ name: "CODEX_AUTH" }).src ?? "", /openai\.com/);
assert.match(resolveServiceMark({ name: "CURSOR_API_KEY" }).src ?? "", /cursor\.com/);
assert.match(resolveServiceMark({ name: "HERMES_AUTH" }).src ?? "", /nousresearch\.com/);
assert.match(resolveServiceMark({ name: "GLM_API_KEY" }).src ?? "", /z\.ai/);
assert.match(resolveServiceMark({ name: "GH_SECRETS_PAT" }).src ?? "", /github\.com/);
});

it("keeps every new harness credential visible even before it is set", () => {
const core = new Map(BUILTIN_SECRETS.map(secret => [secret.name, secret.group]));
assert.equal(core.get("CURSOR_API_KEY"), "Core");
assert.equal(core.get("HERMES_AUTH"), "Core");
assert.equal(core.get("GLM_API_KEY"), "Core");
});

it("prefers a vendored logo over the favicon service", () => {
assert.equal(resolveServiceMark({ domain: "langfuse.com" }).src, "/icons/langfuse.svg");
});
Expand Down
3 changes: 3 additions & 0 deletions apps/dashboard/lib/service-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const DOMAINS: Record<string, string> = {
OPENAI_API_KEY: 'openai.com',
KIMI_AUTH: 'kimi.com',
MOONSHOT_API_KEY: 'moonshot.ai',
CURSOR_API_KEY: 'cursor.com',
HERMES_AUTH: 'nousresearch.com',
GLM_API_KEY: 'z.ai',
MISTRAL_API_KEY: 'mistral.ai',
...GATEWAY_DOMAINS,
// Channels
Expand Down
Loading